From b5ce8ba26ff0eb46719da0107579ee73a0b8b461 Mon Sep 17 00:00:00 2001
From: Liza Katz
Date: Thu, 4 Feb 2021 16:57:26 +0200
Subject: [PATCH 01/42] [Search Session][Management] Rename "cancel" button and
delete "Reload" button (#90015)
* Rename management button to "delete"
* fix jest
* Delete reload action from management
* Added both cancel and delete session
* Improve texts
* fix test
* ts
* doc
* fix jest
---
src/plugins/data/server/search/mocks.ts | 1 +
.../data/server/search/search_service.ts | 13 +++++--
.../data/server/search/session/mocks.ts | 1 +
.../server/search/session/session_service.ts | 3 ++
.../data/server/search/session/types.ts | 1 +
src/plugins/data/server/search/types.ts | 1 +
src/plugins/data/server/server.api.md | 3 +-
.../{cancel_button.tsx => delete_button.tsx} | 32 ++++++++---------
.../components/actions/get_action.tsx | 16 +++------
.../components/actions/popover_actions.tsx | 2 +-
.../components/actions/reload_button.tsx | 33 -----------------
.../sessions_mgmt/components/actions/types.ts | 3 +-
.../search/sessions_mgmt/lib/api.test.ts | 33 ++---------------
.../public/search/sessions_mgmt/lib/api.ts | 11 +++---
.../server/routes/session.test.ts | 36 +++++++++++++++----
.../data_enhanced/server/routes/session.ts | 23 ++++++++++++
.../server/search/session/session_service.ts | 6 ++++
.../api_integration/apis/search/session.ts | 26 ++++++++++++--
.../search_sessions_management_page.ts | 6 ++--
.../search_sessions/sessions_management.ts | 5 ++-
20 files changed, 137 insertions(+), 118 deletions(-)
rename x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/{cancel_button.tsx => delete_button.tsx} (70%)
delete mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx
diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts
index e32e3326dede0..248487f216a56 100644
--- a/src/plugins/data/server/search/mocks.ts
+++ b/src/plugins/data/server/search/mocks.ts
@@ -40,5 +40,6 @@ export function createSearchRequestHandlerContext() {
updateSession: jest.fn(),
extendSession: jest.fn(),
cancelSession: jest.fn(),
+ deleteSession: jest.fn(),
};
}
diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts
index 91d9bd6e0d284..ce0771a1e9df8 100644
--- a/src/plugins/data/server/search/search_service.ts
+++ b/src/plugins/data/server/search/search_service.ts
@@ -307,9 +307,8 @@ export class SearchService implements Plugin {
return strategy.extend(id, keepAlive, options, deps);
};
- private cancelSession = async (deps: SearchStrategyDependencies, sessionId: string) => {
+ private cancelSessionSearches = async (deps: SearchStrategyDependencies, sessionId: string) => {
const searchIdMapping = await deps.searchSessionsClient.getSearchIdMapping(sessionId);
- const response = await deps.searchSessionsClient.cancel(sessionId);
for (const [searchId, strategyName] of searchIdMapping.entries()) {
const searchOptions = {
@@ -319,10 +318,19 @@ export class SearchService implements Plugin {
};
this.cancel(deps, searchId, searchOptions);
}
+ };
+ private cancelSession = async (deps: SearchStrategyDependencies, sessionId: string) => {
+ const response = await deps.searchSessionsClient.cancel(sessionId);
+ this.cancelSessionSearches(deps, sessionId);
return response;
};
+ private deleteSession = async (deps: SearchStrategyDependencies, sessionId: string) => {
+ this.cancelSessionSearches(deps, sessionId);
+ return deps.searchSessionsClient.delete(sessionId);
+ };
+
private extendSession = async (
deps: SearchStrategyDependencies,
sessionId: string,
@@ -372,6 +380,7 @@ export class SearchService implements Plugin {
updateSession: searchSessionsClient.update,
extendSession: this.extendSession.bind(this, deps),
cancelSession: this.cancelSession.bind(this, deps),
+ deleteSession: this.deleteSession.bind(this, deps),
};
};
};
diff --git a/src/plugins/data/server/search/session/mocks.ts b/src/plugins/data/server/search/session/mocks.ts
index 5e940412d9abd..c173e1a1290ea 100644
--- a/src/plugins/data/server/search/session/mocks.ts
+++ b/src/plugins/data/server/search/session/mocks.ts
@@ -21,5 +21,6 @@ export function createSearchSessionsClientMock(): jest.Mocked<
update: jest.fn(),
cancel: jest.fn(),
extend: jest.fn(),
+ delete: jest.fn(),
};
}
diff --git a/src/plugins/data/server/search/session/session_service.ts b/src/plugins/data/server/search/session/session_service.ts
index 2ca580f50db0a..2ed44b4e57d94 100644
--- a/src/plugins/data/server/search/session/session_service.ts
+++ b/src/plugins/data/server/search/session/session_service.ts
@@ -41,6 +41,9 @@ export class SearchSessionService implements ISearchSessionService {
cancel: async () => {
throw new Error('cancel not implemented in OSS search session service');
},
+ delete: async () => {
+ throw new Error('delete not implemented in OSS search session service');
+ },
});
}
}
diff --git a/src/plugins/data/server/search/session/types.ts b/src/plugins/data/server/search/session/types.ts
index 16079b51f4bff..816716360415d 100644
--- a/src/plugins/data/server/search/session/types.ts
+++ b/src/plugins/data/server/search/session/types.ts
@@ -29,6 +29,7 @@ export interface IScopedSearchSessionsClient {
find: (options: Omit) => Promise>;
update: (sessionId: string, attributes: Partial) => Promise>;
cancel: (sessionId: string) => Promise<{}>;
+ delete: (sessionId: string) => Promise<{}>;
extend: (sessionId: string, expires: Date) => Promise>;
}
diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts
index 854f5ed94eb48..e8548257c0167 100644
--- a/src/plugins/data/server/search/types.ts
+++ b/src/plugins/data/server/search/types.ts
@@ -92,6 +92,7 @@ export interface IScopedSearchClient extends ISearchClient {
findSessions: IScopedSearchSessionsClient['find'];
updateSession: IScopedSearchSessionsClient['update'];
cancelSession: IScopedSearchSessionsClient['cancel'];
+ deleteSession: IScopedSearchSessionsClient['delete'];
extendSession: IScopedSearchSessionsClient['extend'];
}
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index beda5b3cea97e..68582a9d877e9 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -1254,6 +1254,7 @@ export class SearchSessionService implements ISearchSessionService {
update: () => Promise;
extend: () => Promise;
cancel: () => Promise;
+ delete: () => Promise;
};
}
@@ -1430,7 +1431,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage;
// src/plugins/data/server/index.ts:266:1 - (ae-forgotten-export) The symbol "calcAutoIntervalLessThan" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/index_patterns/index_patterns_service.ts:59:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts
// src/plugins/data/server/plugin.ts:79:74 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts
-// src/plugins/data/server/search/types.ts:113:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts
+// src/plugins/data/server/search/types.ts:114:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx
similarity index 70%
rename from x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx
rename to x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx
index 5edbc7b08985c..d505752ec3fad 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx
@@ -13,31 +13,31 @@ import { SearchSessionsMgmtAPI } from '../../lib/api';
import { TableText } from '../';
import { OnActionComplete } from './types';
-interface CancelButtonProps {
+interface DeleteButtonProps {
id: string;
name: string;
api: SearchSessionsMgmtAPI;
onActionComplete: OnActionComplete;
}
-const CancelConfirm = ({
- onConfirmDismiss,
+const DeleteConfirm = ({
+ onConfirmCancel,
...props
-}: CancelButtonProps & { onConfirmDismiss: () => void }) => {
+}: DeleteButtonProps & { onConfirmCancel: () => void }) => {
const { id, name, api, onActionComplete } = props;
const [isLoading, setIsLoading] = useState(false);
const title = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.title', {
- defaultMessage: 'Cancel search session',
+ defaultMessage: 'Delete search session',
});
- const confirm = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.cancelButton', {
- defaultMessage: 'Cancel',
+ const confirm = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.deleteButton', {
+ defaultMessage: 'Delete',
});
- const cancel = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.dontCancelButton', {
- defaultMessage: 'Dismiss',
+ const cancel = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.cancelButton', {
+ defaultMessage: 'Cancel',
});
const message = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.message', {
- defaultMessage: `Canceling the search session \'{name}\' will expire any cached results, so that quick restore will no longer be available. You will still be able to re-run it, using the reload action.`,
+ defaultMessage: `Deleting the search session \'{name}\' deletes all cached results.`,
values: {
name,
},
@@ -47,7 +47,7 @@ const CancelConfirm = ({
{
setIsLoading(true);
await api.sendCancel(id);
@@ -65,14 +65,14 @@ const CancelConfirm = ({
);
};
-export const CancelButton = (props: CancelButtonProps) => {
+export const DeleteButton = (props: DeleteButtonProps) => {
const [showConfirm, setShowConfirm] = useState(false);
const onClick = () => {
setShowConfirm(true);
};
- const onConfirmDismiss = () => {
+ const onConfirmCancel = () => {
setShowConfirm(false);
};
@@ -80,11 +80,11 @@ export const CancelButton = (props: CancelButtonProps) => {
<>
- {showConfirm ? : null}
+ {showConfirm ? : null}
>
);
};
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx
index bc849abf125c1..edc5037f1dbec 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx
@@ -10,30 +10,22 @@ import { IClickActionDescriptor } from '../';
import extendSessionIcon from '../../icons/extend_session.svg';
import { SearchSessionsMgmtAPI } from '../../lib/api';
import { UISession } from '../../types';
-import { CancelButton } from './cancel_button';
+import { DeleteButton } from './delete_button';
import { ExtendButton } from './extend_button';
-import { ReloadButton } from './reload_button';
import { ACTION, OnActionComplete } from './types';
export const getAction = (
api: SearchSessionsMgmtAPI,
actionType: string,
- { id, name, expires, reloadUrl }: UISession,
+ { id, name, expires }: UISession,
onActionComplete: OnActionComplete
): IClickActionDescriptor | null => {
switch (actionType) {
- case ACTION.CANCEL:
+ case ACTION.DELETE:
return {
iconType: 'crossInACircleFilled',
textColor: 'default',
- label: ,
- };
-
- case ACTION.RELOAD:
- return {
- iconType: 'refresh',
- textColor: 'default',
- label: ,
+ label: ,
};
case ACTION.EXTEND:
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx
index e47a9a5944b24..fe71b5125dfbb 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx
@@ -90,7 +90,7 @@ export const PopoverActionsMenu = ({ api, onActionComplete, session }: PopoverAc
// add a line above the delete action (when there are multiple)
// NOTE: Delete action MUST be the final action[] item
- if (actions.length > 1 && actionType === ACTION.CANCEL) {
+ if (actions.length > 1 && actionType === ACTION.DELETE) {
itemSet.push({ isSeparator: true, key: 'separadorable' });
}
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx
deleted file mode 100644
index 70ca279c2450d..0000000000000
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { FormattedMessage } from '@kbn/i18n/react';
-import React from 'react';
-import { TableText } from '../';
-import { SearchSessionsMgmtAPI } from '../../lib/api';
-
-interface ReloadButtonProps {
- api: SearchSessionsMgmtAPI;
- reloadUrl: string;
-}
-
-export const ReloadButton = (props: ReloadButtonProps) => {
- function onClick() {
- props.api.reloadSearchSession(props.reloadUrl);
- }
-
- return (
- <>
-
-
-
- >
- );
-};
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts
index 97e67909baea2..5f82f16adcbb6 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts
@@ -9,6 +9,5 @@ export type OnActionComplete = () => void;
export enum ACTION {
EXTEND = 'extend',
- CANCEL = 'cancel',
- RELOAD = 'reload',
+ DELETE = 'delete',
}
diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts
index 2ec9d588d7fd7..86acbcdb53001 100644
--- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts
@@ -61,9 +61,8 @@ describe('Search Sessions Management API', () => {
Array [
Object {
"actions": Array [
- "reload",
"extend",
- "cancel",
+ "delete",
],
"appId": "pizza",
"created": undefined,
@@ -146,7 +145,7 @@ describe('Search Sessions Management API', () => {
await api.sendCancel('abc-123-cool-session-ID');
expect(mockCoreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({
- title: 'The search session was canceled and expired.',
+ title: 'The search session was deleted.',
});
});
@@ -162,37 +161,11 @@ describe('Search Sessions Management API', () => {
expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalledWith(
new Error('implementation is so bad'),
- { title: 'Failed to cancel the search session!' }
+ { title: 'Failed to delete the search session!' }
);
});
});
- describe('reload', () => {
- beforeEach(() => {
- sessionsClient.find = jest.fn().mockImplementation(async () => {
- return {
- saved_objects: [
- {
- id: 'hello-pizza-123',
- attributes: { name: 'Veggie', appId: 'pizza', status: SearchSessionStatus.COMPLETE },
- },
- ],
- } as SavedObjectsFindResponse;
- });
- });
-
- test('send cancel calls the cancel endpoint with a session ID', async () => {
- const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, {
- urls: mockUrls,
- notifications: mockCoreStart.notifications,
- application: mockCoreStart.application,
- });
- await api.reloadSearchSession('www.myurl.com');
-
- expect(mockCoreStart.application.navigateToUrl).toHaveBeenCalledWith('www.myurl.com');
- });
- });
-
describe('extend', () => {
beforeEach(() => {
sessionsClient.find = jest.fn().mockImplementation(async () => {
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 1b024dae1bfca..264556f91cc37 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
@@ -21,10 +21,9 @@ type UrlGeneratorsStart = SharePluginStart['urlGenerators'];
function getActions(status: SearchSessionStatus) {
const actions: ACTION[] = [];
- actions.push(ACTION.RELOAD);
if (status === SearchSessionStatus.IN_PROGRESS || status === SearchSessionStatus.COMPLETE) {
actions.push(ACTION.EXTEND);
- actions.push(ACTION.CANCEL);
+ actions.push(ACTION.DELETE);
}
return actions;
}
@@ -162,8 +161,8 @@ export class SearchSessionsMgmtAPI {
await this.sessionsClient.delete(id);
this.deps.notifications.toasts.addSuccess({
- title: i18n.translate('xpack.data.mgmt.searchSessions.api.canceled', {
- defaultMessage: 'The search session was canceled and expired.',
+ title: i18n.translate('xpack.data.mgmt.searchSessions.api.deleted', {
+ defaultMessage: 'The search session was deleted.',
}),
});
} catch (err) {
@@ -171,8 +170,8 @@ export class SearchSessionsMgmtAPI {
console.error(err);
this.deps.notifications.toasts.addError(err, {
- title: i18n.translate('xpack.data.mgmt.searchSessions.api.cancelError', {
- defaultMessage: 'Failed to cancel the search session!',
+ title: i18n.translate('xpack.data.mgmt.searchSessions.api.deletedError', {
+ defaultMessage: 'Failed to delete the search session!',
}),
});
}
diff --git a/x-pack/plugins/data_enhanced/server/routes/session.test.ts b/x-pack/plugins/data_enhanced/server/routes/session.test.ts
index 830524da0fb97..ebc501346aed2 100644
--- a/x-pack/plugins/data_enhanced/server/routes/session.test.ts
+++ b/x-pack/plugins/data_enhanced/server/routes/session.test.ts
@@ -16,6 +16,13 @@ import type {
import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks';
import { registerSessionRoutes } from './session';
+enum PostHandlerIndex {
+ SAVE,
+ FIND,
+ CANCEL,
+ EXTEND,
+}
+
describe('registerSessionRoutes', () => {
let mockCoreSetup: MockedKeys>;
let mockContext: jest.Mocked;
@@ -37,7 +44,7 @@ describe('registerSessionRoutes', () => {
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
- const [[, saveHandler]] = mockRouter.post.mock.calls;
+ const [, saveHandler] = mockRouter.post.mock.calls[PostHandlerIndex.SAVE];
saveHandler(mockContext, mockRequest, mockResponse);
@@ -71,7 +78,7 @@ describe('registerSessionRoutes', () => {
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
- const [, [, findHandler]] = mockRouter.post.mock.calls;
+ const [, findHandler] = mockRouter.post.mock.calls[PostHandlerIndex.FIND];
findHandler(mockContext, mockRequest, mockResponse);
@@ -89,14 +96,14 @@ describe('registerSessionRoutes', () => {
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
- const [[, updateHandler]] = mockRouter.put.mock.calls;
+ const [, updateHandler] = mockRouter.put.mock.calls[0];
updateHandler(mockContext, mockRequest, mockResponse);
expect(mockContext.search!.updateSession).toHaveBeenCalledWith(id, body);
});
- it('delete calls cancelSession with id', async () => {
+ it('cancel calls cancelSession with id', async () => {
const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
const params = { id };
@@ -104,13 +111,28 @@ describe('registerSessionRoutes', () => {
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
- const [[, deleteHandler]] = mockRouter.delete.mock.calls;
+ const [, cancelHandler] = mockRouter.post.mock.calls[PostHandlerIndex.CANCEL];
- deleteHandler(mockContext, mockRequest, mockResponse);
+ cancelHandler(mockContext, mockRequest, mockResponse);
expect(mockContext.search!.cancelSession).toHaveBeenCalledWith(id);
});
+ it('delete calls deleteSession with id', async () => {
+ const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
+ const params = { id };
+
+ const mockRequest = httpServerMock.createKibanaRequest({ params });
+ const mockResponse = httpServerMock.createResponseFactory();
+
+ const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
+ const [, deleteHandler] = mockRouter.delete.mock.calls[0];
+
+ await deleteHandler(mockContext, mockRequest, mockResponse);
+
+ expect(mockContext.search!.deleteSession).toHaveBeenCalledWith(id);
+ });
+
it('extend calls extendSession with id', async () => {
const id = 'd7170a35-7e2c-48d6-8dec-9a056721b489';
const expires = new Date().toISOString();
@@ -121,7 +143,7 @@ describe('registerSessionRoutes', () => {
const mockResponse = httpServerMock.createResponseFactory();
const mockRouter = mockCoreSetup.http.createRouter.mock.results[0].value;
- const [, , [, extendHandler]] = mockRouter.post.mock.calls;
+ const [, extendHandler] = mockRouter.post.mock.calls[PostHandlerIndex.EXTEND];
extendHandler(mockContext, mockRequest, mockResponse);
diff --git a/x-pack/plugins/data_enhanced/server/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts
index 0b953f8201ece..80388a84d98f8 100644
--- a/x-pack/plugins/data_enhanced/server/routes/session.ts
+++ b/x-pack/plugins/data_enhanced/server/routes/session.ts
@@ -129,6 +129,29 @@ export function registerSessionRoutes(router: DataEnhancedPluginRouter, logger:
}),
},
},
+ async (context, request, res) => {
+ const { id } = request.params;
+ try {
+ await context.search!.deleteSession(id);
+
+ return res.ok();
+ } catch (e) {
+ const err = e.output?.payload || e;
+ logger.error(err);
+ return reportServerError(res, err);
+ }
+ }
+ );
+
+ router.post(
+ {
+ path: '/internal/session/{id}/cancel',
+ validate: {
+ params: schema.object({
+ id: schema.string(),
+ }),
+ },
+ },
async (context, request, res) => {
const { id } = request.params;
try {
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 9d8a730004e1b..059edd5edf1de 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
@@ -226,6 +226,11 @@ export class SearchSessionService
});
};
+ // TODO: Throw an error if this session doesn't belong to this user
+ public delete = ({ savedObjectsClient }: SearchSessionDependencies, sessionId: string) => {
+ return savedObjectsClient.delete(SEARCH_SESSION_TYPE, sessionId);
+ };
+
/**
* Tracks the given search request/search ID in the saved session.
* @internal
@@ -308,6 +313,7 @@ export class SearchSessionService
update: this.update.bind(this, deps),
extend: this.extend.bind(this, deps),
cancel: this.cancel.bind(this, deps),
+ delete: this.delete.bind(this, deps),
};
};
};
diff --git a/x-pack/test/api_integration/apis/search/session.ts b/x-pack/test/api_integration/apis/search/session.ts
index 28b63788a8cfb..984f3e3f7dd4e 100644
--- a/x-pack/test/api_integration/apis/search/session.ts
+++ b/x-pack/test/api_integration/apis/search/session.ts
@@ -45,11 +45,11 @@ export default function ({ getService }: FtrProviderContext) {
await supertest.get(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(200);
});
- it('should fail to cancel an unknown session', async () => {
+ it('should fail to delete an unknown session', async () => {
await supertest.delete(`/internal/session/123`).set('kbn-xsrf', 'foo').expect(404);
});
- it('should create and cancel a session', async () => {
+ it('should create and delete a session', async () => {
const sessionId = `my-session-${Math.random()}`;
await supertest
.post(`/internal/session`)
@@ -65,6 +65,28 @@ export default function ({ getService }: FtrProviderContext) {
await supertest.delete(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(200);
+ await supertest.get(`/internal/session/${sessionId}`).set('kbn-xsrf', 'foo').expect(404);
+ });
+
+ it('should create and cancel a session', async () => {
+ const sessionId = `my-session-${Math.random()}`;
+ await supertest
+ .post(`/internal/session`)
+ .set('kbn-xsrf', 'foo')
+ .send({
+ sessionId,
+ name: 'My Session',
+ appId: 'discover',
+ expires: '123',
+ urlGeneratorId: 'discover',
+ })
+ .expect(200);
+
+ await supertest
+ .post(`/internal/session/${sessionId}/cancel`)
+ .set('kbn-xsrf', 'foo')
+ .expect(200);
+
const resp = await supertest
.get(`/internal/session/${sessionId}`)
.set('kbn-xsrf', 'foo')
diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts
index a5ffa914eac22..df4e99dd595d9 100644
--- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts
+++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts
@@ -49,11 +49,11 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr
'[data-test-subj="sessionManagementPopoverAction-reload"]'
);
},
- cancel: async () => {
- log.debug('management ui: cancel the session');
+ delete: async () => {
+ log.debug('management ui: delete the session');
await actionsCell.click();
await find.clickByCssSelector(
- '[data-test-subj="sessionManagementPopoverAction-cancel"]'
+ '[data-test-subj="sessionManagementPopoverAction-delete"]'
);
await PageObjects.common.clickConfirmOnModal();
},
diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts
index 7e09c6b0fe05c..f925cfb78a8c6 100644
--- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts
+++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts
@@ -88,15 +88,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await searchSessions.expectState('completed');
});
- it('Cancels a session from management', async () => {
+ it('Deletes a session from management', async () => {
await PageObjects.searchSessionsManagement.goTo();
const searchSessionList = await PageObjects.searchSessionsManagement.getList();
expect(searchSessionList.length).to.be(1);
- await searchSessionList[0].cancel();
+ await searchSessionList[0].delete();
- // TODO: update this once canceling doesn't delete the object!
await retry.waitFor(`wait for list to be empty`, async function () {
const s = await PageObjects.searchSessionsManagement.getList();
From c37b0e1474ba3dd7b8994048122e8dd8349b61e0 Mon Sep 17 00:00:00 2001
From: Maja Grubic
Date: Thu, 4 Feb 2021 15:00:22 +0000
Subject: [PATCH 02/42] [Discover] Minor cleanup (#90260)
---
.../embeddable/search_embeddable.ts | 4 +-
.../_indexpattern_with_unmapped_fields.ts | 4 +-
.../fixtures/es_archiver/data/data.json.gz | Bin 224 -> 0 bytes
.../fixtures/es_archiver/data/mappings.json | 450 ------------------
4 files changed, 4 insertions(+), 454 deletions(-)
delete mode 100644 test/functional/fixtures/es_archiver/data/data.json.gz
delete mode 100644 test/functional/fixtures/es_archiver/data/mappings.json
diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts
index d04d482c7aade..658734aa46cb0 100644
--- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts
+++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts
@@ -308,9 +308,9 @@ export class SearchEmbeddable
);
if (useNewFieldsApi) {
searchSource.removeField('fieldsFromSource');
- const fields: Record = { field: '*' };
+ const fields: Record = { field: '*' };
if (pre712) {
- fields.include_unmapped = true;
+ fields.include_unmapped = 'true';
}
searchSource.setField('fields', [fields]);
} else {
diff --git a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts
index bad7afacc1245..0990b3fa29f70 100644
--- a/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts
+++ b/test/functional/apps/discover/_indexpattern_with_unmapped_fields.ts
@@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
before(async () => {
await esArchiver.loadIfNeeded('unmapped_fields');
- await kibanaServer.uiSettings.replace({ defaultIndex: 'test-index-unmapped-fields' });
- await kibanaServer.uiSettings.update({
+ await kibanaServer.uiSettings.replace({
+ defaultIndex: 'test-index-unmapped-fields',
'discover:searchFieldsFromSource': false,
});
log.debug('discover');
diff --git a/test/functional/fixtures/es_archiver/data/data.json.gz b/test/functional/fixtures/es_archiver/data/data.json.gz
deleted file mode 100644
index 629276ccd186e105046047b80791692dfb6588c2..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 224
zcmV<603ZJ!iwFP!000026J?N13c@fDh4(#0$-0IlR%?0(U5N`tBsS9$)U>3HDAK!|
zX#b##5axSt<_+@+!Vop@3Q!s%S!O8m;3@9blaDK0siar4Qs=5jH<)1Zvw1~JczF@u
z)KzG4p}kU<)@0+1uXG?|JpB0)E~=x0RRBJ=x1gC
diff --git a/test/functional/fixtures/es_archiver/data/mappings.json b/test/functional/fixtures/es_archiver/data/mappings.json
deleted file mode 100644
index 256978162b981..0000000000000
--- a/test/functional/fixtures/es_archiver/data/mappings.json
+++ /dev/null
@@ -1,450 +0,0 @@
-{
- "type": "index",
- "value": {
- "aliases": {
- ".kibana": {
- }
- },
- "index": ".kibana_1",
- "mappings": {
- "_meta": {
- "migrationMappingPropertyHashes": {
- "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd",
- "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724",
- "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724",
- "config": "c63748b75f39d0c54de12d12c1ccbc20",
- "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724",
- "coreMigrationVersion": "2f4316de49999235636386fe51dc06c1",
- "dashboard": "40554caf09725935e2c02e02563a2d07",
- "index-pattern": "45915a1ad866812242df474eb0479052",
- "kql-telemetry": "d12a98a6f19a2d273696597547e064ee",
- "legacy-url-alias": "3d1b76c39bfb2cc8296b024d73854724",
- "migrationVersion": "4a1746014a75ade3a714e1db5763276f",
- "namespace": "2f4316de49999235636386fe51dc06c1",
- "namespaces": "2f4316de49999235636386fe51dc06c1",
- "originId": "2f4316de49999235636386fe51dc06c1",
- "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9",
- "references": "7997cf5a56cc02bdc9c93361bde732b0",
- "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4",
- "search": "e5b843b43566421ffa75fb499271dc34",
- "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724",
- "telemetry": "36a616f7026dfa617d6655df850fe16d",
- "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf",
- "type": "2f4316de49999235636386fe51dc06c1",
- "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3",
- "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3",
- "updated_at": "00da57df13e94e9d98437d13ace4bfe0",
- "url": "c7f66a0df8b1b52f17c28c4adb111105",
- "visualization": "f819cf6636b75c9e76ba733a0c6ef355"
- }
- },
- "dynamic": "strict",
- "properties": {
- "application_usage_daily": {
- "dynamic": "false",
- "properties": {
- "timestamp": {
- "type": "date"
- }
- }
- },
- "application_usage_totals": {
- "dynamic": "false",
- "type": "object"
- },
- "application_usage_transactional": {
- "dynamic": "false",
- "type": "object"
- },
- "config": {
- "dynamic": "false",
- "properties": {
- "buildNum": {
- "type": "keyword"
- }
- }
- },
- "core-usage-stats": {
- "dynamic": "false",
- "type": "object"
- },
- "coreMigrationVersion": {
- "type": "keyword"
- },
- "dashboard": {
- "properties": {
- "description": {
- "type": "text"
- },
- "hits": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "index": false,
- "type": "text"
- }
- }
- },
- "optionsJSON": {
- "index": false,
- "type": "text"
- },
- "panelsJSON": {
- "index": false,
- "type": "text"
- },
- "refreshInterval": {
- "properties": {
- "display": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "pause": {
- "doc_values": false,
- "index": false,
- "type": "boolean"
- },
- "section": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "value": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- }
- }
- },
- "timeFrom": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "timeRestore": {
- "doc_values": false,
- "index": false,
- "type": "boolean"
- },
- "timeTo": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "index-pattern": {
- "dynamic": "false",
- "properties": {
- "title": {
- "type": "text"
- },
- "type": {
- "type": "keyword"
- }
- }
- },
- "kql-telemetry": {
- "properties": {
- "optInCount": {
- "type": "long"
- },
- "optOutCount": {
- "type": "long"
- }
- }
- },
- "legacy-url-alias": {
- "dynamic": "false",
- "type": "object"
- },
- "migrationVersion": {
- "dynamic": "true",
- "properties": {
- "config": {
- "fields": {
- "keyword": {
- "ignore_above": 256,
- "type": "keyword"
- }
- },
- "type": "text"
- }
- }
- },
- "namespace": {
- "type": "keyword"
- },
- "namespaces": {
- "type": "keyword"
- },
- "originId": {
- "type": "keyword"
- },
- "query": {
- "properties": {
- "description": {
- "type": "text"
- },
- "filters": {
- "enabled": false,
- "type": "object"
- },
- "query": {
- "properties": {
- "language": {
- "type": "keyword"
- },
- "query": {
- "index": false,
- "type": "keyword"
- }
- }
- },
- "timefilter": {
- "enabled": false,
- "type": "object"
- },
- "title": {
- "type": "text"
- }
- }
- },
- "references": {
- "properties": {
- "id": {
- "type": "keyword"
- },
- "name": {
- "type": "keyword"
- },
- "type": {
- "type": "keyword"
- }
- },
- "type": "nested"
- },
- "sample-data-telemetry": {
- "properties": {
- "installCount": {
- "type": "long"
- },
- "unInstallCount": {
- "type": "long"
- }
- }
- },
- "search": {
- "properties": {
- "columns": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "description": {
- "type": "text"
- },
- "grid": {
- "enabled": false,
- "type": "object"
- },
- "hits": {
- "doc_values": false,
- "index": false,
- "type": "integer"
- },
- "kibanaSavedObjectMeta": {
- "properties": {
- "searchSourceJSON": {
- "index": false,
- "type": "text"
- }
- }
- },
- "pre712": {
- "type": "boolean"
- },
- "sort": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "version": {
- "type": "integer"
- }
- }
- },
- "search-telemetry": {
- "dynamic": "false",
- "type": "object"
- },
- "telemetry": {
- "properties": {
- "allowChangingOptInStatus": {
- "type": "boolean"
- },
- "enabled": {
- "type": "boolean"
- },
- "lastReported": {
- "type": "date"
- },
- "lastVersionChecked": {
- "type": "keyword"
- },
- "reportFailureCount": {
- "type": "integer"
- },
- "reportFailureVersion": {
- "type": "keyword"
- },
- "sendUsageFrom": {
- "type": "keyword"
- },
- "userHasSeenNotice": {
- "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"
- },
- "ui-counter": {
- "properties": {
- "count": {
- "type": "integer"
- }
- }
- },
- "ui-metric": {
- "properties": {
- "count": {
- "type": "integer"
- }
- }
- },
- "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": {
- "index": false,
- "type": "text"
- }
- }
- },
- "savedSearchRefName": {
- "doc_values": false,
- "index": false,
- "type": "keyword"
- },
- "title": {
- "type": "text"
- },
- "uiStateJSON": {
- "index": false,
- "type": "text"
- },
- "version": {
- "type": "integer"
- },
- "visState": {
- "index": false,
- "type": "text"
- }
- }
- }
- }
- },
- "settings": {
- "index": {
- "auto_expand_replicas": "0-1",
- "number_of_replicas": "0",
- "number_of_shards": "1"
- }
- }
- }
-}
\ No newline at end of file
From e0133892f6443ab7b5faceda9b868d0406ec16da Mon Sep 17 00:00:00 2001
From: Marshall Main <55718608+marshallmain@users.noreply.github.com>
Date: Thu, 4 Feb 2021 10:20:16 -0500
Subject: [PATCH 03/42] [Security Solution][Detections] Reduce detection engine
reliance on _source (#89371)
* First pass at switching rules to depend on fields instead of _source
* Fix tests
* Change operator: excluded logic so missing fields are allowlisted
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../signals/__mocks__/es_results.ts | 11 +++++
.../signals/build_events_query.test.ts | 41 ++++++++++++++++---
.../signals/build_events_query.ts | 6 +++
.../bulk_create_threshold_signals.test.ts | 2 +-
.../signals/bulk_create_threshold_signals.ts | 6 +--
.../create_field_and_set_tuples.test.ts | 8 ++--
.../create_set_to_filter_against.test.ts | 8 ++--
.../filters/create_set_to_filter_against.ts | 3 +-
.../signals/filters/filter_events.test.ts | 10 ++---
.../signals/filters/filter_events.ts | 13 +++---
.../filter_events_against_list.test.ts | 16 ++++----
.../signals/find_threshold_signals.ts | 6 +++
.../signals/search_after_bulk_create.test.ts | 14 +++----
.../signals/single_bulk_create.test.ts | 14 +------
.../build_threat_mapping_filter.mock.ts | 11 +++++
.../build_threat_mapping_filter.test.ts | 18 ++++++--
.../build_threat_mapping_filter.ts | 9 ++--
.../signals/threat_mapping/get_threat_list.ts | 6 +++
.../detection_engine/signals/utils.test.ts | 25 +++++++----
.../server/lib/machine_learning/index.ts | 6 +++
20 files changed, 162 insertions(+), 71 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
index eb38c58d82ea1..6011c67376973 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -166,6 +166,12 @@ export const sampleDocWithSortId = (
ip: destIp ?? '127.0.0.1',
},
},
+ fields: {
+ someKey: ['someValue'],
+ '@timestamp': ['2020-04-20T21:27:45+0000'],
+ 'source.ip': ip ? (Array.isArray(ip) ? ip : [ip]) : ['127.0.0.1'],
+ 'destination.ip': destIp ? (Array.isArray(destIp) ? destIp : [destIp]) : ['127.0.0.1'],
+ },
sort: ['1234567891111'],
});
@@ -185,6 +191,11 @@ export const sampleDocNoSortId = (
ip: ip ?? '127.0.0.1',
},
},
+ fields: {
+ someKey: ['someValue'],
+ '@timestamp': ['2020-04-20T21:27:45+0000'],
+ 'source.ip': [ip ?? '127.0.0.1'],
+ },
sort: [],
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts
index dc8ed156d8dea..8597667f64657 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.test.ts
@@ -56,7 +56,12 @@ describe('create_signals', () => {
],
},
},
-
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
sort: [
{
'@timestamp': {
@@ -115,7 +120,12 @@ describe('create_signals', () => {
],
},
},
-
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
sort: [
{
'@timestamp': {
@@ -175,7 +185,12 @@ describe('create_signals', () => {
],
},
},
-
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
sort: [
{
'@timestamp': {
@@ -236,7 +251,12 @@ describe('create_signals', () => {
],
},
},
-
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
sort: [
{
'@timestamp': {
@@ -296,7 +316,12 @@ describe('create_signals', () => {
],
},
},
-
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
sort: [
{
'@timestamp': {
@@ -358,6 +383,12 @@ describe('create_signals', () => {
],
},
},
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
aggregations: {
tags: {
terms: {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts
index dde284ed3beab..f8fd4ed30d6ee 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_events_query.ts
@@ -89,6 +89,12 @@ export const buildEventsSearchQuery = ({
],
},
},
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
...(aggregations ? { aggregations } : {}),
sort: [
{
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts
index f3da37c198ac2..713178345361d 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.test.ts
@@ -67,7 +67,7 @@ describe('transformThresholdResultsToEcs', () => {
_id,
_index: 'test',
_source: {
- '@timestamp': '2020-04-20T21:27:45+0000',
+ '@timestamp': ['2020-04-20T21:27:45+0000'],
threshold_result: {
count: 1,
value: '127.0.0.1',
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts
index e0494c2e92b1c..dd9e1e97a2b73 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_threshold_signals.ts
@@ -75,7 +75,7 @@ const getTransformedHits = (
}
const source = {
- '@timestamp': get(timestampOverride ?? '@timestamp', hit._source),
+ '@timestamp': get(timestampOverride ?? '@timestamp', hit.fields),
threshold_result: {
count: totalResults,
value: ruleId,
@@ -104,10 +104,10 @@ const getTransformedHits = (
}
const source = {
- '@timestamp': get(timestampOverride ?? '@timestamp', hit._source),
+ '@timestamp': get(timestampOverride ?? '@timestamp', hit.fields),
threshold_result: {
count: docCount,
- value: get(threshold.field, hit._source),
+ value: key,
},
};
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts
index 6f744de469d5c..aac0f47c28295 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts
@@ -120,7 +120,7 @@ describe('filterEventsAgainstList', () => {
exceptionItem,
buildRuleMessage,
});
- expect([...matchedSet]).toEqual([JSON.stringify('1.1.1.1')]);
+ expect([...matchedSet]).toEqual([JSON.stringify(['1.1.1.1'])]);
});
test('it returns two matched sets as a JSON.stringify() set from the "events"', async () => {
@@ -133,7 +133,7 @@ describe('filterEventsAgainstList', () => {
exceptionItem,
buildRuleMessage,
});
- expect([...matchedSet]).toEqual([JSON.stringify('1.1.1.1'), JSON.stringify('2.2.2.2')]);
+ expect([...matchedSet]).toEqual([JSON.stringify(['1.1.1.1']), JSON.stringify(['2.2.2.2'])]);
});
test('it returns an array as a set as a JSON.stringify() array from the "events"', async () => {
@@ -282,7 +282,7 @@ describe('filterEventsAgainstList', () => {
exceptionItem,
buildRuleMessage,
});
- expect([...matchedSet1]).toEqual([JSON.stringify('1.1.1.1'), JSON.stringify('2.2.2.2')]);
- expect([...matchedSet2]).toEqual([JSON.stringify('3.3.3.3'), JSON.stringify('5.5.5.5')]);
+ expect([...matchedSet1]).toEqual([JSON.stringify(['1.1.1.1']), JSON.stringify(['2.2.2.2'])]);
+ expect([...matchedSet2]).toEqual([JSON.stringify(['3.3.3.3']), JSON.stringify(['5.5.5.5'])]);
});
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts
index aff372dc5bf3b..aae4a7aae2b9e 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts
@@ -62,9 +62,9 @@ describe('createSetToFilterAgainst', () => {
expect(listClient.searchListItemByValues).toHaveBeenCalledWith({
listId: 'list-123',
type: 'ip',
- value: ['1.1.1.1'],
+ value: [['1.1.1.1']],
});
- expect([...field]).toEqual([JSON.stringify('1.1.1.1')]);
+ expect([...field]).toEqual([JSON.stringify(['1.1.1.1'])]);
});
test('it returns 2 fields if the list returns 2 items', async () => {
@@ -81,9 +81,9 @@ describe('createSetToFilterAgainst', () => {
expect(listClient.searchListItemByValues).toHaveBeenCalledWith({
listId: 'list-123',
type: 'ip',
- value: ['1.1.1.1', '2.2.2.2'],
+ value: [['1.1.1.1'], ['2.2.2.2']],
});
- expect([...field]).toEqual([JSON.stringify('1.1.1.1'), JSON.stringify('2.2.2.2')]);
+ expect([...field]).toEqual([JSON.stringify(['1.1.1.1']), JSON.stringify(['2.2.2.2'])]);
});
test('it returns 0 fields if the field does not match up to a valid field within the event', async () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts
index c9f98e1b1e4e3..d400cc901a3ed 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { get } from 'lodash/fp';
import { CreateSetToFilterAgainstOptions } from './types';
/**
@@ -31,7 +30,7 @@ export const createSetToFilterAgainst = async ({
buildRuleMessage,
}: CreateSetToFilterAgainstOptions): Promise> => {
const valuesFromSearchResultField = events.reduce((acc, searchResultItem) => {
- const valueField = get(field, searchResultItem._source);
+ const valueField = searchResultItem.fields ? searchResultItem.fields[field] : undefined;
if (valueField != null) {
acc.add(valueField);
}
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts
index 092a684756ea3..eb5c69e8abfe8 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts
@@ -40,7 +40,7 @@ describe('filterEvents', () => {
{
field: 'source.ip',
operator: 'included',
- matchedSet: new Set([JSON.stringify('1.1.1.1')]),
+ matchedSet: new Set([JSON.stringify(['1.1.1.1'])]),
},
];
const field = filterEvents({
@@ -56,7 +56,7 @@ describe('filterEvents', () => {
{
field: 'source.ip',
operator: 'excluded',
- matchedSet: new Set([JSON.stringify('1.1.1.1')]),
+ matchedSet: new Set([JSON.stringify(['1.1.1.1'])]),
},
];
const field = filterEvents({
@@ -72,7 +72,7 @@ describe('filterEvents', () => {
{
field: 'madeup.nonexistent', // field does not exist
operator: 'included',
- matchedSet: new Set([JSON.stringify('1.1.1.1')]),
+ matchedSet: new Set([JSON.stringify(['1.1.1.1'])]),
},
];
const field = filterEvents({
@@ -88,12 +88,12 @@ describe('filterEvents', () => {
{
field: 'source.ip',
operator: 'included',
- matchedSet: new Set([JSON.stringify('1.1.1.1')]),
+ matchedSet: new Set([JSON.stringify(['1.1.1.1'])]),
},
{
field: 'source.ip',
operator: 'excluded',
- matchedSet: new Set([JSON.stringify('1.1.1.1')]),
+ matchedSet: new Set([JSON.stringify(['1.1.1.1'])]),
},
];
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts
index 316ef5eb74f41..421ed91278f4c 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { get } from 'lodash/fp';
import { SearchResponse } from '../../../types';
import { FilterEventsOptions } from './types';
@@ -22,13 +21,17 @@ export const filterEvents = ({
return events.filter((item) => {
return fieldAndSetTuples
.map((tuple) => {
- const eventItem = get(tuple.field, item._source);
- if (eventItem == null) {
- return true;
- } else if (tuple.operator === 'included') {
+ const eventItem = item.fields ? item.fields[tuple.field] : undefined;
+ if (tuple.operator === 'included') {
+ if (eventItem == null) {
+ return true;
+ }
// only create a signal if the event is not in the value list
return !tuple.matchedSet.has(JSON.stringify(eventItem));
} else if (tuple.operator === 'excluded') {
+ if (eventItem == null) {
+ return false;
+ }
// only create a signal if the event is in the value list
return tuple.matchedSet.has(JSON.stringify(eventItem));
} else {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts
index c1ba8eabf7110..5b2f3426cd8aa 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts
@@ -162,12 +162,12 @@ describe('filterEventsAgainstList', () => {
// this call represents an exception list with a value list containing ['2.2.2.2', '4.4.4.4']
(listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([
- { ...getSearchListItemResponseMock(), value: '2.2.2.2' },
- { ...getSearchListItemResponseMock(), value: '4.4.4.4' },
+ { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] },
+ { ...getSearchListItemResponseMock(), value: ['4.4.4.4'] },
]);
// this call represents an exception list with a value list containing ['6.6.6.6']
(listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([
- { ...getSearchListItemResponseMock(), value: '6.6.6.6' },
+ { ...getSearchListItemResponseMock(), value: ['6.6.6.6'] },
]);
const res = await filterEventsAgainstList({
@@ -224,11 +224,11 @@ describe('filterEventsAgainstList', () => {
// this call represents an exception list with a value list containing ['2.2.2.2', '4.4.4.4']
(listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([
- { ...getSearchListItemResponseMock(), value: '2.2.2.2' },
+ { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] },
]);
// this call represents an exception list with a value list containing ['6.6.6.6']
(listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([
- { ...getSearchListItemResponseMock(), value: '6.6.6.6' },
+ { ...getSearchListItemResponseMock(), value: ['6.6.6.6'] },
]);
const res = await filterEventsAgainstList({
@@ -283,11 +283,11 @@ describe('filterEventsAgainstList', () => {
// this call represents an exception list with a value list containing ['2.2.2.2']
(listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([
- { ...getSearchListItemResponseMock(), value: '2.2.2.2' },
+ { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] },
]);
// this call represents an exception list with a value list containing ['4.4.4.4']
(listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([
- { ...getSearchListItemResponseMock(), value: '4.4.4.4' },
+ { ...getSearchListItemResponseMock(), value: ['4.4.4.4'] },
]);
const res = await filterEventsAgainstList({
@@ -365,7 +365,7 @@ describe('filterEventsAgainstList', () => {
// this call represents an exception list with a value list containing ['2.2.2.2', '4.4.4.4']
(listClient.searchListItemByValues as jest.Mock).mockResolvedValue([
- { ...getSearchListItemResponseMock(), value: '2.2.2.2' },
+ { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] },
]);
const res = await filterEventsAgainstList({
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts
index 7d32ac6873eb2..6144f1f4b3823 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts
@@ -69,6 +69,12 @@ export const findThresholdSignals = async ({
},
},
],
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
size: 1,
},
},
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
index 418d30711169e..b506a2463a311 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts
@@ -310,9 +310,9 @@ describe('searchAfterAndBulkCreate', () => {
test('should return success when all search results are in the allowlist and with sortId present', async () => {
const searchListItems: SearchListItemArraySchema = [
- { ...getSearchListItemResponseMock(), value: '1.1.1.1' },
- { ...getSearchListItemResponseMock(), value: '2.2.2.2' },
- { ...getSearchListItemResponseMock(), value: '3.3.3.3' },
+ { ...getSearchListItemResponseMock(), value: ['1.1.1.1'] },
+ { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] },
+ { ...getSearchListItemResponseMock(), value: ['3.3.3.3'] },
];
listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems);
const sampleParams = sampleRuleAlertParams(30);
@@ -374,10 +374,10 @@ describe('searchAfterAndBulkCreate', () => {
test('should return success when all search results are in the allowlist and no sortId present', async () => {
const searchListItems: SearchListItemArraySchema = [
- { ...getSearchListItemResponseMock(), value: '1.1.1.1' },
- { ...getSearchListItemResponseMock(), value: '2.2.2.2' },
- { ...getSearchListItemResponseMock(), value: '2.2.2.2' },
- { ...getSearchListItemResponseMock(), value: '2.2.2.2' },
+ { ...getSearchListItemResponseMock(), value: ['1.1.1.1'] },
+ { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] },
+ { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] },
+ { ...getSearchListItemResponseMock(), value: ['2.2.2.2'] },
];
listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
index 942db1e3b1aaa..19aba907f0c84 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts
@@ -316,19 +316,9 @@ describe('singleBulkCreate', () => {
});
test('filter duplicate rules will return back search responses if they do not have a signal and will NOT filter the source out', () => {
- const ancestors = sampleDocWithAncestors();
- ancestors.hits.hits[0]._source = { '@timestamp': '2020-04-20T21:27:45+0000' };
+ const ancestors = sampleDocSearchResultsNoSortId();
const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', ancestors);
- expect(filtered).toEqual([
- {
- _index: 'myFakeSignalIndex',
- _type: 'doc',
- _score: 100,
- _version: 1,
- _id: 'e1e08ddc-5e37-49ff-a258-5393aa44435a',
- _source: { '@timestamp': '2020-04-20T21:27:45+0000' },
- },
- ]);
+ expect(filtered).toEqual(ancestors.hits.hits);
});
test('filter duplicate rules does not attempt filters when the signal is not an event type of signal but rather a "clash" from the source index having its own numeric signal type', () => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts
index a88d9061f7a1f..12865e4dd47a9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.mock.ts
@@ -81,6 +81,7 @@ export const getThreatListSearchResponseMock = (): SearchResponse ({
},
});
+export const getThreatListItemFieldsMock = () => ({
+ '@timestamp': ['2020-09-09T21:59:13Z'],
+ 'host.name': ['host-1'],
+ 'host.ip': ['192.168.0.0.1'],
+ 'source.ip': ['127.0.0.1'],
+ 'source.port': [1],
+ 'destination.ip': ['127.0.0.1'],
+ 'destination.port': [1],
+});
+
export const getFilterThreatMapping = (): ThreatMapping => [
{
entries: [
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts
index 792fa889e395d..7a9c4b43b8f7a 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.test.ts
@@ -133,10 +133,16 @@ describe('build_threat_mapping_filter', () => {
},
],
threatListItem: {
- '@timestamp': '2020-09-09T21:59:13Z',
- host: {
- name: 'host-1',
- // since ip is missing this entire AND clause should be dropped
+ _source: {
+ '@timestamp': '2020-09-09T21:59:13Z',
+ host: {
+ name: 'host-1',
+ // since ip is missing this entire AND clause should be dropped
+ },
+ },
+ fields: {
+ '@timestamp': ['2020-09-09T21:59:13Z'],
+ 'host.name': ['host-1'],
},
},
});
@@ -177,6 +183,10 @@ describe('build_threat_mapping_filter', () => {
name: 'host-1',
},
},
+ fields: {
+ '@timestamp': ['2020-09-09T21:59:13Z'],
+ 'host.name': ['host-1'],
+ },
},
});
expect(item).toEqual([
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts
index 180895877bdd2..cab01a602b8a9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_mapping_filter.ts
@@ -55,7 +55,8 @@ export const filterThreatMapping = ({
threatMapping
.map((threatMap) => {
const atLeastOneItemMissingInThreatList = threatMap.entries.some((entry) => {
- return get(entry.value, threatListItem._source) == null;
+ const itemValue = get(entry.value, threatListItem.fields);
+ return itemValue == null || itemValue.length !== 1;
});
if (atLeastOneItemMissingInThreatList) {
return { ...threatMap, entries: [] };
@@ -70,15 +71,15 @@ export const createInnerAndClauses = ({
threatListItem,
}: CreateInnerAndClausesOptions): BooleanFilter[] => {
return threatMappingEntries.reduce((accum, threatMappingEntry) => {
- const value = get(threatMappingEntry.value, threatListItem._source);
- if (value != null) {
+ const value = get(threatMappingEntry.value, threatListItem.fields);
+ if (value != null && value.length === 1) {
// These values could be potentially 10k+ large so mutating the array intentionally
accum.push({
bool: {
should: [
{
match: {
- [threatMappingEntry.field]: value,
+ [threatMappingEntry.field]: value[0],
},
},
],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts
index c646fee81f1b1..92d4e5cf8a93b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts
@@ -55,6 +55,12 @@ export const getThreatList = async ({
const response: SearchResponse = await callCluster('search', {
body: {
query: queryFilter,
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
search_after: searchAfter,
sort: getSortWithTieBreaker({
sortField,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
index 5444f08474053..75bd9f593a6ac 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts
@@ -1166,6 +1166,9 @@ describe('utils', () => {
test('It will not set an invalid date time stamp from a non-existent @timestamp when the index is not 100% ECS compliant', () => {
const searchResult = sampleDocSearchResultsNoSortId();
(searchResult.hits.hits[0]._source['@timestamp'] as unknown) = undefined;
+ if (searchResult.hits.hits[0].fields != null) {
+ (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = undefined;
+ }
const { lastLookBackDate } = createSearchAfterReturnTypeFromResponse({
searchResult,
timestampOverride: undefined,
@@ -1176,6 +1179,9 @@ describe('utils', () => {
test('It will not set an invalid date time stamp from a null @timestamp when the index is not 100% ECS compliant', () => {
const searchResult = sampleDocSearchResultsNoSortId();
(searchResult.hits.hits[0]._source['@timestamp'] as unknown) = null;
+ if (searchResult.hits.hits[0].fields != null) {
+ (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = null;
+ }
const { lastLookBackDate } = createSearchAfterReturnTypeFromResponse({
searchResult,
timestampOverride: undefined,
@@ -1186,6 +1192,9 @@ describe('utils', () => {
test('It will not set an invalid date time stamp from an invalid @timestamp string', () => {
const searchResult = sampleDocSearchResultsNoSortId();
(searchResult.hits.hits[0]._source['@timestamp'] as unknown) = 'invalid';
+ if (searchResult.hits.hits[0].fields != null) {
+ (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = ['invalid'];
+ }
const { lastLookBackDate } = createSearchAfterReturnTypeFromResponse({
searchResult,
timestampOverride: undefined,
@@ -1198,6 +1207,9 @@ describe('utils', () => {
test('It returns undefined if the search result contains a null timestamp', () => {
const searchResult = sampleDocSearchResultsNoSortId();
(searchResult.hits.hits[0]._source['@timestamp'] as unknown) = null;
+ if (searchResult.hits.hits[0].fields != null) {
+ (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = null;
+ }
const date = lastValidDate({ searchResult, timestampOverride: undefined });
expect(date).toEqual(undefined);
});
@@ -1205,6 +1217,9 @@ describe('utils', () => {
test('It returns undefined if the search result contains a undefined timestamp', () => {
const searchResult = sampleDocSearchResultsNoSortId();
(searchResult.hits.hits[0]._source['@timestamp'] as unknown) = undefined;
+ if (searchResult.hits.hits[0].fields != null) {
+ (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = undefined;
+ }
const date = lastValidDate({ searchResult, timestampOverride: undefined });
expect(date).toEqual(undefined);
});
@@ -1212,13 +1227,9 @@ describe('utils', () => {
test('It returns undefined if the search result contains an invalid string value', () => {
const searchResult = sampleDocSearchResultsNoSortId();
(searchResult.hits.hits[0]._source['@timestamp'] as unknown) = 'invalid value';
- const date = lastValidDate({ searchResult, timestampOverride: undefined });
- expect(date).toEqual(undefined);
- });
-
- test('It returns correct date time stamp if the search result contains an invalid string value', () => {
- const searchResult = sampleDocSearchResultsNoSortId();
- (searchResult.hits.hits[0]._source['@timestamp'] as unknown) = 'invalid value';
+ if (searchResult.hits.hits[0].fields != null) {
+ (searchResult.hits.hits[0].fields['@timestamp'] as unknown) = ['invalid value'];
+ }
const date = lastValidDate({ searchResult, timestampOverride: undefined });
expect(date).toEqual(undefined);
});
diff --git a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts
index a6f4c2086e47b..962c44174d891 100644
--- a/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts
+++ b/x-pack/plugins/security_solution/server/lib/machine_learning/index.ts
@@ -60,6 +60,12 @@ export const getAnomalies = async (
})?.query,
},
},
+ fields: [
+ {
+ field: '*',
+ include_unmapped: true,
+ },
+ ],
sort: [{ record_score: { order: 'desc' } }],
},
},
From 1741cef4ae79566d61b75ee07f0e5af16818b125 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Thu, 4 Feb 2021 16:22:09 +0100
Subject: [PATCH 04/42] [Lens] Hide column in table (#88680)
---
.../__snapshots__/table_basic.test.tsx.snap | 54 +++++
.../components/columns.tsx | 23 +-
.../components/constants.ts | 1 +
.../components/dimension_editor.tsx | 61 ++++++
.../components/table_actions.test.ts | 51 +++--
.../components/table_actions.ts | 56 +++--
.../components/table_basic.test.tsx | 65 ++++--
.../components/table_basic.tsx | 68 ++++--
.../components/types.ts | 21 +-
.../expression.test.tsx | 22 +-
.../datatable_visualization/expression.tsx | 89 +++-----
.../public/datatable_visualization/index.ts | 6 +-
.../visualization.test.tsx | 206 +++++++-----------
.../datatable_visualization/visualization.tsx | 197 +++++++++--------
.../config_panel/color_indicator.tsx | 10 +
x-pack/plugins/lens/public/index.ts | 5 +-
.../shared_components/toolbar_popover.tsx | 1 +
x-pack/plugins/lens/public/types.ts | 5 +-
x-pack/plugins/lens/server/migrations.test.ts | 73 +++++++
x-pack/plugins/lens/server/migrations.ts | 54 +++++
x-pack/test/functional/apps/lens/index.ts | 1 +
x-pack/test/functional/apps/lens/table.ts | 69 ++++++
.../test/functional/page_objects/lens_page.ts | 9 +
23 files changed, 762 insertions(+), 385 deletions(-)
create mode 100644 x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
create mode 100644 x-pack/test/functional/apps/lens/table.ts
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
index a4eb99a972b9b..d69af298018e7 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap
@@ -93,6 +93,15 @@ exports[`DatatableComponent it renders actions column when there are row actions
"onClick": [Function],
"size": "xs",
},
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
],
"showHide": false,
"showMoveLeft": false,
@@ -121,6 +130,15 @@ exports[`DatatableComponent it renders actions column when there are row actions
"onClick": [Function],
"size": "xs",
},
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
],
"showHide": false,
"showMoveLeft": false,
@@ -149,6 +167,15 @@ exports[`DatatableComponent it renders actions column when there are row actions
"onClick": [Function],
"size": "xs",
},
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
],
"showHide": false,
"showMoveLeft": false,
@@ -288,6 +315,15 @@ exports[`DatatableComponent it renders the title and value 1`] = `
"onClick": [Function],
"size": "xs",
},
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
],
"showHide": false,
"showMoveLeft": false,
@@ -316,6 +352,15 @@ exports[`DatatableComponent it renders the title and value 1`] = `
"onClick": [Function],
"size": "xs",
},
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
],
"showHide": false,
"showMoveLeft": false,
@@ -344,6 +389,15 @@ exports[`DatatableComponent it renders the title and value 1`] = `
"onClick": [Function],
"size": "xs",
},
+ Object {
+ "color": "text",
+ "data-test-subj": "lensDatatableHide",
+ "iconType": "eyeClosed",
+ "isDisabled": false,
+ "label": "Hide",
+ "onClick": [Function],
+ "size": "xs",
+ },
],
"showHide": false,
"showMoveLeft": false,
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
index 366e002f50cd8..5ff1e84276ba7 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx
@@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui';
import type { Datatable, DatatableColumnMeta } from 'src/plugins/expressions';
import type { FormatFactory } from '../../types';
-import type { DatatableColumns } from './types';
+import { ColumnConfig } from './table_basic';
export const createGridColumns = (
bucketColumns: string[],
@@ -23,10 +23,11 @@ export const createGridColumns = (
negate?: boolean
) => void,
isReadOnly: boolean,
- columnConfig: DatatableColumns & { type: 'lens_datatable_columns' },
+ columnConfig: ColumnConfig,
visibleColumns: string[],
formatFactory: FormatFactory,
- onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void
+ onColumnResize: (eventData: { columnId: string; width: number | undefined }) => void,
+ onColumnHide: (eventData: { columnId: string }) => void
) => {
const columnsReverseLookup = table.columns.reduce<
Record
@@ -134,8 +135,9 @@ export const createGridColumns = (
]
: undefined;
- const initialWidth = columnConfig.columnWidth?.find(({ columnId }) => columnId === field)
- ?.width;
+ const column = columnConfig.columns.find(({ columnId }) => columnId === field);
+ const initialWidth = column?.width;
+ const isHidden = column?.hidden;
const columnDefinition: EuiDataGridColumn = {
id: field,
@@ -174,6 +176,17 @@ export const createGridColumns = (
'data-test-subj': 'lensDatatableResetWidth',
isDisabled: initialWidth == null,
},
+ {
+ color: 'text',
+ size: 'xs',
+ onClick: () => onColumnHide({ columnId: field }),
+ iconType: 'eyeClosed',
+ label: i18n.translate('xpack.lens.table.hide.hideLabel', {
+ defaultMessage: 'Hide',
+ }),
+ 'data-test-subj': 'lensDatatableHide',
+ isDisabled: !isHidden && visibleColumns.length <= 1,
+ },
],
},
};
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/constants.ts b/x-pack/plugins/lens/public/datatable_visualization/components/constants.ts
index db72f8a4e4a92..84ee4f0e8a18e 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/constants.ts
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/constants.ts
@@ -7,3 +7,4 @@
export const LENS_EDIT_SORT_ACTION = 'sort';
export const LENS_EDIT_RESIZE_ACTION = 'resize';
+export const LENS_TOGGLE_ACTION = 'toggle';
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
new file mode 100644
index 0000000000000..008b805bc8fed
--- /dev/null
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiSwitch, EuiFormRow } from '@elastic/eui';
+import { VisualizationDimensionEditorProps } from '../../types';
+import { DatatableVisualizationState } from '../visualization';
+
+export function TableDimensionEditor(
+ props: VisualizationDimensionEditorProps
+) {
+ const { state, setState, accessor } = props;
+ const column = state.columns.find((c) => c.columnId === accessor);
+
+ const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length;
+
+ if (!column) {
+ return null;
+ }
+
+ return (
+
+ {
+ const newState = {
+ ...state,
+ columns: state.columns.map((currentColumn) => {
+ if (currentColumn.columnId === accessor) {
+ return {
+ ...currentColumn,
+ hidden: !column.hidden,
+ };
+ } else {
+ return currentColumn;
+ }
+ }),
+ };
+ setState(newState);
+ }}
+ />
+
+ );
+}
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts
index b0b7d46e4c3b7..68416ac9a60aa 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts
@@ -14,17 +14,19 @@ import {
createGridFilterHandler,
createGridResizeHandler,
createGridSortingConfig,
+ createGridHideHandler,
} from './table_actions';
-import { DatatableColumns, LensGridDirection } from './types';
+import { LensGridDirection } from './types';
+import { ColumnConfig } from './table_basic';
-function getDefaultConfig(): DatatableColumns & {
- type: 'lens_datatable_columns';
-} {
+function getDefaultConfig(): ColumnConfig {
return {
- columnIds: [],
- sortBy: '',
- sortDirection: 'none',
- type: 'lens_datatable_columns',
+ columns: [
+ { columnId: 'a', type: 'lens_datatable_column' },
+ { columnId: 'b', type: 'lens_datatable_column' },
+ ],
+ sortingColumnId: '',
+ sortingDirection: 'none',
};
}
@@ -207,7 +209,13 @@ describe('Table actions', () => {
expect(setColumnConfig).toHaveBeenCalledWith({
...columnConfig,
- columnWidth: [{ columnId: 'a', width: 100, type: 'lens_datatable_column_width' }],
+ columns: [
+ { columnId: 'a', width: 100, type: 'lens_datatable_column' },
+ {
+ columnId: 'b',
+ type: 'lens_datatable_column',
+ },
+ ],
});
expect(onEditAction).toHaveBeenCalledWith({ action: 'resize', columnId: 'a', width: 100 });
@@ -215,16 +223,14 @@ describe('Table actions', () => {
it('should pull out the table custom width from the local state when passing undefined', () => {
const columnConfig = getDefaultConfig();
- columnConfig.columnWidth = [
- { columnId: 'a', width: 100, type: 'lens_datatable_column_width' },
- ];
+ columnConfig.columns = [{ columnId: 'a', width: 100, type: 'lens_datatable_column' }];
const resizer = createGridResizeHandler(columnConfig, setColumnConfig, onEditAction);
resizer({ columnId: 'a', width: undefined });
expect(setColumnConfig).toHaveBeenCalledWith({
...columnConfig,
- columnWidth: [],
+ columns: [{ columnId: 'a', width: undefined, type: 'lens_datatable_column' }],
});
expect(onEditAction).toHaveBeenCalledWith({
@@ -234,4 +240,23 @@ describe('Table actions', () => {
});
});
});
+ describe('Column hiding', () => {
+ const setColumnConfig = jest.fn();
+
+ it('should allow to hide column', () => {
+ const columnConfig = getDefaultConfig();
+ const hiding = createGridHideHandler(columnConfig, setColumnConfig, onEditAction);
+ hiding({ columnId: 'a' });
+
+ expect(setColumnConfig).toHaveBeenCalledWith({
+ ...columnConfig,
+ columns: [
+ { columnId: 'a', hidden: true, type: 'lens_datatable_column' },
+ { columnId: 'b', type: 'lens_datatable_column' },
+ ],
+ });
+
+ expect(onEditAction).toHaveBeenCalledWith({ action: 'toggle', columnId: 'a' });
+ });
+ });
});
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts
index ca4ec7f3a8d0c..4f0271b758ffb 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts
@@ -9,43 +9,30 @@ import type { EuiDataGridSorting } from '@elastic/eui';
import type { Datatable } from 'src/plugins/expressions';
import type { LensFilterEvent } from '../../types';
import type {
- DatatableColumns,
LensGridDirection,
LensResizeAction,
LensSortAction,
+ LensToggleAction,
} from './types';
+import { ColumnConfig } from './table_basic';
import { desanitizeFilterContext } from '../../utils';
export const createGridResizeHandler = (
- columnConfig: DatatableColumns & {
- type: 'lens_datatable_columns';
- },
- setColumnConfig: React.Dispatch<
- React.SetStateAction<
- DatatableColumns & {
- type: 'lens_datatable_columns';
- }
- >
- >,
+ columnConfig: ColumnConfig,
+ setColumnConfig: React.Dispatch>,
onEditAction: (data: LensResizeAction['data']) => void
) => (eventData: { columnId: string; width: number | undefined }) => {
// directly set the local state of the component to make sure the visualization re-renders immediately,
// re-layouting and taking up all of the available space.
setColumnConfig({
...columnConfig,
- columnWidth: [
- ...(columnConfig.columnWidth || []).filter(({ columnId }) => columnId !== eventData.columnId),
- ...(eventData.width !== undefined
- ? [
- {
- columnId: eventData.columnId,
- width: eventData.width,
- type: 'lens_datatable_column_width' as const,
- },
- ]
- : []),
- ],
+ columns: columnConfig.columns.map((column) => {
+ if (column.columnId === eventData.columnId) {
+ return { ...column, width: eventData.width };
+ }
+ return column;
+ }),
});
return onEditAction({
action: 'resize',
@@ -54,6 +41,27 @@ export const createGridResizeHandler = (
});
};
+export const createGridHideHandler = (
+ columnConfig: ColumnConfig,
+ setColumnConfig: React.Dispatch>,
+ onEditAction: (data: LensToggleAction['data']) => void
+) => (eventData: { columnId: string }) => {
+ // directly set the local state of the component to make sure the visualization re-renders immediately
+ setColumnConfig({
+ ...columnConfig,
+ columns: columnConfig.columns.map((column) => {
+ if (column.columnId === eventData.columnId) {
+ return { ...column, hidden: true };
+ }
+ return column;
+ }),
+ });
+ return onEditAction({
+ action: 'toggle',
+ columnId: eventData.columnId,
+ });
+};
+
export const createGridFilterHandler = (
tableRef: React.MutableRefObject,
onClickValue: (data: LensFilterEvent['data']) => void
@@ -85,7 +93,7 @@ export const createGridFilterHandler = (
};
export const createGridSortingConfig = (
- sortBy: string,
+ sortBy: string | undefined,
sortDirection: LensGridDirection,
onEditAction: (data: LensSortAction['data']) => void
): EuiDataGridSorting => ({
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
index 6935e8313afb0..50d040bc5c397 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx
@@ -65,12 +65,13 @@ function sampleArgs() {
const args: DatatableProps['args'] = {
title: 'My fanci metric chart',
- columns: {
- columnIds: ['a', 'b', 'c'],
- sortBy: '',
- sortDirection: 'none',
- type: 'lens_datatable_columns',
- },
+ columns: [
+ { columnId: 'a', type: 'lens_datatable_column' },
+ { columnId: 'b', type: 'lens_datatable_column' },
+ { columnId: 'c', type: 'lens_datatable_column' },
+ ],
+ sortingColumnId: '',
+ sortingDirection: 'none',
};
return { data, args };
@@ -252,12 +253,12 @@ describe('DatatableComponent', () => {
const args: DatatableProps['args'] = {
title: '',
- columns: {
- columnIds: ['a', 'b'],
- sortBy: '',
- sortDirection: 'none',
- type: 'lens_datatable_columns',
- },
+ columns: [
+ { columnId: 'a', type: 'lens_datatable_column' },
+ { columnId: 'b', type: 'lens_datatable_column' },
+ ],
+ sortingColumnId: '',
+ sortingDirection: 'none',
};
const wrapper = mountWithIntl(
@@ -331,11 +332,8 @@ describe('DatatableComponent', () => {
data={data}
args={{
...args,
- columns: {
- ...args.columns,
- sortBy: 'b',
- sortDirection: 'desc',
- },
+ sortingColumnId: 'b',
+ sortingDirection: 'desc',
}}
formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
dispatchEvent={onDispatchEvent}
@@ -382,11 +380,8 @@ describe('DatatableComponent', () => {
data={data}
args={{
...args,
- columns: {
- ...args.columns,
- sortBy: 'b',
- sortDirection: 'desc',
- },
+ sortingColumnId: 'b',
+ sortingDirection: 'desc',
}}
formatFactory={() => ({ convert: (x) => x } as IFieldFormat)}
dispatchEvent={onDispatchEvent}
@@ -400,6 +395,32 @@ describe('DatatableComponent', () => {
]);
});
+ test('it does not render a hidden column', () => {
+ const { data, args } = sampleArgs();
+
+ const wrapper = mountWithIntl(
+ ({ convert: (x) => x } as IFieldFormat)}
+ dispatchEvent={onDispatchEvent}
+ getType={jest.fn()}
+ renderMode="display"
+ />
+ );
+
+ expect(wrapper.find(EuiDataGrid).prop('columns')!.length).toEqual(2);
+ });
+
test('it should refresh the table header when the datatable data changes', () => {
const { data, args } = sampleArgs();
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
index b4852895a1e20..f685990f12dd2 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx
@@ -22,17 +22,20 @@ import { FormatFactory, LensFilterEvent, LensTableRowContextMenuEvent } from '..
import { VisualizationContainer } from '../../visualization_container';
import { EmptyPlaceholder } from '../../shared_components';
import { LensIconChartDatatable } from '../../assets/chart_datatable';
+import { ColumnState } from '../visualization';
import {
DataContextType,
DatatableRenderProps,
LensSortAction,
LensResizeAction,
LensGridDirection,
+ LensToggleAction,
} from './types';
import { createGridColumns } from './columns';
import { createGridCell } from './cell_value';
import {
createGridFilterHandler,
+ createGridHideHandler,
createGridResizeHandler,
createGridSortingConfig,
} from './table_actions';
@@ -44,15 +47,33 @@ const gridStyle: EuiDataGridStyle = {
header: 'underline',
};
+export interface ColumnConfig {
+ columns: Array<
+ ColumnState & {
+ type: 'lens_datatable_column';
+ }
+ >;
+ sortingColumnId: string | undefined;
+ sortingDirection: LensGridDirection;
+}
+
export const DatatableComponent = (props: DatatableRenderProps) => {
const [firstTable] = Object.values(props.data.tables);
- const [columnConfig, setColumnConfig] = useState(props.args.columns);
+ const [columnConfig, setColumnConfig] = useState({
+ columns: props.args.columns,
+ sortingColumnId: props.args.sortingColumnId,
+ sortingDirection: props.args.sortingDirection,
+ });
const [firstLocalTable, updateTable] = useState(firstTable);
useDeepCompareEffect(() => {
- setColumnConfig(props.args.columns);
- }, [props.args.columns]);
+ setColumnConfig({
+ columns: props.args.columns,
+ sortingColumnId: props.args.sortingColumnId,
+ sortingDirection: props.args.sortingDirection,
+ });
+ }, [props.args.columns, props.args.sortingColumnId, props.args.sortingDirection]);
useDeepCompareEffect(() => {
updateTable(firstTable);
@@ -85,7 +106,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
);
const onEditAction = useCallback(
- (data: LensSortAction['data'] | LensResizeAction['data']) => {
+ (data: LensSortAction['data'] | LensResizeAction['data'] | LensToggleAction['data']) => {
if (renderMode === 'edit') {
dispatchEvent({ name: 'edit', data });
}
@@ -106,13 +127,15 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
const bucketColumns = useMemo(
() =>
- columnConfig.columnIds.filter((_colId, index) => {
- const col = firstTableRef.current.columns[index];
- return (
- col?.meta?.sourceParams?.type &&
- getType(col.meta.sourceParams.type as string)?.type === 'buckets'
- );
- }),
+ columnConfig.columns
+ .filter((_col, index) => {
+ const col = firstTableRef.current.columns[index];
+ return (
+ col?.meta?.sourceParams?.type &&
+ getType(col.meta.sourceParams.type as string)?.type === 'buckets'
+ );
+ })
+ .map((col) => col.columnId),
[firstTableRef, columnConfig, getType]
);
@@ -121,11 +144,15 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
(bucketColumns.length &&
firstTable.rows.every((row) => bucketColumns.every((col) => row[col] == null)));
- const visibleColumns = useMemo(() => columnConfig.columnIds.filter((field) => !!field), [
- columnConfig,
- ]);
+ const visibleColumns = useMemo(
+ () =>
+ columnConfig.columns
+ .filter((col) => !!col.columnId && !col.hidden)
+ .map((col) => col.columnId),
+ [columnConfig]
+ );
- const { sortBy, sortDirection } = columnConfig;
+ const { sortingColumnId: sortBy, sortingDirection: sortDirection } = props.args;
const isReadOnlySorted = renderMode !== 'edit';
@@ -134,6 +161,11 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
[onEditAction, setColumnConfig, columnConfig]
);
+ const onColumnHide = useMemo(
+ () => createGridHideHandler(columnConfig, setColumnConfig, onEditAction),
+ [onEditAction, setColumnConfig, columnConfig]
+ );
+
const columns: EuiDataGridColumn[] = useMemo(
() =>
createGridColumns(
@@ -144,7 +176,8 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
columnConfig,
visibleColumns,
formatFactory,
- onColumnResize
+ onColumnResize,
+ onColumnHide
),
[
bucketColumns,
@@ -155,6 +188,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
visibleColumns,
formatFactory,
onColumnResize,
+ onColumnHide,
]
);
@@ -184,7 +218,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => {
onRowContextMenuClick({
rowIndex,
table: firstTableRef.current,
- columns: columnConfig.columnIds,
+ columns: columnConfig.columns.map((col) => col.columnId),
});
}}
/>
diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/types.ts b/x-pack/plugins/lens/public/datatable_visualization/components/types.ts
index e2cc1daf0f900..8a280b3d15bca 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/components/types.ts
+++ b/x-pack/plugins/lens/public/datatable_visualization/components/types.ts
@@ -10,7 +10,7 @@ import type { IAggType } from 'src/plugins/data/public';
import type { Datatable, RenderMode } from 'src/plugins/expressions';
import type { FormatFactory, ILensInterpreterRenderHandlers, LensEditEvent } from '../../types';
import type { DatatableProps } from '../expression';
-import { LENS_EDIT_SORT_ACTION, LENS_EDIT_RESIZE_ACTION } from './constants';
+import { LENS_EDIT_SORT_ACTION, LENS_EDIT_RESIZE_ACTION, LENS_TOGGLE_ACTION } from './constants';
export type LensGridDirection = 'none' | Direction;
@@ -24,24 +24,13 @@ export interface LensResizeActionData {
width: number | undefined;
}
-export type LensSortAction = LensEditEvent;
-export type LensResizeAction = LensEditEvent;
-
-export interface DatatableColumns {
- columnIds: string[];
- sortBy: string;
- sortDirection: string;
- columnWidth?: DatatableColumnWidthResult[];
-}
-
-export interface DatatableColumnWidth {
+export interface LensToggleActionData {
columnId: string;
- width: number;
}
-export type DatatableColumnWidthResult = DatatableColumnWidth & {
- type: 'lens_datatable_column_width';
-};
+export type LensSortAction = LensEditEvent;
+export type LensResizeAction = LensEditEvent;
+export type LensToggleAction = LensEditEvent;
export type DatatableRenderProps = DatatableProps & {
formatFactory: FormatFactory;
diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx
index 5e51cb2c93c7c..3ee41d4e9aeed 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx
@@ -59,12 +59,22 @@ function sampleArgs() {
const args: DatatableProps['args'] = {
title: 'My fanci metric chart',
- columns: {
- columnIds: ['a', 'b', 'c'],
- sortBy: '',
- sortDirection: 'none',
- type: 'lens_datatable_columns',
- },
+ columns: [
+ {
+ columnId: 'a',
+ type: 'lens_datatable_column',
+ },
+ {
+ columnId: 'b',
+ type: 'lens_datatable_column',
+ },
+ {
+ columnId: 'c',
+ type: 'lens_datatable_column',
+ },
+ ],
+ sortingColumnId: '',
+ sortingDirection: 'none',
};
return { data, args };
diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
index 82964a03e29e5..7ead7be67947c 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx
@@ -19,19 +19,17 @@ import type {
import { getSortingCriteria } from './sorting';
import { DatatableComponent } from './components/table_basic';
+import { ColumnState } from './visualization';
import type { FormatFactory, ILensInterpreterRenderHandlers, LensMultiTable } from '../types';
-import type {
- DatatableRender,
- DatatableColumns,
- DatatableColumnWidth,
- DatatableColumnWidthResult,
-} from './components/types';
+import type { DatatableRender } from './components/types';
interface Args {
title: string;
description?: string;
- columns: DatatableColumns & { type: 'lens_datatable_columns' };
+ columns: Array;
+ sortingColumnId: string | undefined;
+ sortingDirection: 'asc' | 'desc' | 'none';
}
export interface DatatableProps {
@@ -66,7 +64,16 @@ export const getDatatable = ({
help: '',
},
columns: {
- types: ['lens_datatable_columns'],
+ types: ['lens_datatable_column'],
+ help: '',
+ multi: true,
+ },
+ sortingColumnId: {
+ types: ['string'],
+ help: '',
+ },
+ sortingDirection: {
+ types: ['string'],
help: '',
},
},
@@ -79,7 +86,7 @@ export const getDatatable = ({
firstTable.columns.forEach((column) => {
formatters[column.id] = formatFactory(column.meta?.params);
});
- const { sortBy, sortDirection } = args.columns;
+ const { sortingColumnId: sortBy, sortingDirection: sortDirection } = args;
const columnsReverseLookup = firstTable.columns.reduce<
Record
@@ -116,65 +123,27 @@ export const getDatatable = ({
},
});
-type DatatableColumnsResult = DatatableColumns & { type: 'lens_datatable_columns' };
+type DatatableColumnResult = ColumnState & { type: 'lens_datatable_column' };
-export const datatableColumns: ExpressionFunctionDefinition<
- 'lens_datatable_columns',
+export const datatableColumn: ExpressionFunctionDefinition<
+ 'lens_datatable_column',
null,
- DatatableColumns,
- DatatableColumnsResult
+ ColumnState,
+ DatatableColumnResult
> = {
- name: 'lens_datatable_columns',
+ name: 'lens_datatable_column',
aliases: [],
- type: 'lens_datatable_columns',
+ type: 'lens_datatable_column',
help: '',
inputTypes: ['null'],
args: {
- sortBy: { types: ['string'], help: '' },
- sortDirection: { types: ['string'], help: '' },
- columnIds: {
- types: ['string'],
- multi: true,
- help: '',
- },
- columnWidth: {
- types: ['lens_datatable_column_width'],
- multi: true,
- help: '',
- },
- },
- fn: function fn(input: unknown, args: DatatableColumns) {
- return {
- type: 'lens_datatable_columns',
- ...args,
- };
- },
-};
-
-export const datatableColumnWidth: ExpressionFunctionDefinition<
- 'lens_datatable_column_width',
- null,
- DatatableColumnWidth,
- DatatableColumnWidthResult
-> = {
- name: 'lens_datatable_column_width',
- aliases: [],
- type: 'lens_datatable_column_width',
- help: '',
- inputTypes: ['null'],
- args: {
- columnId: {
- types: ['string'],
- help: '',
- },
- width: {
- types: ['number'],
- help: '',
- },
+ columnId: { types: ['string'], help: '' },
+ hidden: { types: ['boolean'], help: '' },
+ width: { types: ['number'], help: '' },
},
- fn: function fn(input: unknown, args: DatatableColumnWidth) {
+ fn: function fn(input: unknown, args: ColumnState) {
return {
- type: 'lens_datatable_column_width',
+ type: 'lens_datatable_column',
...args,
};
},
@@ -213,7 +182,7 @@ export const getDatatableRenderer = (dependencies: {
data: {
rowIndex,
table,
- columns: config.args.columns.columnIds,
+ columns: config.args.columns.map((column) => column.columnId),
},
});
diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts
index 23e0a2b7918a4..f0939f6195229 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/index.ts
+++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts
@@ -29,15 +29,13 @@ export class DatatableVisualization {
editorFrame.registerVisualization(async () => {
const {
getDatatable,
- datatableColumns,
- datatableColumnWidth,
+ datatableColumn,
getDatatableRenderer,
datatableVisualization,
} = await import('../async_services');
const resolvedFormatFactory = await formatFactory;
- expressions.registerFunction(() => datatableColumns);
- expressions.registerFunction(() => datatableColumnWidth);
+ expressions.registerFunction(() => datatableColumn);
expressions.registerFunction(() => getDatatable({ formatFactory: resolvedFormatFactory }));
expressions.registerRenderer(() =>
getDatatableRenderer({
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
index 0627effa30be7..25275ba8e2249 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx
@@ -30,23 +30,15 @@ describe('Datatable Visualization', () => {
describe('#initialize', () => {
it('should initialize from the empty state', () => {
expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({
- layers: [
- {
- layerId: 'aaa',
- columns: [],
- },
- ],
+ layerId: 'aaa',
+ columns: [],
});
});
it('should initialize from a persisted state', () => {
const expectedState: DatatableVisualizationState = {
- layers: [
- {
- layerId: 'foo',
- columns: ['saved'],
- },
- ],
+ layerId: 'foo',
+ columns: [{ columnId: 'saved' }],
};
expect(datatableVisualization.initialize(mockFrame(), expectedState)).toEqual(expectedState);
});
@@ -55,12 +47,8 @@ describe('Datatable Visualization', () => {
describe('#getLayerIds', () => {
it('return the layer ids', () => {
const state: DatatableVisualizationState = {
- layers: [
- {
- layerId: 'baz',
- columns: ['a', 'b', 'c'],
- },
- ],
+ layerId: 'baz',
+ columns: [{ columnId: 'a' }, { columnId: 'b' }, { columnId: 'c' }],
};
expect(datatableVisualization.getLayerIds(state)).toEqual(['baz']);
});
@@ -69,20 +57,12 @@ describe('Datatable Visualization', () => {
describe('#clearLayer', () => {
it('should reset the layer', () => {
const state: DatatableVisualizationState = {
- layers: [
- {
- layerId: 'baz',
- columns: ['a', 'b', 'c'],
- },
- ],
+ layerId: 'baz',
+ columns: [{ columnId: 'a' }, { columnId: 'b' }, { columnId: 'c' }],
};
expect(datatableVisualization.clearLayer(state, 'baz')).toMatchObject({
- layers: [
- {
- layerId: 'baz',
- columns: [],
- },
- ],
+ layerId: 'baz',
+ columns: [],
});
});
});
@@ -113,7 +93,8 @@ describe('Datatable Visualization', () => {
it('should accept a single-layer suggestion', () => {
const suggestions = datatableVisualization.getSuggestions({
state: {
- layers: [{ layerId: 'first', columns: ['col1'] }],
+ layerId: 'first',
+ columns: [{ columnId: 'col1' }],
},
table: {
isMultiRow: true,
@@ -130,7 +111,8 @@ describe('Datatable Visualization', () => {
it('should not make suggestions when the table is unchanged', () => {
const suggestions = datatableVisualization.getSuggestions({
state: {
- layers: [{ layerId: 'first', columns: ['col1'] }],
+ layerId: 'first',
+ columns: [{ columnId: 'col1' }],
},
table: {
isMultiRow: true,
@@ -147,7 +129,8 @@ describe('Datatable Visualization', () => {
it('should not make suggestions when multiple layers are involved', () => {
const suggestions = datatableVisualization.getSuggestions({
state: {
- layers: [{ layerId: 'first', columns: ['col1'] }],
+ layerId: 'first',
+ columns: [{ columnId: 'col1' }],
},
table: {
isMultiRow: true,
@@ -164,7 +147,8 @@ describe('Datatable Visualization', () => {
it('should not make suggestions when the suggestion keeps a different layer', () => {
const suggestions = datatableVisualization.getSuggestions({
state: {
- layers: [{ layerId: 'older', columns: ['col1'] }],
+ layerId: 'older',
+ columns: [{ columnId: 'col1' }],
},
table: {
isMultiRow: true,
@@ -203,7 +187,8 @@ describe('Datatable Visualization', () => {
datatableVisualization.getConfiguration({
layerId: 'first',
state: {
- layers: [{ layerId: 'first', columns: [] }],
+ layerId: 'first',
+ columns: [],
},
frame,
}).groups
@@ -218,7 +203,8 @@ describe('Datatable Visualization', () => {
const filterOperations = datatableVisualization.getConfiguration({
layerId: 'first',
state: {
- layers: [{ layerId: 'first', columns: [] }],
+ layerId: 'first',
+ columns: [],
},
frame,
}).groups[0].filterOperations;
@@ -249,7 +235,8 @@ describe('Datatable Visualization', () => {
const filterOperations = datatableVisualization.getConfiguration({
layerId: 'first',
state: {
- layers: [{ layerId: 'first', columns: [] }],
+ layerId: 'first',
+ columns: [],
},
frame,
}).groups[1].filterOperations;
@@ -274,7 +261,6 @@ describe('Datatable Visualization', () => {
it('reorders the rendered colums based on the order from the datasource', () => {
const datasource = createMockDatasource('test');
- const layer = { layerId: 'a', columns: ['b', 'c'] };
const frame = mockFrame();
frame.datasourceLayers = { a: datasource.publicAPIMock };
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
@@ -282,7 +268,10 @@ describe('Datatable Visualization', () => {
expect(
datatableVisualization.getConfiguration({
layerId: 'a',
- state: { layers: [layer] },
+ state: {
+ layerId: 'a',
+ columns: [{ columnId: 'b' }, { columnId: 'c' }],
+ },
frame,
}).groups[1].accessors
).toEqual([{ columnId: 'c' }, { columnId: 'b' }]);
@@ -291,95 +280,75 @@ describe('Datatable Visualization', () => {
describe('#removeDimension', () => {
it('allows columns to be removed', () => {
- const layer = { layerId: 'layer1', columns: ['b', 'c'] };
expect(
datatableVisualization.removeDimension({
- prevState: { layers: [layer] },
+ prevState: {
+ layerId: 'layer1',
+ columns: [{ columnId: 'b' }, { columnId: 'c' }],
+ },
layerId: 'layer1',
columnId: 'b',
})
).toEqual({
- layers: [
- {
- layerId: 'layer1',
- columns: ['c'],
- },
- ],
+ layerId: 'layer1',
+ columns: [{ columnId: 'c' }],
});
});
it('should handle correctly the sorting state on removing dimension', () => {
- const layer = { layerId: 'layer1', columns: ['b', 'c'] };
+ const state = { layerId: 'layer1', columns: [{ columnId: 'b' }, { columnId: 'c' }] };
expect(
datatableVisualization.removeDimension({
- prevState: { layers: [layer], sorting: { columnId: 'b', direction: 'asc' } },
+ prevState: { ...state, sorting: { columnId: 'b', direction: 'asc' } },
layerId: 'layer1',
columnId: 'b',
})
).toEqual({
sorting: undefined,
- layers: [
- {
- layerId: 'layer1',
- columns: ['c'],
- },
- ],
+ layerId: 'layer1',
+ columns: [{ columnId: 'c' }],
});
expect(
datatableVisualization.removeDimension({
- prevState: { layers: [layer], sorting: { columnId: 'c', direction: 'asc' } },
+ prevState: { ...state, sorting: { columnId: 'c', direction: 'asc' } },
layerId: 'layer1',
columnId: 'b',
})
).toEqual({
sorting: { columnId: 'c', direction: 'asc' },
- layers: [
- {
- layerId: 'layer1',
- columns: ['c'],
- },
- ],
+ layerId: 'layer1',
+ columns: [{ columnId: 'c' }],
});
});
});
describe('#setDimension', () => {
it('allows columns to be added', () => {
- const layer = { layerId: 'layer1', columns: ['b', 'c'] };
expect(
datatableVisualization.setDimension({
- prevState: { layers: [layer] },
+ prevState: { layerId: 'layer1', columns: [{ columnId: 'b' }, { columnId: 'c' }] },
layerId: 'layer1',
columnId: 'd',
groupId: '',
})
).toEqual({
- layers: [
- {
- layerId: 'layer1',
- columns: ['b', 'c', 'd'],
- },
- ],
+ layerId: 'layer1',
+ columns: [{ columnId: 'b' }, { columnId: 'c' }, { columnId: 'd' }],
});
});
it('does not set a duplicate dimension', () => {
- const layer = { layerId: 'layer1', columns: ['b', 'c'] };
expect(
datatableVisualization.setDimension({
- prevState: { layers: [layer] },
+ prevState: { layerId: 'layer1', columns: [{ columnId: 'b' }, { columnId: 'c' }] },
layerId: 'layer1',
columnId: 'b',
groupId: '',
})
).toEqual({
- layers: [
- {
- layerId: 'layer1',
- columns: ['b', 'c'],
- },
- ],
+ layerId: 'layer1',
+ columns: [{ columnId: 'b' }, { columnId: 'c' }],
});
});
});
@@ -387,7 +356,6 @@ describe('Datatable Visualization', () => {
describe('#toExpression', () => {
it('reorders the rendered colums based on the order from the datasource', () => {
const datasource = createMockDatasource('test');
- const layer = { layerId: 'a', columns: ['b', 'c'] };
const frame = mockFrame();
frame.datasourceLayers = { a: datasource.publicAPIMock };
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
@@ -398,24 +366,35 @@ describe('Datatable Visualization', () => {
});
const expression = datatableVisualization.toExpression(
- { layers: [layer] },
+ { layerId: 'a', columns: [{ columnId: 'b' }, { columnId: 'c' }] },
frame.datasourceLayers
) as Ast;
- const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns');
+ const tableArgs = buildExpression(expression).findFunction('lens_datatable');
expect(tableArgs).toHaveLength(1);
- expect(tableArgs[0].arguments).toEqual({
- columnIds: ['c', 'b'],
- sortBy: [''],
- sortDirection: ['none'],
- columnWidth: [],
+ expect(tableArgs[0].arguments).toEqual(
+ expect.objectContaining({
+ sortingColumnId: [''],
+ sortingDirection: ['none'],
+ })
+ );
+ const columnArgs = buildExpression(expression).findFunction('lens_datatable_column');
+ expect(columnArgs).toHaveLength(2);
+ expect(columnArgs[0].arguments).toEqual({
+ columnId: ['c'],
+ hidden: [],
+ width: [],
+ });
+ expect(columnArgs[1].arguments).toEqual({
+ columnId: ['b'],
+ hidden: [],
+ width: [],
});
});
it('returns no expression if the metric dimension is not defined', () => {
const datasource = createMockDatasource('test');
- const layer = { layerId: 'a', columns: ['b', 'c'] };
const frame = mockFrame();
frame.datasourceLayers = { a: datasource.publicAPIMock };
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
@@ -426,7 +405,7 @@ describe('Datatable Visualization', () => {
});
const expression = datatableVisualization.toExpression(
- { layers: [layer] },
+ { layerId: 'a', columns: [{ columnId: 'b' }, { columnId: 'c' }] },
frame.datasourceLayers
);
@@ -437,7 +416,6 @@ describe('Datatable Visualization', () => {
describe('#getErrorMessages', () => {
it('returns undefined if the datasource is missing a metric dimension', () => {
const datasource = createMockDatasource('test');
- const layer = { layerId: 'a', columns: ['b', 'c'] };
const frame = mockFrame();
frame.datasourceLayers = { a: datasource.publicAPIMock };
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
@@ -447,14 +425,16 @@ describe('Datatable Visualization', () => {
label: 'label',
});
- const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame);
+ const error = datatableVisualization.getErrorMessages(
+ { layerId: 'a', columns: [{ columnId: 'b' }, { columnId: 'c' }] },
+ frame
+ );
expect(error).toBeUndefined();
});
it('returns undefined if the metric dimension is defined', () => {
const datasource = createMockDatasource('test');
- const layer = { layerId: 'a', columns: ['b', 'c'] };
const frame = mockFrame();
frame.datasourceLayers = { a: datasource.publicAPIMock };
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
@@ -464,7 +444,10 @@ describe('Datatable Visualization', () => {
label: 'label',
});
- const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame);
+ const error = datatableVisualization.getErrorMessages(
+ { layerId: 'a', columns: [{ columnId: 'b' }, { columnId: 'c' }] },
+ frame
+ );
expect(error).toBeUndefined();
});
@@ -473,12 +456,8 @@ describe('Datatable Visualization', () => {
describe('#onEditAction', () => {
it('should add a sort column to the state', () => {
const currentState: DatatableVisualizationState = {
- layers: [
- {
- layerId: 'foo',
- columns: ['saved'],
- },
- ],
+ layerId: 'foo',
+ columns: [{ columnId: 'saved' }],
};
expect(
datatableVisualization.onEditAction!(currentState, {
@@ -496,12 +475,8 @@ describe('Datatable Visualization', () => {
it('should add a custom width to a column in the state', () => {
const currentState: DatatableVisualizationState = {
- layers: [
- {
- layerId: 'foo',
- columns: ['saved'],
- },
- ],
+ layerId: 'foo',
+ columns: [{ columnId: 'saved' }],
};
expect(
datatableVisualization.onEditAction!(currentState, {
@@ -510,29 +485,14 @@ describe('Datatable Visualization', () => {
})
).toEqual({
...currentState,
- columnWidth: [
- {
- columnId: 'saved',
- width: 500,
- },
- ],
+ columns: [{ columnId: 'saved', width: 500 }],
});
});
it('should clear custom width value for the column from the state', () => {
const currentState: DatatableVisualizationState = {
- layers: [
- {
- layerId: 'foo',
- columns: ['saved'],
- },
- ],
- columnWidth: [
- {
- columnId: 'saved',
- width: 500,
- },
- ],
+ layerId: 'foo',
+ columns: [{ columnId: 'saved', width: 5000 }],
};
expect(
datatableVisualization.onEditAction!(currentState, {
@@ -541,7 +501,7 @@ describe('Datatable Visualization', () => {
})
).toEqual({
...currentState,
- columnWidth: [],
+ columns: [{ columnId: 'saved', width: undefined }],
});
});
});
diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
index 6a221396b8a84..77fda43c37fef 100644
--- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
+++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx
@@ -5,37 +5,35 @@
* 2.0.
*/
+import React from 'react';
+import { render } from 'react-dom';
import { Ast } from '@kbn/interpreter/common';
+import { I18nProvider } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import type {
SuggestionRequest,
Visualization,
VisualizationSuggestion,
- Operation,
DatasourcePublicAPI,
} from '../types';
-import type { DatatableColumnWidth } from './components/types';
import { LensIconChartDatatable } from '../assets/chart_datatable';
+import { TableDimensionEditor } from './components/dimension_editor';
-export interface DatatableLayerState {
- layerId: string;
- columns: string[];
+export interface ColumnState {
+ columnId: string;
+ width?: number;
+ hidden?: boolean;
}
-export interface DatatableVisualizationState {
- layers: DatatableLayerState[];
- sorting?: {
- columnId: string | undefined;
- direction: 'asc' | 'desc' | 'none';
- };
- columnWidth?: DatatableColumnWidth[];
+export interface SortingState {
+ columnId: string | undefined;
+ direction: 'asc' | 'desc' | 'none';
}
-function newLayerState(layerId: string): DatatableLayerState {
- return {
- layerId,
- columns: [],
- };
+export interface DatatableVisualizationState {
+ columns: ColumnState[];
+ layerId: string;
+ sorting?: SortingState;
}
export const datatableVisualization: Visualization = {
@@ -56,12 +54,13 @@ export const datatableVisualization: Visualization
},
getLayerIds(state) {
- return state.layers.map((l) => l.layerId);
+ return [state.layerId];
},
clearLayer(state) {
return {
- layers: state.layers.map((l) => newLayerState(l.layerId)),
+ ...state,
+ columns: [],
};
},
@@ -79,7 +78,8 @@ export const datatableVisualization: Visualization
initialize(frame, state) {
return (
state || {
- layers: [newLayerState(frame.addNewLayer())],
+ columns: [],
+ layerId: frame.addNewLayer(),
}
);
},
@@ -126,12 +126,8 @@ export const datatableVisualization: Visualization
// table with >= 10 columns will have a score of 0.4, fewer columns reduce score
score: (Math.min(table.columns.length, 10) / 10) * 0.4,
state: {
- layers: [
- {
- layerId: table.layerId,
- columns: table.columns.map((col) => col.columnId),
- },
- ],
+ layerId: table.layerId,
+ columns: table.columns.map((col) => ({ columnId: col.columnId })),
},
previewIcon: LensIconChartDatatable,
// tables are hidden from suggestion bar, but used for drag & drop and chart switching
@@ -144,6 +140,11 @@ export const datatableVisualization: Visualization
const { sortedColumns, datasource } =
getDataSourceAndSortedColumns(state, frame.datasourceLayers, layerId) || {};
+ const columnMap: Record = {};
+ state.columns.forEach((column) => {
+ columnMap[column.columnId] = column;
+ });
+
if (!sortedColumns) {
return { groups: [] };
}
@@ -155,61 +156,68 @@ export const datatableVisualization: Visualization
groupLabel: i18n.translate('xpack.lens.datatable.breakdown', {
defaultMessage: 'Break down by',
}),
- layerId: state.layers[0].layerId,
+ layerId: state.layerId,
accessors: sortedColumns
.filter((c) => datasource!.getOperationForColumnId(c)?.isBucketed)
- .map((accessor) => ({ columnId: accessor })),
+ .map((accessor) => ({
+ columnId: accessor,
+ triggerIcon: columnMap[accessor].hidden ? 'invisible' : undefined,
+ })),
supportsMoreColumns: true,
filterOperations: (op) => op.isBucketed,
dataTestSubj: 'lnsDatatable_column',
+ enableDimensionEditor: true,
},
{
groupId: 'metrics',
groupLabel: i18n.translate('xpack.lens.datatable.metrics', {
defaultMessage: 'Metrics',
}),
- layerId: state.layers[0].layerId,
+ layerId: state.layerId,
accessors: sortedColumns
.filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed)
- .map((accessor) => ({ columnId: accessor })),
+ .map((accessor) => ({
+ columnId: accessor,
+ triggerIcon: columnMap[accessor].hidden ? 'invisible' : undefined,
+ })),
supportsMoreColumns: true,
filterOperations: (op) => !op.isBucketed,
required: true,
dataTestSubj: 'lnsDatatable_metrics',
+ enableDimensionEditor: true,
},
],
};
},
- setDimension({ prevState, layerId, columnId }) {
+ setDimension({ prevState, columnId }) {
+ if (prevState.columns.some((column) => column.columnId === columnId)) {
+ return prevState;
+ }
return {
...prevState,
- layers: prevState.layers.map((l) => {
- if (l.layerId !== layerId || l.columns.includes(columnId)) {
- return l;
- }
- return { ...l, columns: [...l.columns, columnId] };
- }),
+ columns: [...prevState.columns, { columnId }],
};
},
- removeDimension({ prevState, layerId, columnId }) {
+ removeDimension({ prevState, columnId }) {
return {
...prevState,
- layers: prevState.layers.map((l) =>
- l.layerId === layerId
- ? {
- ...l,
- columns: l.columns.filter((c) => c !== columnId),
- }
- : l
- ),
+ columns: prevState.columns.filter((column) => column.columnId !== columnId),
sorting: prevState.sorting?.columnId === columnId ? undefined : prevState.sorting,
};
},
+ renderDimensionEditor(domElement, props) {
+ render(
+
+
+ ,
+ domElement
+ );
+ },
toExpression(state, datasourceLayers, { title, description } = {}): Ast | null {
const { sortedColumns, datasource } =
- getDataSourceAndSortedColumns(state, datasourceLayers, state.layers[0].layerId) || {};
+ getDataSourceAndSortedColumns(state, datasourceLayers, state.layerId) || {};
if (
sortedColumns?.length &&
@@ -218,9 +226,14 @@ export const datatableVisualization: Visualization
return null;
}
- const operations = sortedColumns!
- .map((columnId) => ({ columnId, operation: datasource!.getOperationForColumnId(columnId) }))
- .filter((o): o is { columnId: string; operation: Operation } => !!o.operation);
+ const columnMap: Record = {};
+ state.columns.forEach((column) => {
+ columnMap[column.columnId] = column;
+ });
+
+ const columns = sortedColumns!
+ .filter((columnId) => datasource!.getOperationForColumnId(columnId))
+ .map((columnId) => columnMap[columnId]);
return {
type: 'expression',
@@ -231,35 +244,22 @@ export const datatableVisualization: Visualization
arguments: {
title: [title || ''],
description: [description || ''],
- columns: [
- {
- type: 'expression',
- chain: [
- {
- type: 'function',
- function: 'lens_datatable_columns',
- arguments: {
- columnIds: operations.map((o) => o.columnId),
- sortBy: [state.sorting?.columnId || ''],
- sortDirection: [state.sorting?.direction || 'none'],
- columnWidth: (state.columnWidth || []).map((columnWidth) => ({
- type: 'expression',
- chain: [
- {
- type: 'function',
- function: 'lens_datatable_column_width',
- arguments: {
- columnId: [columnWidth.columnId],
- width: [columnWidth.width],
- },
- },
- ],
- })),
- },
+ columns: columns.map((column) => ({
+ type: 'expression',
+ chain: [
+ {
+ type: 'function',
+ function: 'lens_datatable_column',
+ arguments: {
+ columnId: [column.columnId],
+ hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden],
+ width: typeof column.width === 'undefined' ? [] : [column.width],
},
- ],
- },
- ],
+ },
+ ],
+ })),
+ sortingColumnId: [state.sorting?.columnId || ''],
+ sortingDirection: [state.sorting?.direction || 'none'],
},
},
],
@@ -280,15 +280,34 @@ export const datatableVisualization: Visualization
direction: event.data.direction,
},
};
+ case 'toggle':
+ return {
+ ...state,
+ columns: state.columns.map((column) => {
+ if (column.columnId === event.data.columnId) {
+ return {
+ ...column,
+ hidden: !column.hidden,
+ };
+ } else {
+ return column;
+ }
+ }),
+ };
case 'resize':
+ const targetWidth = event.data.width;
return {
...state,
- columnWidth: [
- ...(state.columnWidth || []).filter(({ columnId }) => columnId !== event.data.columnId),
- ...(event.data.width !== undefined
- ? [{ columnId: event.data.columnId, width: event.data.width }]
- : []),
- ],
+ columns: state.columns.map((column) => {
+ if (column.columnId === event.data.columnId) {
+ return {
+ ...column,
+ width: targetWidth,
+ };
+ } else {
+ return column;
+ }
+ }),
};
default:
return state;
@@ -301,13 +320,11 @@ function getDataSourceAndSortedColumns(
datasourceLayers: Record,
layerId: string
) {
- const layer = state.layers.find((l: DatatableLayerState) => l.layerId === layerId);
- if (!layer) {
- return undefined;
- }
- const datasource = datasourceLayers[layer.layerId];
+ const datasource = datasourceLayers[state.layerId];
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
// When we add a column it could be empty, and therefore have no order
- const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns)));
+ const sortedColumns = Array.from(
+ new Set(originalOrder.concat(state.columns.map(({ columnId }) => columnId)))
+ );
return { datasource, sortedColumns };
}
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx
index e3a30883a2209..a3d5c6fd22fcd 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/color_indicator.tsx
@@ -49,6 +49,16 @@ export function ColorIndicator({
})}
/>
)}
+ {accessorConfig.triggerIcon === 'invisible' && (
+
+ )}
{accessorConfig.triggerIcon === 'colorBy' && (
void;
@@ -368,7 +370,7 @@ export type VisualizationDimensionEditorProps = VisualizationConfig
export interface AccessorConfig {
columnId: string;
- triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none';
+ triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none' | 'invisible';
color?: string;
palette?: string[];
}
@@ -649,6 +651,7 @@ export interface LensBrushEvent {
interface LensEditContextMapping {
[LENS_EDIT_SORT_ACTION]: LensSortActionData;
[LENS_EDIT_RESIZE_ACTION]: LensResizeActionData;
+ [LENS_TOGGLE_ACTION]: LensToggleActionData;
}
type LensEditSupportedActions = keyof LensEditContextMapping;
diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts
index 077204b07ed73..01329d85baf00 100644
--- a/x-pack/plugins/lens/server/migrations.test.ts
+++ b/x-pack/plugins/lens/server/migrations.test.ts
@@ -597,4 +597,77 @@ describe('Lens migrations', () => {
expect(layersWithSuggestedPriority).toEqual(0);
});
});
+
+ describe('7.12.0 restructure datatable state', () => {
+ const context = ({ log: { warning: () => {} } } as unknown) as SavedObjectMigrationContext;
+ const example = {
+ type: 'lens',
+ id: 'mock-saved-object-id',
+ attributes: {
+ state: {
+ datasourceStates: {
+ indexpattern: {},
+ },
+ visualization: {
+ layers: [
+ {
+ layerId: 'first',
+ columns: ['a', 'b', 'c'],
+ },
+ ],
+ sorting: {
+ columnId: 'a',
+ direction: 'asc',
+ },
+ },
+ query: { query: '', language: 'kuery' },
+ filters: [],
+ },
+ title: 'Table',
+ visualizationType: 'lnsDatatable',
+ },
+ };
+
+ it('should not touch non datatable visualization', () => {
+ const xyChart = {
+ ...example,
+ attributes: { ...example.attributes, visualizationType: 'xy' },
+ };
+ const result = migrations['7.12.0'](xyChart, context) as ReturnType<
+ SavedObjectMigrationFn
+ >;
+ expect(result).toBe(xyChart);
+ });
+
+ it('should remove layer array and reshape state', () => {
+ const result = migrations['7.12.0'](example, context) as ReturnType<
+ SavedObjectMigrationFn
+ >;
+ expect(result.attributes.state.visualization).toEqual({
+ layerId: 'first',
+ columns: [
+ {
+ columnId: 'a',
+ },
+ {
+ columnId: 'b',
+ },
+ {
+ columnId: 'c',
+ },
+ ],
+ sorting: {
+ columnId: 'a',
+ direction: 'asc',
+ },
+ });
+ // should leave other parts alone
+ expect(result.attributes.state.datasourceStates).toEqual(
+ example.attributes.state.datasourceStates
+ );
+ expect(result.attributes.state.query).toEqual(example.attributes.state.query);
+ expect(result.attributes.state.filters).toEqual(example.attributes.state.filters);
+ expect(result.attributes.title).toEqual(example.attributes.title);
+ });
+ });
});
diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations.ts
index bb078ff204f2b..4c6dfcd7949be 100644
--- a/x-pack/plugins/lens/server/migrations.ts
+++ b/x-pack/plugins/lens/server/migrations.ts
@@ -83,6 +83,29 @@ interface XYStatePost77 {
layers: Array>;
}
+interface DatatableStatePre711 {
+ layers: Array<{
+ layerId: string;
+ columns: string[];
+ }>;
+ sorting?: {
+ columnId: string | undefined;
+ direction: 'asc' | 'desc' | 'none';
+ };
+}
+interface DatatableStatePost711 {
+ layerId: string;
+ columns: Array<{
+ columnId: string;
+ width?: number;
+ hidden?: boolean;
+ }>;
+ sorting?: {
+ columnId: string | undefined;
+ direction: 'asc' | 'desc' | 'none';
+ };
+}
+
/**
* Removes the `lens_auto_date` subexpression from a stored expression
* string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"}
@@ -334,6 +357,36 @@ const removeSuggestedPriority: SavedObjectMigrationFn,
+ LensDocShape
+> = (doc) => {
+ // nothing to do for non-datatable visualizations
+ if (doc.attributes.visualizationType !== 'lnsDatatable')
+ return (doc as unknown) as SavedObjectUnsanitizedDoc>;
+ const oldState = doc.attributes.state.visualization;
+ const layer = oldState.layers[0] || {
+ layerId: '',
+ columns: [],
+ };
+ // put together new saved object format
+ const newDoc: SavedObjectUnsanitizedDoc> = {
+ ...doc,
+ attributes: {
+ ...doc.attributes,
+ state: {
+ ...doc.attributes.state,
+ visualization: {
+ sorting: oldState.sorting,
+ layerId: layer.layerId,
+ columns: layer.columns.map((columnId) => ({ columnId })),
+ },
+ },
+ },
+ };
+ return newDoc;
+};
+
export const migrations: SavedObjectMigrationMap = {
'7.7.0': removeInvalidAccessors,
// The order of these migrations matter, since the timefield migration relies on the aggConfigs
@@ -341,4 +394,5 @@ export const migrations: SavedObjectMigrationMap = {
'7.8.0': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context),
'7.10.0': extractReferences,
'7.11.0': removeSuggestedPriority,
+ '7.12.0': transformTableState,
};
diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts
index 6cbd18bdeef04..10b1f4d30145f 100644
--- a/x-pack/test/functional/apps/lens/index.ts
+++ b/x-pack/test/functional/apps/lens/index.ts
@@ -29,6 +29,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
this.tags(['ciGroup4', 'skipFirefox']);
loadTestFile(require.resolve('./smokescreen'));
+ loadTestFile(require.resolve('./table'));
loadTestFile(require.resolve('./dashboard'));
loadTestFile(require.resolve('./persistent_context'));
loadTestFile(require.resolve('./colors'));
diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/table.ts
new file mode 100644
index 0000000000000..f79d1c342b72f
--- /dev/null
+++ b/x-pack/test/functional/apps/lens/table.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
+ const listingTable = getService('listingTable');
+ const find = getService('find');
+
+ describe('lens datatable', () => {
+ it('should able to sort a table by a column', async () => {
+ await PageObjects.visualize.gotoVisualizationLandingPage();
+ await listingTable.searchForItemWithName('lnsXYvis');
+ await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
+ await PageObjects.lens.goToTimeRange();
+ await PageObjects.lens.switchToVisualization('lnsDatatable');
+ // Sort by number
+ await PageObjects.lens.changeTableSortingBy(2, 'asc');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ expect(await PageObjects.lens.getDatatableCellText(0, 2)).to.eql('17,246');
+ // Now sort by IP
+ await PageObjects.lens.changeTableSortingBy(0, 'asc');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('78.83.247.30');
+ // Change the sorting
+ await PageObjects.lens.changeTableSortingBy(0, 'desc');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('169.228.188.120');
+ // Remove the sorting
+ await PageObjects.lens.changeTableSortingBy(0, 'none');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ expect(await PageObjects.lens.isDatatableHeaderSorted(0)).to.eql(false);
+ });
+
+ it('should able to use filters cell actions in table', async () => {
+ const firstCellContent = await PageObjects.lens.getDatatableCellText(0, 0);
+ await PageObjects.lens.clickTableCellAction(0, 0, 'lensDatatableFilterOut');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ expect(
+ await find.existsByCssSelector(
+ `[data-test-subj*="filter-value-${firstCellContent}"][data-test-subj*="filter-negated"]`
+ )
+ ).to.eql(true);
+ });
+
+ it('should allow to configure column visibility', async () => {
+ expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('Top values of ip');
+ expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('@timestamp per 3 hours');
+ expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal('Average of bytes');
+
+ await PageObjects.lens.toggleColumnVisibility('lnsDatatable_column > lns-dimensionTrigger');
+
+ expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('@timestamp per 3 hours');
+ expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('Average of bytes');
+
+ await PageObjects.lens.toggleColumnVisibility('lnsDatatable_column > lns-dimensionTrigger');
+
+ expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('Top values of ip');
+ expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('@timestamp per 3 hours');
+ expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal('Average of bytes');
+ });
+ });
+}
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 37d97cd014c9f..f6960600a6d7c 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -562,6 +562,15 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
return buttonEl.click();
},
+ async toggleColumnVisibility(dimension: string) {
+ await this.openDimensionEditor(dimension);
+ const id = 'lns-table-column-hidden';
+ const isChecked = await testSubjects.isEuiSwitchChecked(id);
+ await testSubjects.setEuiSwitch(id, isChecked ? 'uncheck' : 'check');
+ await this.closeDimensionEditor();
+ await PageObjects.header.waitUntilLoadingHasFinished();
+ },
+
async clickTableCellAction(rowIndex = 0, colIndex = 0, actionTestSub: string) {
const el = await this.getDatatableCell(rowIndex, colIndex);
await el.focus();
From e676617f69a5959f6a6219e0b067592d2889391a Mon Sep 17 00:00:00 2001
From: Bhavya RM
Date: Thu, 4 Feb 2021 11:04:30 -0500
Subject: [PATCH 05/42] Test user for maps tests under import geoJSON tests
(#86015)
test user assignment for test files under import geoJSON files
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../import_geojson/add_layer_import_panel.js | 8 ++++++-
.../import_geojson/file_indexing_panel.js | 9 ++++++++
x-pack/test/functional/config.js | 22 +++++++++++++++++++
3 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/functional/apps/maps/import_geojson/add_layer_import_panel.js b/x-pack/test/functional/apps/maps/import_geojson/add_layer_import_panel.js
index 390c7af98c653..46b87b1c4195c 100644
--- a/x-pack/test/functional/apps/maps/import_geojson/add_layer_import_panel.js
+++ b/x-pack/test/functional/apps/maps/import_geojson/add_layer_import_panel.js
@@ -8,18 +8,24 @@
import expect from '@kbn/expect';
import path from 'path';
-export default function ({ getPageObjects }) {
+export default function ({ getPageObjects, getService }) {
const PageObjects = getPageObjects(['maps', 'common']);
const IMPORT_FILE_PREVIEW_NAME = 'Import File';
const FILE_LOAD_DIR = 'test_upload_files';
const DEFAULT_LOAD_FILE_NAME = 'point.json';
+ const security = getService('security');
describe('GeoJSON import layer panel', () => {
before(async () => {
+ await security.testUser.setRoles(['global_maps_all', 'geoall_data_writer']);
await PageObjects.maps.openNewMap();
});
+ after(async () => {
+ await security.testUser.restoreDefaults();
+ });
+
beforeEach(async () => {
await PageObjects.maps.clickAddLayer();
await PageObjects.maps.selectGeoJsonUploadSource();
diff --git a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js
index ea8366d809fb7..4496b59393eec 100644
--- a/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js
+++ b/x-pack/test/functional/apps/maps/import_geojson/file_indexing_panel.js
@@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['maps', 'common']);
const testSubjects = getService('testSubjects');
const log = getService('log');
+ const security = getService('security');
const IMPORT_FILE_PREVIEW_NAME = 'Import File';
const FILE_LOAD_DIR = 'test_upload_files';
@@ -37,9 +38,17 @@ export default function ({ getService, getPageObjects }) {
describe('On GeoJSON index name & pattern operation complete', () => {
before(async () => {
+ await security.testUser.setRoles(
+ ['global_maps_all', 'geoall_data_writer', 'global_index_pattern_management_all'],
+ false
+ );
await PageObjects.maps.openNewMap();
});
+ after(async () => {
+ await security.testUser.restoreDefaults();
+ });
+
beforeEach(async () => {
await PageObjects.maps.clickAddLayer();
await PageObjects.maps.selectGeoJsonUploadSource();
diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js
index f2be2974986fb..4d63f033f8756 100644
--- a/x-pack/test/functional/config.js
+++ b/x-pack/test/functional/config.js
@@ -377,6 +377,28 @@ export default async function ({ readConfigFile }) {
},
},
+ geoall_data_writer: {
+ elasticsearch: {
+ indices: [
+ {
+ names: ['*'],
+ privileges: ['create', 'read', 'view_index_metadata', 'monitor', 'create_index'],
+ },
+ ],
+ },
+ },
+
+ global_index_pattern_management_all: {
+ kibana: [
+ {
+ feature: {
+ indexPatterns: ['all'],
+ },
+ spaces: ['*'],
+ },
+ ],
+ },
+
global_devtools_read: {
kibana: [
{
From 7a42a6f410abc3ef5aab03d72e6d688fe484e780 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Thu, 4 Feb 2021 17:20:50 +0100
Subject: [PATCH 06/42] [APM] Enabling yesterday option when 24 hours is
selected (#90017)
* enabling yesterday option when 24 hours is selected
* addressing PR comments
* addressing PR comments
* enabling select box
---
.../common/utils/formatters/datetime.test.ts | 29 +++++--
.../apm/common/utils/formatters/datetime.ts | 26 +++---
.../shared/time_comparison/index.test.tsx | 83 ++++++++++++++++++-
.../shared/time_comparison/index.tsx | 53 ++++++++----
4 files changed, 154 insertions(+), 37 deletions(-)
diff --git a/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts b/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts
index 6aee1e2b9842d..9efb7184f3927 100644
--- a/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts
+++ b/x-pack/plugins/apm/common/utils/formatters/datetime.test.ts
@@ -170,37 +170,52 @@ describe('date time formatters', () => {
it('milliseconds', () => {
const start = moment('2019-10-29 08:00:00.001');
const end = moment('2019-10-29 08:00:00.005');
- expect(getDateDifference(start, end, 'milliseconds')).toEqual(4);
+ expect(
+ getDateDifference({ start, end, unitOfTime: 'milliseconds' })
+ ).toEqual(4);
});
it('seconds', () => {
const start = moment('2019-10-29 08:00:00');
const end = moment('2019-10-29 08:00:10');
- expect(getDateDifference(start, end, 'seconds')).toEqual(10);
+ expect(getDateDifference({ start, end, unitOfTime: 'seconds' })).toEqual(
+ 10
+ );
});
it('minutes', () => {
const start = moment('2019-10-29 08:00:00');
const end = moment('2019-10-29 08:15:00');
- expect(getDateDifference(start, end, 'minutes')).toEqual(15);
+ expect(getDateDifference({ start, end, unitOfTime: 'minutes' })).toEqual(
+ 15
+ );
});
it('hours', () => {
const start = moment('2019-10-29 08:00:00');
const end = moment('2019-10-29 10:00:00');
- expect(getDateDifference(start, end, 'hours')).toEqual(2);
+ expect(getDateDifference({ start, end, unitOfTime: 'hours' })).toEqual(2);
});
it('days', () => {
const start = moment('2019-10-29 08:00:00');
const end = moment('2019-10-30 10:00:00');
- expect(getDateDifference(start, end, 'days')).toEqual(1);
+ expect(getDateDifference({ start, end, unitOfTime: 'days' })).toEqual(1);
});
it('months', () => {
const start = moment('2019-10-29 08:00:00');
const end = moment('2019-12-29 08:00:00');
- expect(getDateDifference(start, end, 'months')).toEqual(2);
+ expect(getDateDifference({ start, end, unitOfTime: 'months' })).toEqual(
+ 2
+ );
});
it('years', () => {
const start = moment('2019-10-29 08:00:00');
const end = moment('2020-10-29 08:00:00');
- expect(getDateDifference(start, end, 'years')).toEqual(1);
+ expect(getDateDifference({ start, end, unitOfTime: 'years' })).toEqual(1);
+ });
+ it('precise days', () => {
+ const start = moment('2019-10-29 08:00:00');
+ const end = moment('2019-10-30 10:00:00');
+ expect(
+ getDateDifference({ start, end, unitOfTime: 'days', precise: true })
+ ).toEqual(1.0833333333333333);
});
});
});
diff --git a/x-pack/plugins/apm/common/utils/formatters/datetime.ts b/x-pack/plugins/apm/common/utils/formatters/datetime.ts
index 624a0b8a664bc..88f70753f47c8 100644
--- a/x-pack/plugins/apm/common/utils/formatters/datetime.ts
+++ b/x-pack/plugins/apm/common/utils/formatters/datetime.ts
@@ -58,37 +58,43 @@ function getDateFormat(dateUnit: DateUnit) {
}
}
-export const getDateDifference = (
- start: moment.Moment,
- end: moment.Moment,
- unitOfTime: DateUnit | TimeUnit
-) => end.diff(start, unitOfTime);
+export const getDateDifference = ({
+ start,
+ end,
+ unitOfTime,
+ precise,
+}: {
+ start: moment.Moment;
+ end: moment.Moment;
+ unitOfTime: DateUnit | TimeUnit;
+ precise?: boolean;
+}) => end.diff(start, unitOfTime, precise);
function getFormatsAccordingToDateDifference(
start: moment.Moment,
end: moment.Moment
) {
- if (getDateDifference(start, end, 'years') >= 5) {
+ if (getDateDifference({ start, end, unitOfTime: 'years' }) >= 5) {
return { dateFormat: getDateFormat('years') };
}
- if (getDateDifference(start, end, 'months') >= 5) {
+ if (getDateDifference({ start, end, unitOfTime: 'months' }) >= 5) {
return { dateFormat: getDateFormat('months') };
}
const dateFormatWithDays = getDateFormat('days');
- if (getDateDifference(start, end, 'days') > 1) {
+ if (getDateDifference({ start, end, unitOfTime: 'days' }) > 1) {
return { dateFormat: dateFormatWithDays };
}
- if (getDateDifference(start, end, 'minutes') >= 1) {
+ if (getDateDifference({ start, end, unitOfTime: 'minutes' }) >= 1) {
return {
dateFormat: dateFormatWithDays,
timeFormat: getTimeFormat('minutes'),
};
}
- if (getDateDifference(start, end, 'seconds') >= 10) {
+ if (getDateDifference({ start, end, unitOfTime: 'seconds' }) >= 10) {
return {
dateFormat: dateFormatWithDays,
timeFormat: getTimeFormat('seconds'),
diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx
index 52d971a551144..4ace78f74ee79 100644
--- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx
@@ -17,6 +17,7 @@ import {
} from '../../../utils/testHelpers';
import { TimeComparison } from './';
import * as urlHelpers from '../../shared/Links/url_helpers';
+import moment from 'moment';
function getWrapper(params?: IUrlParams) {
return ({ children }: { children?: ReactNode }) => {
@@ -31,6 +32,10 @@ function getWrapper(params?: IUrlParams) {
}
describe('TimeComparison', () => {
+ beforeAll(() => {
+ moment.tz.setDefault('Europe/Amsterdam');
+ });
+ afterAll(() => moment.tz.setDefault(''));
const spy = jest.spyOn(urlHelpers, 'replace');
beforeEach(() => {
jest.resetAllMocks();
@@ -40,6 +45,7 @@ describe('TimeComparison', () => {
const Wrapper = getWrapper({
start: '2021-01-28T14:45:00.000Z',
end: '2021-01-28T15:00:00.000Z',
+ rangeTo: 'now',
});
render(, {
wrapper: Wrapper,
@@ -57,6 +63,7 @@ describe('TimeComparison', () => {
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'yesterday',
+ rangeTo: 'now',
});
const component = render(, {
wrapper: Wrapper,
@@ -67,13 +74,64 @@ describe('TimeComparison', () => {
.selectedIndex
).toEqual(0);
});
+
+ it('enables yesterday option when date difference is equal to 24 hours', () => {
+ const Wrapper = getWrapper({
+ start: '2021-01-28T10:00:00.000Z',
+ end: '2021-01-29T10:00:00.000Z',
+ comparisonEnabled: true,
+ comparisonType: 'yesterday',
+ rangeTo: 'now',
+ });
+ const component = render(, {
+ wrapper: Wrapper,
+ });
+ expectTextsInDocument(component, ['Yesterday', 'A week ago']);
+ expect(
+ (component.getByTestId('comparisonSelect') as HTMLSelectElement)
+ .selectedIndex
+ ).toEqual(0);
+ });
+
+ it('selects previous period when rangeTo is different than now', () => {
+ const Wrapper = getWrapper({
+ start: '2021-01-28T10:00:00.000Z',
+ end: '2021-01-29T10:00:00.000Z',
+ comparisonEnabled: true,
+ comparisonType: 'previousPeriod',
+ rangeTo: 'now-15m',
+ });
+ const component = render(, {
+ wrapper: Wrapper,
+ });
+ expectTextsInDocument(component, ['28/01 11:00 - 29/01 11:00']);
+ expect(
+ (component.getByTestId('comparisonSelect') as HTMLSelectElement)
+ .selectedIndex
+ ).toEqual(0);
+ });
});
describe('Time range is between 24 hours - 1 week', () => {
+ it("doesn't show yesterday option when date difference is greater than 24 hours", () => {
+ const Wrapper = getWrapper({
+ start: '2021-01-28T10:00:00.000Z',
+ end: '2021-01-29T11:00:00.000Z',
+ comparisonEnabled: true,
+ comparisonType: 'week',
+ rangeTo: 'now',
+ });
+ const component = render(, {
+ wrapper: Wrapper,
+ });
+ expectTextsNotInDocument(component, ['Yesterday']);
+ expectTextsInDocument(component, ['A week ago']);
+ });
it('sets default values', () => {
const Wrapper = getWrapper({
start: '2021-01-26T15:00:00.000Z',
end: '2021-01-28T15:00:00.000Z',
+ rangeTo: 'now',
});
render(, {
wrapper: Wrapper,
@@ -91,6 +149,7 @@ describe('TimeComparison', () => {
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'week',
+ rangeTo: 'now',
});
const component = render(, {
wrapper: Wrapper,
@@ -102,6 +161,24 @@ describe('TimeComparison', () => {
.selectedIndex
).toEqual(0);
});
+
+ it('selects previous period when rangeTo is different than now', () => {
+ const Wrapper = getWrapper({
+ start: '2021-01-26T15:00:00.000Z',
+ end: '2021-01-28T15:00:00.000Z',
+ comparisonEnabled: true,
+ comparisonType: 'previousPeriod',
+ rangeTo: '2021-01-28T15:00:00.000Z',
+ });
+ const component = render(, {
+ wrapper: Wrapper,
+ });
+ expectTextsInDocument(component, ['26/01 16:00 - 28/01 16:00']);
+ expect(
+ (component.getByTestId('comparisonSelect') as HTMLSelectElement)
+ .selectedIndex
+ ).toEqual(0);
+ });
});
describe('Time range is greater than 7 days', () => {
@@ -111,12 +188,13 @@ describe('TimeComparison', () => {
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'previousPeriod',
+ rangeTo: 'now',
});
const component = render(, {
wrapper: Wrapper,
});
expect(spy).not.toHaveBeenCalled();
- expectTextsInDocument(component, ['20/01 - 28/01']);
+ expectTextsInDocument(component, ['20/01 16:00 - 28/01 16:00']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex
@@ -129,12 +207,13 @@ describe('TimeComparison', () => {
end: '2021-01-28T15:00:00.000Z',
comparisonEnabled: true,
comparisonType: 'previousPeriod',
+ rangeTo: 'now',
});
const component = render(, {
wrapper: Wrapper,
});
expect(spy).not.toHaveBeenCalled();
- expectTextsInDocument(component, ['20/12/20 - 28/01/21']);
+ expectTextsInDocument(component, ['20/12/20 16:00 - 28/01/21 16:00']);
expect(
(component.getByTestId('comparisonSelect') as HTMLSelectElement)
.selectedIndex
diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx
index bb50ca1a45e8c..02064ea786fb0 100644
--- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx
@@ -33,14 +33,21 @@ function formatPreviousPeriodDates({
momentEnd: moment.Moment;
}) {
const isDifferentYears = momentStart.get('year') !== momentEnd.get('year');
- const dateFormat = isDifferentYears ? 'DD/MM/YY' : 'DD/MM';
+ const dateFormat = isDifferentYears ? 'DD/MM/YY HH:mm' : 'DD/MM HH:mm';
return `${momentStart.format(dateFormat)} - ${momentEnd.format(dateFormat)}`;
}
-function getSelectOptions({ start, end }: { start?: string; end?: string }) {
+function getSelectOptions({
+ start,
+ end,
+ rangeTo,
+}: {
+ start?: string;
+ end?: string;
+ rangeTo?: string;
+}) {
const momentStart = moment(start);
const momentEnd = moment(end);
- const dateDiff = getDateDifference(momentStart, momentEnd, 'days');
const yesterdayOption = {
value: 'yesterday',
@@ -56,22 +63,32 @@ function getSelectOptions({ start, end }: { start?: string; end?: string }) {
}),
};
+ const dateDiff = getDateDifference({
+ start: momentStart,
+ end: momentEnd,
+ unitOfTime: 'days',
+ precise: true,
+ });
+ const isRangeToNow = rangeTo === 'now';
+
+ if (isRangeToNow) {
+ // Less than or equals to one day
+ if (dateDiff <= 1) {
+ return [yesterdayOption, aWeekAgoOption];
+ }
+
+ // Less than or equals to one week
+ if (dateDiff <= 7) {
+ return [aWeekAgoOption];
+ }
+ }
+
const prevPeriodOption = {
value: 'previousPeriod',
text: formatPreviousPeriodDates({ momentStart, momentEnd }),
};
- // Less than one day
- if (dateDiff < 1) {
- return [yesterdayOption, aWeekAgoOption];
- }
-
- // Less than one week
- if (dateDiff <= 7) {
- return [aWeekAgoOption];
- }
-
- // above one week
+ // above one week or when rangeTo is not "now"
return [prevPeriodOption];
}
@@ -79,10 +96,10 @@ export function TimeComparison() {
const history = useHistory();
const { isMedium, isLarge } = useBreakPoints();
const {
- urlParams: { start, end, comparisonEnabled, comparisonType },
+ urlParams: { start, end, comparisonEnabled, comparisonType, rangeTo },
} = useUrlParams();
- const selectOptions = getSelectOptions({ start, end });
+ const selectOptions = getSelectOptions({ start, end, rangeTo });
// Sets default values
if (comparisonEnabled === undefined || comparisonType === undefined) {
@@ -113,7 +130,7 @@ export function TimeComparison() {
0}
+ checked={comparisonEnabled}
onChange={() => {
urlHelpers.push(history, {
query: {
From 5052499e2028579f9e4926725f34692c8cf7d366 Mon Sep 17 00:00:00 2001
From: Aaron Caldwell
Date: Thu, 4 Feb 2021 09:47:47 -0700
Subject: [PATCH 07/42] Add readme to geo containment alert covering test alert
setup (#89625)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../alert_types/geo_containment/readme.md | 123 ++++++++++++++++++
1 file changed, 123 insertions(+)
create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md
new file mode 100644
index 0000000000000..798beed8d17bd
--- /dev/null
+++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/readme.md
@@ -0,0 +1,123 @@
+## Instructions for loading & observing data
+
+There are several steps required to set up geo containment alerts for testing in a way
+that allows you to view triggered alerts as they happen. These instructions outline
+how to load test data, but really these steps can be used to load any data for geo
+containment alerts so long as you have the following data:
+- An index containing a`geo_point` field and a `date` field. This data is presumed to
+be dynamic (updated).
+- An index containing `geo_shape` data, such as boundary data, bounding box data, etc.
+This data is presumed to be static (not updated). Shape data matching the query is
+harvested once when the alert is created and anytime after when alert is re-enabled
+after disablement
+The ability for containment alerts to monitor data requires there be somewhat "real time"
+data streaming in as indicated by the `date` field.
+
+### 1. Set experimental flag to enable containment alerts
+- Your `kibana.yml` config file is located in the `config/` dir in the base of your kibana
+project. To edit it, open this file in your editor of choice, add the line described in
+the next step to the bottom of the file (or really anywhere) and save. For more details
+on different config modifications or on how to make production config modifications,
+see [the current docs](https://www.elastic.co/guide/en/kibana/current/settings.html)
+- Set the following configuration settings in your `config/kibana.yml`:
+`xpack.stack_alerts.enableGeoAlerting: true`
+
+### 2. Run ES/Kibana dev env with ssl enabled
+- In two terminals, run the normal commands to launch both elasticsearch and kibana but
+append `--ssl` to the end of each as an arg, i.e.:
+ - `yarn es snapshot --ssl # Runs Elasticsearch`
+ - `yarn start --ssl # Runs Kibana`
+
+### 3. Get an MTA data api key
+- You'll need to obtain an NYC MTA api key, you can request this
+ key [here](https://docs.google.com/forms/d/e/1FAIpQLSfGUZA6h4eHd2-ImaK5Q_I5Gb7C3UEP5vYDALyGd7r3h08YKg/viewform?hl=en&formkey=dG9kcGIxRFpSS0NhQWM4UjA0V0VkNGc6MQ#gid=0)
+
+### 4. Get trackable point data (MTA bus data) into elasticsearch
+- You'll be using the script: `https://github.com/thomasneirynck/mtatracks` to harvest
+live bus data to populate the system. Clone the repo and follow the instructions in
+the readme to set up.
+- Using the MTA key you obtained in the previous step, the final command to run
+in a local terminal should look something like the following. This script loads large
+quantities of data the frequency listed below (20000ms = 20s) or higher:
+`node ./load_tracks.js -a -f 20000`
+
+### 5. Open required Kibana tabs
+There are 3 separate tabs you'll need for a combination of loading and viewing the
+data. Since you'll be jumping between them, it might be easiest to just open them
+upfront. Each is preceded by `https://localhost:5601//app/`:
+- Stack Management > Index Patterns: `management/kibana/indexPatterns`
+- Stack Management > Alerts & Actions: `management/insightsAndAlerting/triggersActions/alerts`
+- Maps: `maps`
+
+### 6 Create map to monitor alerts
+- Go to the Maps app and create a new map
+- Using GeoJSON Upload, upload the GeoJSON file located in the folder of the previously
+cloned `mta_tracks` repo: `nyc-neighborhoods.geo.json`. Accept all of the default
+settings and add the layer.
+- You may want to click your newly added layer and select "Fit to data" so you can see the
+boundaries you've added.
+_ When finished uploading and adding the layer, save the map using a name of your
+choice.
+- Keep the Maps tab open, you'll come back to this
+
+### 7. Create index pattern for generated tracks
+- Go to the index pattern tab to create a new index pattern.
+- Give it the index name `mtatracks*`
+- For `Time field` select `@timestamp`
+- Click `Create index pattern`
+- Leave this tab open, you'll come back to this
+
+### 8. Create containment alert
+- Go to the Alerts tab and click `Create Alert` > `Tracking containment`
+- Fill the side bar form top to bottom. This _should_ flow somewhat logically. In the top
+section, set both `Check every` and `Notify every` to `1 minute`.
+ For `Notify`, leave
+on default selected option `Only on status change`, this will notify only on newly
+contained entities.
+ **Please note that `2 seconds` is an unusually quick interval but done here for demo
+ purposes. With real world data, setting an appropriate interval speed is highly dependent
+ upon the quantity, update frequency and complexity of data handled.**
+- The default settings for `Select Entity` will mostly be correct. Select `mta_tracks*`
+as the index you'd like to track. Use the defaults populated under
+`Select entity` > `INDEX`, update `Select entity` > `BY` to `vehicle_ref`.
+- For `Select boundary` > `INDEX`, select `nyc-neighborhoods` and all populated defaults.
+- Under `Actions`, create an `Server log` action, then create a `Connector` which you can simply name
+`Log test`.
+- For `Run when`, the default `Tracking containment met` will work here. This will track
+only points that are newly contained in the boundaries.
+- Leave the log level at `Info`
+- For the message, use the following sample message or one of your own:
+```
+Entity: {{context.entityId}} with document ID: {{context.entityDocumentId}} has been recorded at location: {{context.entityLocation}} in boundary: {{context.containingBoundaryName}}({{context.containingBoundaryId}}) at {{context.entityDateTime}}. This was detected by the alerting framework at: {{context.detectionDateTime}}.
+```
+- At the bottom right, click `Save`. Your alert should now be created!
+- You should now be able to see alerts generated in your Kibana console log.
+
+### 9. Visually confirm your alerts with Maps
+- Creating layers
+ - Using the source data below, you can create the following layers:
+ - Boundary data (`nyc-neighborhoods`)
+ - Boundary layer
+ - Original tracks data (`mtatracks*`)
+ - Last known location
+ - Geo-line track
+ - Boundary layer
+ - This layer should already be added from when you uploaded the GeoJSON
+ file earlier. If it's not already added, it can be added by selecting `Documents`
+ > `Index patterns` > `nyc-neighborhoods` then accept the defaults and add the layer.
+ - Vehicle tracks
+ - Add `Tracks` > `Index patterns` > `mtatracks*`, accept the defaults selected and set `Entity` > `entity_id`. Add the layer and style appropriately.
+ - Last known location
+ - Add `Documents` > `Index patterns` > `mtatracks*` and select `Show top hits per entity`
+ - For `Entity` select `entity_id` and add the layer.
+ - The only required setting on the following screen is to set `Sorting` to sort on `@timestamp`
+- Update time scope of data
+ - Changing the refresh rate `Refresh every`: `4 seconds` keeps the layers updated and in particular
+ shows the latest values obtained in the `Top hits` layer
+ - The time picker should already be set to the default `15 minutes`, this is a good default but
+ can be adjusted up or down to see more or less data respectively
+- General tips
+ - Style layers with contrasting colors to clearly see each
+ - Consider using icons for the `Top hits` vehicle movement layer
+ - Consider adding tooltips to layers to better understand the data in your layers.
+ - Save your Map anytime you've made any layer adjustments
From 0f45439a5fd882e0503979c84f1a8a333e695a79 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Tue, 8 Dec 2020 02:34:49 +0000
Subject: [PATCH 08/42] skip flaky suite (#85086)
(cherry picked from commit e564439348bbda561bf740a5a62b5638a60ed864)
---
test/api_integration/apis/ui_counters/ui_counters.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts
index 881fa33f5fbc0..c2286f8ea3dce 100644
--- a/test/api_integration/apis/ui_counters/ui_counters.ts
+++ b/test/api_integration/apis/ui_counters/ui_counters.ts
@@ -22,7 +22,8 @@ export default function ({ getService }: FtrProviderContext) {
count,
});
- describe('UI Counters API', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/85086
+ describe.skip('UI Counters API', () => {
const dayDate = moment().format('DDMMYYYY');
it('stores ui counter events in savedObjects', async () => {
From 54b1fb616353a584ea7fe230fa77cbd384b4bfb1 Mon Sep 17 00:00:00 2001
From: Greg Back <1045796+gtback@users.noreply.github.com>
Date: Thu, 4 Feb 2021 13:27:52 -0500
Subject: [PATCH 09/42] Use newfeed.service config for all newsfeeds (#90252)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Alejandro Fernández Haro
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/newsfeed/public/plugin.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx
index 94a113a2786c2..a788b3c4d0b59 100644
--- a/src/plugins/newsfeed/public/plugin.tsx
+++ b/src/plugins/newsfeed/public/plugin.tsx
@@ -51,7 +51,10 @@ export class NewsfeedPublicPlugin
return {
createNewsFeed$: (endpoint: NewsfeedApiEndpoint) => {
const config = Object.assign({}, this.config, {
- service: { pathTemplate: `/${endpoint}/v{VERSION}.json` },
+ service: {
+ ...this.config.service,
+ pathTemplate: `/${endpoint}/v{VERSION}.json`,
+ },
});
return this.fetchNewsfeed(core, config);
},
From 85e92b5ccd3241cbdac551711ed4030b8b0ea55d Mon Sep 17 00:00:00 2001
From: Nathan L Smith
Date: Thu, 4 Feb 2021 13:27:17 -0600
Subject: [PATCH 10/42] Remove UI filters from UI (#89793)
* Start moving some stuff
* Move some stuff around
* more
* Transactions label
* some snake casing
* i18n fix
* Remove unused ui filters endpoints
* Updates to select
* remove projections
* Use urlHelpers.push
* License change
---
x-pack/plugins/apm/common/projections.ts | 17 --
.../apm/public/components/app/Home/index.tsx | 2 +-
.../app/Main/route_config/index.tsx | 2 +-
.../LocalUIFilters/Filter/FilterBadgeList.tsx | 2 +-
.../Filter/FilterTitleButton.tsx | 0
.../LocalUIFilters/Filter/index.tsx | 2 +-
.../RumDashboard}/LocalUIFilters/index.tsx | 8 +-
.../RumDashboard}/hooks/useLocalUIFilters.ts | 25 ++-
.../app/RumDashboard/hooks/use_call_api.ts} | 6 +-
.../components/app/RumDashboard/index.tsx | 7 +-
.../List/List.test.tsx | 0
.../List/__fixtures__/props.json | 0
.../List/__snapshots__/List.test.tsx.snap | 0
.../List/index.tsx | 0
.../index.tsx | 76 +++-----
.../service_details/service_detail_tabs.tsx | 4 +-
.../app/service_inventory/index.tsx | 63 ++----
.../service_inventory.test.tsx | 8 -
.../components/app/service_metrics/index.tsx | 65 +++----
.../index.tsx | 65 ++-----
.../TraceList.tsx | 0
.../index.tsx | 42 ++--
.../Distribution/distribution.test.ts | 0
.../Distribution/index.tsx | 0
.../WaterfallWithSummmary/ErrorCount.test.tsx | 0
.../WaterfallWithSummmary/ErrorCount.tsx | 0
.../MaybeViewTraceLink.tsx | 0
.../WaterfallWithSummmary/PercentOfParent.tsx | 0
.../WaterfallWithSummmary/TransactionTabs.tsx | 0
.../Marks/get_agent_marks.test.ts | 0
.../Marks/get_agent_marks.ts | 0
.../Marks/get_error_marks.test.ts | 0
.../Marks/get_error_marks.ts | 0
.../WaterfallContainer/Marks/index.ts | 0
.../WaterfallContainer/ServiceLegends.tsx | 0
.../Waterfall/FlyoutTopLevelProperties.tsx | 0
.../Waterfall/ResponsiveFlyout.tsx | 0
.../Waterfall/SpanFlyout/DatabaseContext.tsx | 0
.../Waterfall/SpanFlyout/HttpContext.tsx | 0
.../SpanFlyout/StickySpanProperties.tsx | 0
.../SpanFlyout/TruncateHeightSection.tsx | 0
.../Waterfall/SpanFlyout/index.tsx | 0
.../Waterfall/SyncBadge.stories.tsx | 0
.../Waterfall/SyncBadge.tsx | 0
.../TransactionFlyout/DroppedSpansWarning.tsx | 0
.../Waterfall/TransactionFlyout/index.tsx | 0
.../Waterfall/WaterfallFlyout.tsx | 0
.../Waterfall/WaterfallItem.tsx | 0
.../Waterfall/accordion_waterfall.tsx | 0
.../WaterfallContainer/Waterfall/index.tsx | 0
.../waterfall_helpers.test.ts.snap | 0
.../mock_responses/spans.json | 0
.../mock_responses/transaction.json | 0
.../waterfall_helpers.test.ts | 0
.../waterfall_helpers/waterfall_helpers.ts | 0
.../WaterfallContainer.stories.tsx | 0
.../WaterfallContainer/index.tsx | 0
.../waterfallContainer.stories.data.ts | 0
.../WaterfallWithSummmary/index.tsx | 0
.../index.tsx | 121 +++++-------
.../use_waterfall_fetcher.ts | 0
.../app/transaction_overview/index.tsx | 182 +++++++++---------
.../transaction_overview.test.tsx | 4 +-
.../TransactionTypeFilter/index.tsx | 65 -------
.../shared/Summary/DurationSummaryItem.tsx | 2 +-
.../Timeline/Marker/AgentMarker.test.tsx | 2 +-
.../charts/Timeline/Marker/AgentMarker.tsx | 2 +-
.../Timeline/Marker/ErrorMarker.test.tsx | 2 +-
.../charts/Timeline/Marker/ErrorMarker.tsx | 2 +-
.../charts/Timeline/Marker/index.test.tsx | 4 +-
.../shared/charts/Timeline/Marker/index.tsx | 4 +-
.../shared/charts/Timeline/VerticalLines.tsx | 2 +-
.../shared/charts/Timeline/index.tsx | 4 +-
.../public/components/shared/search_bar.tsx | 2 +-
.../shared/transaction_type_select.tsx | 55 ++++++
.../apm/server/routes/create_apm_api.ts | 16 +-
.../plugins/apm/server/routes/ui_filters.ts | 154 +--------------
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
79 files changed, 350 insertions(+), 669 deletions(-)
delete mode 100644 x-pack/plugins/apm/common/projections.ts
rename x-pack/plugins/apm/public/components/{shared => app/RumDashboard}/LocalUIFilters/Filter/FilterBadgeList.tsx (95%)
rename x-pack/plugins/apm/public/components/{shared => app/RumDashboard}/LocalUIFilters/Filter/FilterTitleButton.tsx (100%)
rename x-pack/plugins/apm/public/components/{shared => app/RumDashboard}/LocalUIFilters/Filter/index.tsx (98%)
rename x-pack/plugins/apm/public/components/{shared => app/RumDashboard}/LocalUIFilters/index.tsx (89%)
rename x-pack/plugins/apm/public/{ => components/app/RumDashboard}/hooks/useLocalUIFilters.ts (76%)
rename x-pack/plugins/apm/public/{hooks/useCallApi.ts => components/app/RumDashboard/hooks/use_call_api.ts} (68%)
rename x-pack/plugins/apm/public/components/app/{ErrorGroupOverview => error_group_overview}/List/List.test.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{ErrorGroupOverview => error_group_overview}/List/__fixtures__/props.json (100%)
rename x-pack/plugins/apm/public/components/app/{ErrorGroupOverview => error_group_overview}/List/__snapshots__/List.test.tsx.snap (100%)
rename x-pack/plugins/apm/public/components/app/{ErrorGroupOverview => error_group_overview}/List/index.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{ErrorGroupOverview => error_group_overview}/index.tsx (59%)
rename x-pack/plugins/apm/public/components/app/{ServiceNodeOverview => service_node_overview}/index.tsx (78%)
rename x-pack/plugins/apm/public/components/app/{TraceOverview => trace_overview}/TraceList.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TraceOverview => trace_overview}/index.tsx (66%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/Distribution/distribution.test.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/Distribution/index.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/ErrorCount.test.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/ErrorCount.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/MaybeViewTraceLink.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/PercentOfParent.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/TransactionTabs.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/index.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/WaterfallWithSummmary/index.tsx (100%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/index.tsx (59%)
rename x-pack/plugins/apm/public/components/app/{TransactionDetails => transaction_details}/use_waterfall_fetcher.ts (100%)
delete mode 100644 x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx
create mode 100644 x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx
diff --git a/x-pack/plugins/apm/common/projections.ts b/x-pack/plugins/apm/common/projections.ts
deleted file mode 100644
index dab9dfce5e58a..0000000000000
--- a/x-pack/plugins/apm/common/projections.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-export enum Projection {
- services = 'services',
- transactionGroups = 'transactionGroups',
- traces = 'traces',
- transactions = 'transactions',
- metrics = 'metrics',
- errorGroups = 'errorGroups',
- serviceNodes = 'serviceNodes',
- rumOverview = 'rumOverview',
-}
diff --git a/x-pack/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx
index bb3903727f509..834c2d5c40bce 100644
--- a/x-pack/plugins/apm/public/components/app/Home/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Home/index.tsx
@@ -16,7 +16,7 @@ import { useTraceOverviewHref } from '../../shared/Links/apm/TraceOverviewLink';
import { MainTabs } from '../../shared/main_tabs';
import { ServiceMap } from '../ServiceMap';
import { ServiceInventory } from '../service_inventory';
-import { TraceOverview } from '../TraceOverview';
+import { TraceOverview } from '../trace_overview';
interface Tab {
key: string;
diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
index 0fd85df37bb78..08d95aca24714 100644
--- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx
@@ -23,7 +23,7 @@ import { AnomalyDetection } from '../../Settings/anomaly_detection';
import { ApmIndices } from '../../Settings/ApmIndices';
import { CustomizeUI } from '../../Settings/CustomizeUI';
import { TraceLink } from '../../TraceLink';
-import { TransactionDetails } from '../../TransactionDetails';
+import { TransactionDetails } from '../../transaction_details';
import {
CreateAgentConfigurationRouteHandler,
EditAgentConfigurationRouteHandler,
diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx
similarity index 95%
rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx
rename to x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx
index 6423d295da469..6bc345ea5bd87 100644
--- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
-import { unit, px, truncate } from '../../../../style/variables';
+import { unit, px, truncate } from '../../../../../style/variables';
const BadgeText = styled.div`
display: inline-block;
diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx
rename to x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx
diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx
similarity index 98%
rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx
rename to x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx
index 59ec3b683b4d3..e1debde1117f9 100644
--- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx
@@ -21,7 +21,7 @@ import {
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { FilterBadgeList } from './FilterBadgeList';
-import { unit, px } from '../../../../style/variables';
+import { unit, px } from '../../../../../style/variables';
import { FilterTitleButton } from './FilterTitleButton';
const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs(
diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx
similarity index 89%
rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx
rename to x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx
index 0cab58bc5f448..a07997fb74921 100644
--- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx
@@ -15,12 +15,10 @@ import {
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import { Filter } from './Filter';
-import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters';
-import { Projection } from '../../../../common/projections';
-import { LocalUIFilterName } from '../../../../common/ui_filter';
+import { useLocalUIFilters } from '../hooks/useLocalUIFilters';
+import { LocalUIFilterName } from '../../../../../common/ui_filter';
interface Props {
- projection: Projection;
filterNames: LocalUIFilterName[];
params?: Record;
showCount?: boolean;
@@ -33,7 +31,6 @@ const ButtonWrapper = styled.div`
`;
function LocalUIFilters({
- projection,
params,
filterNames,
children,
@@ -42,7 +39,6 @@ function LocalUIFilters({
}: Props) {
const { filters, setFilterValue, clearValues } = useLocalUIFilters({
filterNames,
- projection,
params,
shouldFetch,
});
diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts
similarity index 76%
rename from x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts
rename to x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts
index 1e0aa4fd96171..3f366300792ac 100644
--- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts
@@ -7,19 +7,21 @@
import { omit } from 'lodash';
import { useHistory } from 'react-router-dom';
-import { Projection } from '../../common/projections';
-import { pickKeys } from '../../common/utils/pick_keys';
+import { LocalUIFilterName } from '../../../../../common/ui_filter';
+import { pickKeys } from '../../../../../common/utils/pick_keys';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters';
+import { LocalUIFiltersAPIResponse } from '../../../../../server/lib/ui_filters/local_ui_filters';
import {
localUIFilters,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-} from '../../server/lib/ui_filters/local_ui_filters/config';
-import { fromQuery, toQuery } from '../components/shared/Links/url_helpers';
-import { removeUndefinedProps } from '../context/url_params_context/helpers';
-import { useFetcher } from './use_fetcher';
-import { useUrlParams } from '../context/url_params_context/use_url_params';
-import { LocalUIFilterName } from '../../common/ui_filter';
+} from '../../../../../server/lib/ui_filters/local_ui_filters/config';
+import {
+ fromQuery,
+ toQuery,
+} from '../../../../components/shared/Links/url_helpers';
+import { removeUndefinedProps } from '../../../../context/url_params_context/helpers';
+import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
+import { useFetcher } from '../../../../hooks/use_fetcher';
const getInitialData = (
filterNames: LocalUIFilterName[]
@@ -31,12 +33,10 @@ const getInitialData = (
};
export function useLocalUIFilters({
- projection,
filterNames,
params,
shouldFetch,
}: {
- projection: Projection;
filterNames: LocalUIFilterName[];
params?: Record;
shouldFetch: boolean;
@@ -72,7 +72,7 @@ export function useLocalUIFilters({
(callApmApi) => {
if (shouldFetch && urlParams.start && urlParams.end) {
return callApmApi({
- endpoint: `GET /api/apm/ui_filters/local_filters/${projection}` as const,
+ endpoint: `GET /api/apm/ui_filters/local_filters/rumOverview`,
params: {
query: {
uiFilters: JSON.stringify(uiFilters),
@@ -87,7 +87,6 @@ export function useLocalUIFilters({
}
},
[
- projection,
uiFilters,
urlParams.start,
urlParams.end,
diff --git a/x-pack/plugins/apm/public/hooks/useCallApi.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts
similarity index 68%
rename from x-pack/plugins/apm/public/hooks/useCallApi.ts
rename to x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts
index a2bb77c6ad6fc..5b448871804eb 100644
--- a/x-pack/plugins/apm/public/hooks/useCallApi.ts
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/use_call_api.ts
@@ -6,9 +6,9 @@
*/
import { useMemo } from 'react';
-import { callApi } from '../services/rest/callApi';
-import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
-import { FetchOptions } from '../../common/fetch_options';
+import { callApi } from '../../../../services/rest/callApi';
+import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
+import { FetchOptions } from '../../../../../common/fetch_options';
export function useCallApi() {
const { http } = useApmPluginContext().core;
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx
index 7b0b1d204ac4d..9bdad14eb8a18 100644
--- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx
@@ -5,13 +5,11 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import React, { useMemo } from 'react';
import { useTrackPageview } from '../../../../../observability/public';
-import { Projection } from '../../../../common/projections';
+import { LocalUIFilters } from './LocalUIFilters';
import { RumDashboard } from './RumDashboard';
-
-import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { URLFilter } from './URLFilter';
export function RumOverview() {
@@ -21,7 +19,6 @@ export function RumOverview() {
const localUIFiltersConfig = useMemo(() => {
const config: React.ComponentProps = {
filterNames: ['location', 'device', 'os', 'browser'],
- projection: Projection.rumOverview,
};
return config;
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/List.test.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/List.test.tsx
rename to x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__fixtures__/props.json b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__fixtures__/props.json
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__fixtures__/props.json
rename to x-pack/plugins/apm/public/components/app/error_group_overview/List/__fixtures__/props.json
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__snapshots__/List.test.tsx.snap
rename to x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
rename to x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
similarity index 59%
rename from x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx
rename to x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
index 58fea5e985fae..29bdf6467e544 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx
@@ -7,29 +7,26 @@
import {
EuiFlexGroup,
- EuiFlexItem,
EuiPage,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React, { useMemo } from 'react';
+import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
-import { Projection } from '../../../../common/projections';
-import { useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
-import { LocalUIFilters } from '../../shared/LocalUIFilters';
+import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
+import { useFetcher } from '../../../hooks/use_fetcher';
import { SearchBar } from '../../shared/search_bar';
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
import { ErrorGroupList } from './List';
-import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher';
interface ErrorGroupOverviewProps {
serviceName: string;
}
-function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
+export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
const { urlParams, uiFilters } = useUrlParams();
const { start, end, sortField, sortDirection } = urlParams;
const { errorDistributionData } = useErrorGroupDistributionFetcher({
@@ -68,18 +65,6 @@ function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
});
useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 });
- const localUIFiltersConfig = useMemo(() => {
- const config: React.ComponentProps = {
- filterNames: ['host', 'containerId', 'podName', 'serviceVersion'],
- params: {
- serviceName,
- },
- projection: Projection.errorGroups,
- };
-
- return config;
- }, [serviceName]);
-
if (!errorDistributionData || !errorGroupListData) {
return null;
}
@@ -88,41 +73,34 @@ function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) {
<>
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
-
- Errors
-
-
+
+
+ Errors
+
+
-
-
-
+
+
>
);
}
-
-export { ErrorGroupOverview };
diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx
index 1c8a33d1968b1..23f699b63d207 100644
--- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx
@@ -20,9 +20,9 @@ import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOv
import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link';
import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link';
import { MainTabs } from '../../shared/main_tabs';
-import { ErrorGroupOverview } from '../ErrorGroupOverview';
+import { ErrorGroupOverview } from '../error_group_overview';
import { ServiceMap } from '../ServiceMap';
-import { ServiceNodeOverview } from '../ServiceNodeOverview';
+import { ServiceNodeOverview } from '../service_node_overview';
import { ServiceMetrics } from '../service_metrics';
import { ServiceOverview } from '../service_overview';
import { TransactionOverview } from '../transaction_overview';
diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx
index 4ba96b63c91f4..1cb420a8ac194 100644
--- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx
@@ -13,21 +13,19 @@ import {
EuiPanel,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React, { useEffect, useMemo } from 'react';
+import React, { useEffect } from 'react';
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
import { useTrackPageview } from '../../../../../observability/public';
-import { Projection } from '../../../../common/projections';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
-import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
-import { useLocalStorage } from '../../../hooks/useLocalStorage';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
-import { LocalUIFilters } from '../../shared/LocalUIFilters';
+import { useLocalStorage } from '../../../hooks/useLocalStorage';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
+import { useUpgradeAssistantHref } from '../../shared/Links/kibana';
import { SearchBar } from '../../shared/search_bar';
import { NoServicesMessage } from './no_services_message';
import { ServiceList } from './ServiceList';
import { MLCallout } from './ServiceList/MLCallout';
import { useAnomalyDetectionJobsFetcher } from './use_anomaly_detection_jobs_fetcher';
-import { useUpgradeAssistantHref } from '../../shared/Links/kibana';
const initialData = {
items: [],
@@ -100,16 +98,6 @@ export function ServiceInventory() {
useTrackPageview({ app: 'apm', path: 'services_overview' });
useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 });
- const localFiltersConfig: React.ComponentProps<
- typeof LocalUIFilters
- > = useMemo(
- () => ({
- filterNames: ['host', 'agentName'],
- projection: Projection.services,
- }),
- []
- );
-
const {
anomalyDetectionJobsData,
anomalyDetectionJobsStatus,
@@ -132,33 +120,24 @@ export function ServiceInventory() {
<>
-
-
-
-
-
-
- {displayMlCallout ? (
-
- setUserHasDismissedCallout(true)}
+
+ {displayMlCallout ? (
+
+ setUserHasDismissedCallout(true)} />
+
+ ) : null}
+
+
+
-
- ) : null}
-
-
-
- }
- />
-
-
-
+ }
+ />
+
diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx
index 647792bb13046..69b4149625824 100644
--- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx
@@ -20,7 +20,6 @@ import {
MockApmPluginContextWrapper,
} from '../../../context/apm_plugin/mock_apm_plugin_context';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
-import * as useLocalUIFilters from '../../../hooks/useLocalUIFilters';
import * as useDynamicIndexPatternHooks from '../../../hooks/use_dynamic_index_pattern';
import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock';
import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider';
@@ -76,13 +75,6 @@ describe('ServiceInventory', () => {
// @ts-expect-error
global.sessionStorage = new SessionStorageMock();
- jest.spyOn(useLocalUIFilters, 'useLocalUIFilters').mockReturnValue({
- filters: [],
- setFilterValue: () => null,
- clearValues: () => null,
- status: FETCH_STATUS.SUCCESS,
- });
-
jest.spyOn(hook, 'useAnomalyDetectionJobsFetcher').mockReturnValue({
anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS,
anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false },
diff --git a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx
index d1e6cc0d84ac4..44a5adf31d0b6 100644
--- a/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_metrics/index.tsx
@@ -7,19 +7,17 @@
import {
EuiFlexGrid,
+ EuiFlexGroup,
EuiFlexItem,
EuiPage,
EuiPanel,
EuiSpacer,
- EuiFlexGroup,
} from '@elastic/eui';
-import React, { useMemo } from 'react';
+import React from 'react';
+import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
+import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { useServiceMetricChartsFetcher } from '../../../hooks/use_service_metric_charts_fetcher';
import { MetricsChart } from '../../shared/charts/metrics_chart';
-import { useUrlParams } from '../../../context/url_params_context/use_url_params';
-import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
-import { Projection } from '../../../../common/projections';
-import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { SearchBar } from '../../shared/search_bar';
interface ServiceMetricsProps {
@@ -37,47 +35,28 @@ export function ServiceMetrics({
});
const { start, end } = urlParams;
- const localFiltersConfig: React.ComponentProps<
- typeof LocalUIFilters
- > = useMemo(
- () => ({
- filterNames: ['host', 'containerId', 'podName', 'serviceVersion'],
- params: {
- serviceName,
- },
- projection: Projection.metrics,
- showCount: false,
- }),
- [serviceName]
- );
-
return (
<>
-
-
-
-
-
-
-
- {data.charts.map((chart) => (
-
-
-
-
-
- ))}
-
-
-
-
+
+
+
+ {data.charts.map((chart) => (
+
+
+
+
+
+ ))}
+
+
+
>
diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx
similarity index 78%
rename from x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
rename to x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx
index 01874c956e8f9..00d184f692e3b 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx
@@ -4,30 +4,21 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiPage,
- EuiPanel,
- EuiToolTip,
-} from '@elastic/eui';
+import { EuiFlexGroup, EuiPage, EuiPanel, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React, { useMemo } from 'react';
+import React from 'react';
import styled from 'styled-components';
import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n';
-import { Projection } from '../../../../common/projections';
import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
import {
asDynamicBytes,
asInteger,
asPercent,
} from '../../../../common/utils/formatters';
-import { useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
+import { useFetcher } from '../../../hooks/use_fetcher';
import { px, truncate, unit } from '../../../style/variables';
import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink';
-import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { ITableColumn, ManagedTable } from '../../shared/ManagedTable';
import { SearchBar } from '../../shared/search_bar';
@@ -47,19 +38,6 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) {
const { uiFilters, urlParams } = useUrlParams();
const { start, end } = urlParams;
- const localFiltersConfig: React.ComponentProps<
- typeof LocalUIFilters
- > = useMemo(
- () => ({
- filterNames: ['host', 'containerId', 'podName'],
- params: {
- serviceName,
- },
- projection: Projection.serviceNodes,
- }),
- [serviceName]
- );
-
const { data: items = [] } = useFetcher(
(callApmApi) => {
if (!start || !end) {
@@ -164,27 +142,22 @@ function ServiceNodeOverview({ serviceName }: ServiceNodeOverviewProps) {
<>
-
-
-
-
-
-
-
-
-
+
+
+
+
>
diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx
rename to x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx
similarity index 66%
rename from x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx
rename to x-pack/plugins/apm/public/components/app/trace_overview/index.tsx
index 624aee1e92472..d29dad7a7e3de 100644
--- a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx
@@ -6,16 +6,14 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui';
-import React, { useMemo } from 'react';
+import React from 'react';
import { useTrackPageview } from '../../../../../observability/public';
-import { Projection } from '../../../../common/projections';
-import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
+import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import { APIReturnType } from '../../../services/rest/createCallApmApi';
-import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { SearchBar } from '../../shared/search_bar';
-import { TraceList } from './TraceList';
import { Correlations } from '../Correlations';
+import { TraceList } from './TraceList';
type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>;
const DEFAULT_RESPONSE: TracesAPIResponse = {
@@ -48,32 +46,22 @@ export function TraceOverview() {
useTrackPageview({ app: 'apm', path: 'traces_overview' });
useTrackPageview({ app: 'apm', path: 'traces_overview', delay: 15000 });
- const localUIFiltersConfig = useMemo(() => {
- const config: React.ComponentProps = {
- filterNames: ['transactionResult', 'host', 'containerId', 'podName'],
- projection: Projection.traces,
- };
-
- return config;
- }, []);
-
return (
<>
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
>
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/distribution.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/distribution.test.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/Distribution/distribution.test.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/Distribution/index.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/ErrorCount.test.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.test.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/ErrorCount.test.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/ErrorCount.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/ErrorCount.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/MaybeViewTraceLink.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/PercentOfParent.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/PercentOfParent.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/TransactionTabs.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/TransactionTabs.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.test.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.test.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/index.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/index.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/index.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/index.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx
similarity index 59%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
rename to x-pack/plugins/apm/public/components/app/transaction_details/index.tsx
index b155672405b9f..d5f5eed311de8 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx
@@ -14,26 +14,23 @@ import {
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
-import React, { useMemo } from 'react';
-import { isEmpty, flatten } from 'lodash';
-import { useHistory } from 'react-router-dom';
-import { RouteComponentProps } from 'react-router-dom';
+import { flatten, isEmpty } from 'lodash';
+import React from 'react';
+import { RouteComponentProps, useHistory } from 'react-router-dom';
+import { useTrackPageview } from '../../../../../observability/public';
+import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
+import { useUrlParams } from '../../../context/url_params_context/use_url_params';
+import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useTransactionDistributionFetcher } from '../../../hooks/use_transaction_distribution_fetcher';
-import { useWaterfallFetcher } from './use_waterfall_fetcher';
import { ApmHeader } from '../../shared/ApmHeader';
import { TransactionCharts } from '../../shared/charts/transaction_charts';
-import { TransactionDistribution } from './Distribution';
-import { WaterfallWithSummmary } from './WaterfallWithSummmary';
-import { FETCH_STATUS } from '../../../hooks/use_fetcher';
-import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
-import { useTrackPageview } from '../../../../../observability/public';
-import { Projection } from '../../../../common/projections';
-import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
-import { useUrlParams } from '../../../context/url_params_context/use_url_params';
-import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { HeightRetainer } from '../../shared/HeightRetainer';
-import { Correlations } from '../Correlations';
+import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
import { SearchBar } from '../../shared/search_bar';
+import { Correlations } from '../Correlations';
+import { TransactionDistribution } from './Distribution';
+import { useWaterfallFetcher } from './use_waterfall_fetcher';
+import { WaterfallWithSummmary } from './WaterfallWithSummmary';
interface Sample {
traceId: string;
@@ -46,7 +43,6 @@ export function TransactionDetails({
location,
match,
}: TransactionDetailsProps) {
- const { serviceName } = match.params;
const { urlParams } = useUrlParams();
const history = useHistory();
const {
@@ -59,24 +55,11 @@ export function TransactionDetails({
exceedsMax,
status: waterfallStatus,
} = useWaterfallFetcher();
- const { transactionName, transactionType } = urlParams;
+ const { transactionName } = urlParams;
useTrackPageview({ app: 'apm', path: 'transaction_details' });
useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 });
- const localUIFiltersConfig = useMemo(() => {
- const config: React.ComponentProps = {
- filterNames: ['transactionResult', 'serviceVersion'],
- projection: Projection.transactions,
- params: {
- transactionName,
- transactionType,
- serviceName,
- },
- };
- return config;
- }, [transactionName, transactionType, serviceName]);
-
const selectedSample = flatten(
distributionData.buckets.map((bucket) => bucket.samples)
).find(
@@ -116,45 +99,45 @@ export function TransactionDetails({
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- if (!isEmpty(bucket.samples)) {
- selectSampleFromBucketClick(bucket.samples[0]);
- }
- }}
- />
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (!isEmpty(bucket.samples)) {
+ selectSampleFromBucketClick(bucket.samples[0]);
+ }
+ }}
+ />
+
+
+
+
+
+
+
>
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts b/x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts
similarity index 100%
rename from x-pack/plugins/apm/public/components/app/TransactionDetails/use_waterfall_fetcher.ts
rename to x-pack/plugins/apm/public/components/app/transaction_details/use_waterfall_fetcher.ts
diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx
index 08904da396678..1f8b431d072b7 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx
@@ -10,7 +10,6 @@ import {
EuiCode,
EuiFlexGroup,
EuiFlexItem,
- EuiHorizontalRule,
EuiPage,
EuiPanel,
EuiSpacer,
@@ -19,25 +18,23 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { Location } from 'history';
-import React, { useMemo } from 'react';
+import React from 'react';
import { useLocation } from 'react-router-dom';
import { useTrackPageview } from '../../../../../observability/public';
-import { Projection } from '../../../../common/projections';
import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types';
+import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { IUrlParams } from '../../../context/url_params_context/types';
-import { useTransactionListFetcher } from './use_transaction_list';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { TransactionCharts } from '../../shared/charts/transaction_charts';
import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
import { fromQuery, toQuery } from '../../shared/Links/url_helpers';
-import { LocalUIFilters } from '../../shared/LocalUIFilters';
-import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter';
import { SearchBar } from '../../shared/search_bar';
+import { TransactionTypeSelect } from '../../shared/transaction_type_select';
import { Correlations } from '../Correlations';
import { TransactionList } from './TransactionList';
import { useRedirect } from './useRedirect';
import { UserExperienceCallout } from './user_experience_callout';
-import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
+import { useTransactionListFetcher } from './use_transaction_list';
function getRedirectLocation({
location,
@@ -68,7 +65,7 @@ interface TransactionOverviewProps {
export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
const location = useLocation();
const { urlParams } = useUrlParams();
- const { transactionType, transactionTypes } = useApmServiceContext();
+ const { transactionType } = useApmServiceContext();
// redirect to first transaction type
useRedirect(getRedirectLocation({ location, transactionType, urlParams }));
@@ -80,27 +77,6 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
transactionListStatus,
} = useTransactionListFetcher();
- const localFiltersConfig: React.ComponentProps<
- typeof LocalUIFilters
- > = useMemo(
- () => ({
- shouldFetch: !!transactionType,
- filterNames: [
- 'transactionResult',
- 'host',
- 'containerId',
- 'podName',
- 'serviceVersion',
- ],
- params: {
- serviceName,
- transactionType,
- },
- projection: Projection.transactionGroups,
- }),
- [serviceName, transactionType]
- );
-
// TODO: improve urlParams typings.
// `serviceName` or `transactionType` will never be undefined here, and this check should not be needed
if (!serviceName) {
@@ -112,74 +88,92 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {i18n.translate('xpack.apm.transactionOverviewTitle', {
+ defaultMessage: 'Transactions',
+ })}
+
+
+
+
+
+
+
-
-
-
-
- {transactionType === TRANSACTION_PAGE_LOAD && (
- <>
-
-
- >
- )}
-
-
-
-
- Transactions
-
-
- {!transactionListData.isAggregationAccurate && (
-
-
-
- xpack.apm.ui.transactionGroupBucketSize
-
- ),
- }}
- />
+
+
+
+
+
-
- {i18n.translate(
- 'xpack.apm.transactionCardinalityWarning.docsLink',
- { defaultMessage: 'Learn more in the docs' }
- )}
-
-
-
- )}
+ {transactionType === TRANSACTION_PAGE_LOAD && (
+ <>
+
-
-
-
+ >
+ )}
+
+
+
+
+ Transactions
+
+
+ {!transactionListData.isAggregationAccurate && (
+
+
+
+ xpack.apm.ui.transactionGroupBucketSize
+
+ ),
+ }}
+ />
+
+
+ {i18n.translate(
+ 'xpack.apm.transactionCardinalityWarning.docsLink',
+ { defaultMessage: 'Learn more in the docs' }
+ )}
+
+
+
+ )}
+
+
+
>
diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx
index e0b1a4cbd05d5..7d0ada3e31bff 100644
--- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx
@@ -136,7 +136,9 @@ describe('TransactionOverview', () => {
expect(getByText(container, 'firstType')).toBeInTheDocument();
expect(getByText(container, 'secondType')).toBeInTheDocument();
- fireEvent.click(getByText(container, 'firstType'));
+ fireEvent.change(getByText(container, 'firstType').parentElement!, {
+ target: { value: 'firstType' },
+ });
expect(history.push).toHaveBeenCalled();
expect(history.location.search).toEqual(
diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx
deleted file mode 100644
index 19eefca5ee27e..0000000000000
--- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import {
- EuiHorizontalRule,
- EuiRadioGroup,
- EuiSpacer,
- EuiTitle,
-} from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import React from 'react';
-import { useHistory } from 'react-router-dom';
-import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
-import { fromQuery, toQuery } from '../../Links/url_helpers';
-
-interface Props {
- transactionTypes: string[];
-}
-
-function TransactionTypeFilter({ transactionTypes }: Props) {
- const history = useHistory();
- const {
- urlParams: { transactionType },
- } = useUrlParams();
-
- const options = transactionTypes.map((type) => ({
- id: type,
- label: type,
- }));
-
- return (
- <>
-
-
- {i18n.translate('xpack.apm.localFilters.titles.transactionType', {
- defaultMessage: 'Transaction type',
- })}
-
-
-
-
-
- {
- const newLocation = {
- ...history.location,
- search: fromQuery({
- ...toQuery(history.location.search),
- transactionType: selectedTransactionType,
- }),
- };
- history.push(newLocation);
- }}
- />
- >
- );
-}
-
-export { TransactionTypeFilter };
diff --git a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx
index 94fc79dd2164e..1ceccc5203fb2 100644
--- a/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx
@@ -9,7 +9,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip, EuiText } from '@elastic/eui';
import { asDuration } from '../../../../common/utils/formatters';
-import { PercentOfParent } from '../../app/TransactionDetails/WaterfallWithSummmary/PercentOfParent';
+import { PercentOfParent } from '../../app/transaction_details/WaterfallWithSummmary/PercentOfParent';
interface Props {
duration: number;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx
index 28a581d09908e..1411a264b065e 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.test.tsx
@@ -8,7 +8,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import { AgentMarker } from './AgentMarker';
-import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
+import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common';
describe('AgentMarker', () => {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx
index f669063f07545..ad8b85ba70c9b 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx
@@ -12,7 +12,7 @@ import { asDuration } from '../../../../../../common/utils/formatters';
import { useTheme } from '../../../../../hooks/use_theme';
import { px, units } from '../../../../../style/variables';
import { Legend } from '../../Legend';
-import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
+import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
const NameContainer = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade};
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx
index 29e553235e57b..36634f97a3a45 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx
@@ -14,7 +14,7 @@ import {
expectTextsInDocument,
renderWithTheme,
} from '../../../../../utils/testHelpers';
-import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
+import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
import { ErrorMarker } from './ErrorMarker';
function Wrapper({ children }: { children?: ReactNode }) {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx
index c38cc07955996..393281b2bf848 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx
@@ -16,7 +16,7 @@ import {
} from '../../../../../../common/elasticsearch_fieldnames';
import { useUrlParams } from '../../../../../context/url_params_context/use_url_params';
import { px, unit, units } from '../../../../../style/variables';
-import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
+import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
import { ErrorDetailLink } from '../../../Links/apm/ErrorDetailLink';
import { Legend, Shape } from '../../Legend';
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx
index 16ded0b2402c4..f156d82f05a51 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.test.tsx
@@ -8,8 +8,8 @@
import { shallow } from 'enzyme';
import React from 'react';
import { Marker } from './';
-import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
-import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
+import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
+import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
describe('Marker', () => {
it('renders agent marker', () => {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx
index 14688fe7e0c61..b426a10a7562d 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx
@@ -10,8 +10,8 @@ import styled from 'styled-components';
import { px } from '../../../../../style/variables';
import { AgentMarker } from './AgentMarker';
import { ErrorMarker } from './ErrorMarker';
-import { AgentMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
-import { ErrorMark } from '../../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
+import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
+import { ErrorMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
interface Props {
mark: ErrorMark | AgentMark;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx
index 218bdde37abd0..428da80fb808a 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { VerticalGridLines, XYPlot } from 'react-vis';
import { useTheme } from '../../../../hooks/use_theme';
-import { Mark } from '../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks';
+import { Mark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks';
import { PlotValues } from './plotUtils';
interface VerticalLinesProps {
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx
index 84bdd7998cfad..650faa195271c 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx
@@ -11,8 +11,8 @@ import { makeWidthFlexible } from 'react-vis';
import { getPlotValues } from './plotUtils';
import { TimelineAxis } from './TimelineAxis';
import { VerticalLines } from './VerticalLines';
-import { ErrorMark } from '../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
-import { AgentMark } from '../../../app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
+import { ErrorMark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks';
+import { AgentMark } from '../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks';
export type Mark = AgentMark | ErrorMark;
diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx
index 296ec3c2d32e9..34ba1d86264c1 100644
--- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx
+++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx
@@ -16,7 +16,7 @@ import { useBreakPoints } from '../../hooks/use_break_points';
const SearchBarFlexGroup = styled(EuiFlexGroup)`
margin: ${({ theme }) =>
- `${theme.eui.euiSizeM} ${theme.eui.euiSizeM} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeM}`};
+ `${theme.eui.euiSizeS} ${theme.eui.euiSizeS} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeS}`};
`;
interface Props {
diff --git a/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx b/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx
new file mode 100644
index 0000000000000..772b42ed13577
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/transaction_type_select.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiSelect } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React, { FormEvent, useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+import styled from 'styled-components';
+import { useApmServiceContext } from '../../context/apm_service/use_apm_service_context';
+import { useUrlParams } from '../../context/url_params_context/use_url_params';
+import * as urlHelpers from './Links/url_helpers';
+
+// The default transaction type (for non-RUM services) is "request". Set the
+// min-width on here to the width when "request" is loaded so it doesn't start
+// out collapsed and change its width when the list of transaction types is loaded.
+const EuiSelectWithWidth = styled(EuiSelect)`
+ min-width: 157px;
+`;
+
+export function TransactionTypeSelect() {
+ const { transactionTypes } = useApmServiceContext();
+ const history = useHistory();
+ const {
+ urlParams: { transactionType },
+ } = useUrlParams();
+
+ const handleChange = useCallback(
+ (event: FormEvent) => {
+ const selectedTransactionType = event.currentTarget.value;
+ urlHelpers.push(history, {
+ query: { transactionType: selectedTransactionType },
+ });
+ },
+ [history]
+ );
+
+ const options = transactionTypes.map((t) => ({ text: t, value: t }));
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts
index 610442d4ff614..5d580fc0e253a 100644
--- a/x-pack/plugins/apm/server/routes/create_apm_api.ts
+++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts
@@ -66,15 +66,8 @@ import {
transactionThroughputChatsRoute,
} from './transactions';
import {
- errorGroupsLocalFiltersRoute,
- metricsLocalFiltersRoute,
- servicesLocalFiltersRoute,
- tracesLocalFiltersRoute,
- transactionGroupsLocalFiltersRoute,
- transactionsLocalFiltersRoute,
- serviceNodesLocalFiltersRoute,
- uiFiltersEnvironmentsRoute,
rumOverviewLocalFiltersRoute,
+ uiFiltersEnvironmentsRoute,
} from './ui_filters';
import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map';
import {
@@ -176,13 +169,6 @@ const createApmApi = () => {
.add(transactionThroughputChatsRoute)
// UI filters
- .add(errorGroupsLocalFiltersRoute)
- .add(metricsLocalFiltersRoute)
- .add(servicesLocalFiltersRoute)
- .add(tracesLocalFiltersRoute)
- .add(transactionGroupsLocalFiltersRoute)
- .add(transactionsLocalFiltersRoute)
- .add(serviceNodesLocalFiltersRoute)
.add(uiFiltersEnvironmentsRoute)
// Service map
diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts
index 9cedbf16e161b..b14a47e302caa 100644
--- a/x-pack/plugins/apm/server/routes/ui_filters.ts
+++ b/x-pack/plugins/apm/server/routes/ui_filters.ts
@@ -7,29 +7,23 @@
import * as t from 'io-ts';
import { omit } from 'lodash';
+import { jsonRt } from '../../common/runtime_types/json_rt';
+import { LocalUIFilterName } from '../../common/ui_filter';
+import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
+import { getEsFilter } from '../lib/helpers/convert_ui_filters/get_es_filter';
import {
- setupRequest,
Setup,
+ setupRequest,
SetupTimeRange,
} from '../lib/helpers/setup_request';
import { getEnvironments } from '../lib/ui_filters/get_environments';
-import { Projection } from '../projections/typings';
-import { localUIFilterNames } from '../lib/ui_filters/local_ui_filters/config';
-import { getEsFilter } from '../lib/helpers/convert_ui_filters/get_es_filter';
import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters';
-import { getServicesProjection } from '../projections/services';
-import { getTransactionGroupsProjection } from '../projections/transaction_groups';
-import { getMetricsProjection } from '../projections/metrics';
-import { getErrorGroupsProjection } from '../projections/errors';
-import { getTransactionsProjection } from '../projections/transactions';
-import { createRoute } from './create_route';
-import { uiFiltersRt, rangeRt } from './default_api_types';
-import { jsonRt } from '../../common/runtime_types/json_rt';
-import { getServiceNodesProjection } from '../projections/service_nodes';
+import { localUIFilterNames } from '../lib/ui_filters/local_ui_filters/config';
import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions';
-import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
+import { Projection } from '../projections/typings';
+import { createRoute } from './create_route';
+import { rangeRt, uiFiltersRt } from './default_api_types';
import { APMRequestHandlerContext } from './typings';
-import { LocalUIFilterName } from '../../common/ui_filter';
export const uiFiltersEnvironmentsRoute = createRoute({
endpoint: 'GET /api/apm/ui_filters/environments',
@@ -122,136 +116,6 @@ function createLocalFiltersRoute<
});
}
-export const servicesLocalFiltersRoute = createLocalFiltersRoute({
- endpoint: `GET /api/apm/ui_filters/local_filters/services`,
- getProjection: async ({ context, setup }) => {
- const searchAggregatedTransactions = await getSearchAggregatedTransactions(
- setup
- );
-
- return getServicesProjection({ setup, searchAggregatedTransactions });
- },
- queryRt: t.type({}),
-});
-
-export const transactionGroupsLocalFiltersRoute = createLocalFiltersRoute({
- endpoint: 'GET /api/apm/ui_filters/local_filters/transactionGroups',
- getProjection: async ({ context, setup, query }) => {
- const { transactionType, serviceName, transactionName } = query;
-
- const searchAggregatedTransactions = await getSearchAggregatedTransactions(
- setup
- );
-
- return getTransactionGroupsProjection({
- setup,
- options: {
- type: 'top_transactions',
- transactionType,
- serviceName,
- transactionName,
- searchAggregatedTransactions,
- },
- });
- },
- queryRt: t.intersection([
- t.type({
- serviceName: t.string,
- transactionType: t.string,
- }),
- t.partial({
- transactionName: t.string,
- }),
- ]),
-});
-
-export const tracesLocalFiltersRoute = createLocalFiltersRoute({
- endpoint: 'GET /api/apm/ui_filters/local_filters/traces',
- getProjection: async ({ setup, context }) => {
- const searchAggregatedTransactions = await getSearchAggregatedTransactions(
- setup
- );
-
- return getTransactionGroupsProjection({
- setup,
- options: { type: 'top_traces', searchAggregatedTransactions },
- });
- },
- queryRt: t.type({}),
-});
-
-export const transactionsLocalFiltersRoute = createLocalFiltersRoute({
- endpoint: 'GET /api/apm/ui_filters/local_filters/transactions',
- getProjection: async ({ context, setup, query }) => {
- const { transactionType, serviceName, transactionName } = query;
-
- const searchAggregatedTransactions = await getSearchAggregatedTransactions(
- setup
- );
-
- return getTransactionsProjection({
- setup,
- transactionType,
- serviceName,
- transactionName,
- searchAggregatedTransactions,
- });
- },
- queryRt: t.type({
- transactionType: t.string,
- transactionName: t.string,
- serviceName: t.string,
- }),
-});
-
-export const metricsLocalFiltersRoute = createLocalFiltersRoute({
- endpoint: 'GET /api/apm/ui_filters/local_filters/metrics',
- getProjection: ({ setup, query }) => {
- const { serviceName, serviceNodeName } = query;
- return getMetricsProjection({
- setup,
- serviceName,
- serviceNodeName,
- });
- },
- queryRt: t.intersection([
- t.type({
- serviceName: t.string,
- }),
- t.partial({
- serviceNodeName: t.string,
- }),
- ]),
-});
-
-export const errorGroupsLocalFiltersRoute = createLocalFiltersRoute({
- endpoint: 'GET /api/apm/ui_filters/local_filters/errorGroups',
- getProjection: ({ setup, query }) => {
- const { serviceName } = query;
- return getErrorGroupsProjection({
- setup,
- serviceName,
- });
- },
- queryRt: t.type({
- serviceName: t.string,
- }),
-});
-
-export const serviceNodesLocalFiltersRoute = createLocalFiltersRoute({
- endpoint: 'GET /api/apm/ui_filters/local_filters/serviceNodes',
- getProjection: ({ setup, query }) => {
- const { serviceName } = query;
- return getServiceNodesProjection({
- setup,
- serviceName,
- });
- },
- queryRt: t.type({
- serviceName: t.string,
- }),
-});
-
export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({
endpoint: 'GET /api/apm/ui_filters/local_filters/rumOverview',
getProjection: async ({ setup }) => {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index cab6973072f24..b472655bf9028 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -5104,7 +5104,6 @@
"xpack.apm.localFilters.titles.serviceName": "サービス名",
"xpack.apm.localFilters.titles.serviceVersion": "サービスバージョン",
"xpack.apm.localFilters.titles.transactionResult": "トランザクション結果",
- "xpack.apm.localFilters.titles.transactionType": "トランザクションタイプ",
"xpack.apm.localFilters.titles.transactionUrl": "Url",
"xpack.apm.localFiltersTitle": "フィルター",
"xpack.apm.metadataTable.section.agentLabel": "エージェント",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 2bdbfc3d565e5..135aa92fb0b1b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -5112,7 +5112,6 @@
"xpack.apm.localFilters.titles.serviceName": "服务名称",
"xpack.apm.localFilters.titles.serviceVersion": "服务版本",
"xpack.apm.localFilters.titles.transactionResult": "事务结果",
- "xpack.apm.localFilters.titles.transactionType": "事务类型",
"xpack.apm.localFilters.titles.transactionUrl": "URL",
"xpack.apm.localFiltersTitle": "筛选",
"xpack.apm.metadataTable.section.agentLabel": "代理",
From 82df009fd1db6b9a74f8f7e75018a5b712fa135a Mon Sep 17 00:00:00 2001
From: Jason Stoltzfus
Date: Thu, 4 Feb 2021 14:40:19 -0500
Subject: [PATCH 11/42] [App Search] Relevance Tuning logic - actions and
selectors only, no listeners (#89313)
---
.../components/relevance_tuning/index.ts | 1 +
.../relevance_tuning_logic.test.ts | 297 ++++++++++++++++++
.../relevance_tuning_logic.ts | 158 ++++++++++
.../components/relevance_tuning/types.ts | 23 ++
4 files changed, 479 insertions(+)
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts
create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts
index 909d10aae6823..07e53d0d29282 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/index.ts
@@ -7,3 +7,4 @@
export { RELEVANCE_TUNING_TITLE } from './constants';
export { RelevanceTuning } from './relevance_tuning';
+export { RelevanceTuningLogic } from './relevance_tuning_logic';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts
new file mode 100644
index 0000000000000..586a845ce382a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts
@@ -0,0 +1,297 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { LogicMounter } from '../../../__mocks__';
+
+import { BoostType } from './types';
+
+import { RelevanceTuningLogic } from './relevance_tuning_logic';
+
+describe('RelevanceTuningLogic', () => {
+ const { mount } = new LogicMounter(RelevanceTuningLogic);
+
+ const searchSettings = {
+ boosts: {
+ foo: [
+ {
+ type: 'value' as BoostType,
+ factor: 5,
+ },
+ ],
+ },
+ search_fields: {},
+ };
+ const schema = {};
+ const schemaConflicts = {};
+ const relevanceTuningProps = {
+ searchSettings,
+ schema,
+ schemaConflicts,
+ };
+ const searchResults = [{}, {}];
+
+ const DEFAULT_VALUES = {
+ dataLoading: true,
+ schema: {},
+ schemaConflicts: {},
+ searchSettings: {},
+ unsavedChanges: false,
+ filterInputValue: '',
+ query: '',
+ resultsLoading: false,
+ searchResults: null,
+ showSchemaConflictCallout: true,
+ engineHasSchemaFields: false,
+ schemaFields: [],
+ schemaFieldsWithConflicts: [],
+ filteredSchemaFields: [],
+ filteredSchemaFieldsWithConflicts: [],
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('has expected default values', () => {
+ mount();
+ expect(RelevanceTuningLogic.values).toEqual(DEFAULT_VALUES);
+ });
+
+ describe('actions', () => {
+ describe('onInitializeRelevanceTuning', () => {
+ it('should set searchSettings, schema, & schemaConflicts from the API response, and set dataLoading to false', () => {
+ mount({
+ dataLoading: true,
+ });
+ RelevanceTuningLogic.actions.onInitializeRelevanceTuning(relevanceTuningProps);
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ searchSettings,
+ schema,
+ dataLoading: false,
+ schemaConflicts,
+ });
+ });
+ });
+
+ describe('setSearchSettings', () => {
+ it('should set setSearchSettings and set unsavedChanges to true', () => {
+ mount({
+ unsavedChanges: false,
+ });
+ RelevanceTuningLogic.actions.setSearchSettings(searchSettings);
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ searchSettings,
+ unsavedChanges: true,
+ });
+ });
+ });
+
+ describe('setFilterValue', () => {
+ it('should set filterInputValue', () => {
+ mount();
+ RelevanceTuningLogic.actions.setFilterValue('foo');
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ filterInputValue: 'foo',
+ });
+ });
+ });
+
+ describe('setSearchQuery', () => {
+ it('should set query', () => {
+ mount();
+ RelevanceTuningLogic.actions.setSearchQuery('foo');
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ query: 'foo',
+ });
+ });
+ });
+
+ describe('setSearchResults', () => {
+ it('should set searchResults and set resultLoading to false', () => {
+ mount({
+ resultsLoading: true,
+ });
+ RelevanceTuningLogic.actions.setSearchResults(searchResults);
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ searchResults,
+ resultsLoading: false,
+ });
+ });
+ });
+
+ describe('setResultsLoading', () => {
+ it('should set resultsLoading', () => {
+ mount({
+ resultsLoading: false,
+ });
+ RelevanceTuningLogic.actions.setResultsLoading(true);
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ resultsLoading: true,
+ });
+ });
+ });
+
+ describe('clearSearchResults', () => {
+ it('should set searchResults', () => {
+ mount({
+ searchResults: [{}],
+ });
+ RelevanceTuningLogic.actions.clearSearchResults();
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ searchResults: null,
+ });
+ });
+ });
+
+ describe('resetSearchSettingsState', () => {
+ it('should set dataLoading', () => {
+ mount({
+ dataLoading: false,
+ });
+ RelevanceTuningLogic.actions.resetSearchSettingsState();
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ dataLoading: true,
+ });
+ });
+ });
+
+ describe('dismissSchemaConflictCallout', () => {
+ it('should set showSchemaConflictCallout to false', () => {
+ mount({
+ showSchemaConflictCallout: true,
+ });
+ RelevanceTuningLogic.actions.dismissSchemaConflictCallout();
+
+ expect(RelevanceTuningLogic.values).toEqual({
+ ...DEFAULT_VALUES,
+ showSchemaConflictCallout: false,
+ });
+ });
+ });
+ });
+
+ describe('selectors', () => {
+ describe('engineHasSchemaFields', () => {
+ it('should return false if there is only a single field in a schema', () => {
+ // This is because if a schema only has a single field, it is "id", which we do not
+ // consider a tunable field.
+ mount({
+ schema: {
+ id: 'foo',
+ },
+ });
+ expect(RelevanceTuningLogic.values.engineHasSchemaFields).toEqual(false);
+ });
+
+ it('should return true otherwise', () => {
+ mount({
+ schema: {
+ id: 'foo',
+ bar: 'bar',
+ },
+ });
+ expect(RelevanceTuningLogic.values.engineHasSchemaFields).toEqual(true);
+ });
+ });
+
+ describe('schemaFields', () => {
+ it('should return the list of field names from the schema', () => {
+ mount({
+ schema: {
+ id: 'foo',
+ bar: 'bar',
+ },
+ });
+ expect(RelevanceTuningLogic.values.schemaFields).toEqual(['id', 'bar']);
+ });
+ });
+
+ describe('schemaFieldsWithConflicts', () => {
+ it('should return the list of field names that have schema conflicts', () => {
+ mount({
+ schemaConflicts: {
+ foo: {
+ text: ['source_engine_1'],
+ number: ['source_engine_2'],
+ },
+ },
+ });
+ expect(RelevanceTuningLogic.values.schemaFieldsWithConflicts).toEqual(['foo']);
+ });
+ });
+
+ describe('filteredSchemaFields', () => {
+ it('should return a list of schema field names that contain the text from filterInputValue ', () => {
+ mount({
+ filterInputValue: 'ba',
+ schema: {
+ id: 'string',
+ foo: 'string',
+ bar: 'string',
+ baz: 'string',
+ },
+ });
+ expect(RelevanceTuningLogic.values.filteredSchemaFields).toEqual(['bar', 'baz']);
+ });
+
+ it('should return all schema fields if there is no filter applied', () => {
+ mount({
+ filterTerm: '',
+ schema: {
+ id: 'string',
+ foo: 'string',
+ bar: 'string',
+ baz: 'string',
+ },
+ });
+ expect(RelevanceTuningLogic.values.filteredSchemaFields).toEqual([
+ 'id',
+ 'foo',
+ 'bar',
+ 'baz',
+ ]);
+ });
+ });
+
+ describe('filteredSchemaFieldsWithConflicts', () => {
+ it('should return a list of schema field names that contain the text from filterInputValue, and if that field has a schema conflict', () => {
+ mount({
+ filterInputValue: 'ba',
+ schema: {
+ id: 'string',
+ foo: 'string',
+ bar: 'string',
+ baz: 'string',
+ },
+ schemaConflicts: {
+ bar: {
+ text: ['source_engine_1'],
+ number: ['source_engine_2'],
+ },
+ },
+ });
+ expect(RelevanceTuningLogic.values.filteredSchemaFieldsWithConflicts).toEqual(['bar']);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts
new file mode 100644
index 0000000000000..d4ec5e37f6ce5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts
@@ -0,0 +1,158 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { kea, MakeLogicType } from 'kea';
+
+import { Schema, SchemaConflicts } from '../../../shared/types';
+
+import { SearchSettings } from './types';
+
+interface RelevanceTuningProps {
+ searchSettings: SearchSettings;
+ schema: Schema;
+ schemaConflicts: SchemaConflicts;
+}
+
+interface RelevanceTuningActions {
+ onInitializeRelevanceTuning(props: RelevanceTuningProps): RelevanceTuningProps;
+ setSearchSettings(searchSettings: SearchSettings): { searchSettings: SearchSettings };
+ setFilterValue(value: string): string;
+ setSearchQuery(value: string): string;
+ setSearchResults(searchResults: object[]): object[];
+ setResultsLoading(resultsLoading: boolean): boolean;
+ clearSearchResults(): void;
+ resetSearchSettingsState(): void;
+ dismissSchemaConflictCallout(): void;
+}
+
+interface RelevanceTuningValues {
+ searchSettings: Partial;
+ schema: Schema;
+ schemaFields: string[];
+ schemaFieldsWithConflicts: string[];
+ filteredSchemaFields: string[];
+ filteredSchemaFieldsWithConflicts: string[];
+ schemaConflicts: SchemaConflicts;
+ showSchemaConflictCallout: boolean;
+ engineHasSchemaFields: boolean;
+ filterInputValue: string;
+ query: string;
+ unsavedChanges: boolean;
+ dataLoading: boolean;
+ searchResults: object[] | null;
+ resultsLoading: boolean;
+}
+
+// If the user hasn't entered a filter, then we can skip filtering the array entirely
+const filterIfTerm = (array: string[], filterTerm: string): string[] => {
+ return filterTerm === '' ? array : array.filter((item) => item.includes(filterTerm));
+};
+
+export const RelevanceTuningLogic = kea<
+ MakeLogicType
+>({
+ path: ['enterprise_search', 'app_search', 'relevance_tuning_logic'],
+ actions: () => ({
+ onInitializeRelevanceTuning: (props) => props,
+ setSearchSettings: (searchSettings) => ({ searchSettings }),
+ setFilterValue: (value) => value,
+ setSearchQuery: (query) => query,
+ setSearchResults: (searchResults) => searchResults,
+ setResultsLoading: (resultsLoading) => resultsLoading,
+ clearSearchResults: true,
+ resetSearchSettingsState: true,
+ dismissSchemaConflictCallout: true,
+ }),
+ reducers: () => ({
+ searchSettings: [
+ {},
+ {
+ onInitializeRelevanceTuning: (_, { searchSettings }) => searchSettings,
+ setSearchSettings: (_, { searchSettings }) => searchSettings,
+ },
+ ],
+ schema: [
+ {},
+ {
+ onInitializeRelevanceTuning: (_, { schema }) => schema,
+ },
+ ],
+ schemaConflicts: [
+ {},
+ {
+ onInitializeRelevanceTuning: (_, { schemaConflicts }) => schemaConflicts,
+ },
+ ],
+ showSchemaConflictCallout: [
+ true,
+ {
+ dismissSchemaConflictCallout: () => false,
+ },
+ ],
+ filterInputValue: [
+ '',
+ {
+ setFilterValue: (_, filterInputValue) => filterInputValue,
+ },
+ ],
+ query: [
+ '',
+ {
+ setSearchQuery: (_, query) => query,
+ },
+ ],
+ unsavedChanges: [
+ false,
+ {
+ setSearchSettings: () => true,
+ },
+ ],
+
+ dataLoading: [
+ true,
+ {
+ onInitializeRelevanceTuning: () => false,
+ resetSearchSettingsState: () => true,
+ },
+ ],
+ searchResults: [
+ null,
+ {
+ clearSearchResults: () => null,
+ setSearchResults: (_, searchResults) => searchResults,
+ },
+ ],
+ resultsLoading: [
+ false,
+ {
+ setResultsLoading: (_, resultsLoading) => resultsLoading,
+ setSearchResults: () => false,
+ },
+ ],
+ }),
+ selectors: ({ selectors }) => ({
+ schemaFields: [() => [selectors.schema], (schema: Schema) => Object.keys(schema)],
+ schemaFieldsWithConflicts: [
+ () => [selectors.schemaConflicts],
+ (schemaConflicts: SchemaConflicts) => Object.keys(schemaConflicts),
+ ],
+ filteredSchemaFields: [
+ () => [selectors.schemaFields, selectors.filterInputValue],
+ (schemaFields: string[], filterInputValue: string): string[] =>
+ filterIfTerm(schemaFields, filterInputValue),
+ ],
+ filteredSchemaFieldsWithConflicts: [
+ () => [selectors.schemaFieldsWithConflicts, selectors.filterInputValue],
+ (schemaFieldsWithConflicts: string[], filterInputValue: string): string[] =>
+ filterIfTerm(schemaFieldsWithConflicts, filterInputValue),
+ ],
+ engineHasSchemaFields: [
+ () => [selectors.schema],
+ (schema: Schema): boolean => Object.keys(schema).length >= 2,
+ ],
+ }),
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts
new file mode 100644
index 0000000000000..25187df89d64c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export type BoostType = 'value' | 'functional' | 'proximity';
+
+export interface Boost {
+ type: BoostType;
+ operation?: string;
+ function?: string;
+ newBoost?: boolean;
+ center?: string | number;
+ value?: string | number | string[] | number[];
+ factor: number;
+}
+
+export interface SearchSettings {
+ boosts: Record;
+ search_fields: object;
+}
From 0ef276dce1dcc33f48450be0acff818d3ae31d42 Mon Sep 17 00:00:00 2001
From: Lisa Cawley
Date: Thu, 4 Feb 2021 11:44:57 -0800
Subject: [PATCH 12/42] Use doc link service in more Stack Monitoring pages
(#89050)
Co-authored-by: igoristic
---
...-plugin-core-public.doclinksstart.links.md | 5 +++
.../public/doc_links/doc_links_service.ts | 13 ++++++
src/core/public/public.api.md | 5 +++
.../public/alerts/lib/alerts_toast.tsx | 16 ++-----
.../logs/__snapshots__/reason.test.js.snap | 16 +++----
.../public/components/logs/reason.js | 44 +++++--------------
.../public/components/logs/reason.test.js | 9 ++++
.../flyout/__snapshots__/flyout.test.js.snap | 42 +++++++-----------
.../metricbeat_migration/flyout/flyout.js | 5 +--
.../flyout/flyout.test.js | 12 ++++-
.../apm/enable_metricbeat_instructions.js | 18 +++-----
.../beats/common_beats_instructions.js | 1 -
.../beats/enable_metricbeat_instructions.js | 25 ++++-------
.../enable_metricbeat_instructions.js | 18 +++-----
.../kibana/enable_metricbeat_instructions.js | 18 +++-----
.../enable_metricbeat_instructions.js | 18 +++-----
.../public/lib/internal_monitoring_toasts.tsx | 16 ++-----
17 files changed, 119 insertions(+), 162 deletions(-)
diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
index 51e8d1a0b6bef..fd46a8a0f82c1 100644
--- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
+++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md
@@ -21,6 +21,7 @@ readonly links: {
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
+ readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
@@ -29,6 +30,10 @@ readonly links: {
};
readonly metricbeat: {
readonly base: string;
+ readonly configure: string;
+ readonly httpEndpoint: string;
+ readonly install: string;
+ readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 7fd62d6f02e96..4cb2969a63908 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -39,6 +39,7 @@ export class DocLinksService {
base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`,
installation: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-installation-configuration.html`,
configuration: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/configuring-howto-filebeat.html`,
+ elasticsearchModule: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-module-elasticsearch.html`,
elasticsearchOutput: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/elasticsearch-output.html`,
startup: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/filebeat-starting.html`,
exportedFields: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}/exported-fields.html`,
@@ -53,6 +54,10 @@ export class DocLinksService {
},
metricbeat: {
base: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}`,
+ configure: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/configuring-howto-metricbeat.html`,
+ httpEndpoint: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/http-endpoint.html`,
+ install: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation-configuration.html`,
+ start: `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`,
},
heartbeat: {
base: `${ELASTIC_WEBSITE_URL}guide/en/beats/heartbeat/${DOC_LINK_VERSION}`,
@@ -193,8 +198,11 @@ export class DocLinksService {
alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`,
alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`,
alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`,
+ metricbeatBlog: `${ELASTIC_WEBSITE_URL}blog/external-collection-for-elastic-stack-monitoring-is-now-available-via-metricbeat`,
monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`,
monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`,
+ monitorLogstash: `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}/monitoring-with-metricbeat.html`,
+ troubleshootKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitor-troubleshooting.html`,
},
security: {
apiKeyServiceSettings: `${ELASTICSEARCH_DOCS}security-settings.html#api-key-service-settings`,
@@ -257,6 +265,7 @@ export interface DocLinksStart {
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
+ readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
@@ -265,6 +274,10 @@ export interface DocLinksStart {
};
readonly metricbeat: {
readonly base: string;
+ readonly configure: string;
+ readonly httpEndpoint: string;
+ readonly install: string;
+ readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 37ebbcaa752af..75ed9aa5f150f 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -474,6 +474,7 @@ export interface DocLinksStart {
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
+ readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
@@ -482,6 +483,10 @@ export interface DocLinksStart {
};
readonly metricbeat: {
readonly base: string;
+ readonly configure: string;
+ readonly httpEndpoint: string;
+ readonly install: string;
+ readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
diff --git a/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx b/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx
index 8d889a7a4dc2a..026f172147192 100644
--- a/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx
@@ -19,7 +19,7 @@ export interface EnableAlertResponse {
}
const showTlsAndEncryptionError = () => {
- const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks;
+ const settingsUrl = Legacy.shims.docLinks.links.alerting.generalSettings;
Legacy.shims.toastNotifications.addWarning({
title: toMountPoint(
@@ -36,11 +36,7 @@ const showTlsAndEncryptionError = () => {
})}
-
+
{i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAction', {
defaultMessage: 'Learn how.',
})}
@@ -51,7 +47,7 @@ const showTlsAndEncryptionError = () => {
};
const showUnableToDisableWatcherClusterAlertsError = () => {
- const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks;
+ const settingsUrl = Legacy.shims.docLinks.links.alerting.generalSettings;
Legacy.shims.toastNotifications.addWarning({
title: toMountPoint(
@@ -68,11 +64,7 @@ const showUnableToDisableWatcherClusterAlertsError = () => {
})}
-
+
{i18n.translate('xpack.monitoring.healthCheck.unableToDisableWatches.action', {
defaultMessage: 'Learn more.',
})}
diff --git a/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap
index c925ecd1c98ff..40541aeaad4c1 100644
--- a/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap
@@ -13,7 +13,7 @@ exports[`Logs should render a default message 1`] = `
values={
Object {
"link":
Click here for more information
@@ -67,7 +67,7 @@ exports[`Logs should render with a no cluster found reason 1`] = `
values={
Object {
"link":
setup
@@ -92,7 +92,7 @@ exports[`Logs should render with a no index found reason 1`] = `
values={
Object {
"link":
setup
@@ -117,7 +117,7 @@ exports[`Logs should render with a no index pattern found reason 1`] = `
values={
Object {
"link":
Filebeat
@@ -142,7 +142,7 @@ exports[`Logs should render with a no node found reason 1`] = `
values={
Object {
"link":
setup
@@ -167,7 +167,7 @@ exports[`Logs should render with a no structured logs reason 1`] = `
values={
Object {
"link":
points to JSON logs
@@ -195,7 +195,7 @@ exports[`Logs should render with a no type found reason 1`] = `
values={
Object {
"link":
these directions
diff --git a/x-pack/plugins/monitoring/public/components/logs/reason.js b/x-pack/plugins/monitoring/public/components/logs/reason.js
index 538c8934cdaef..512b44c8165b1 100644
--- a/x-pack/plugins/monitoring/public/components/logs/reason.js
+++ b/x-pack/plugins/monitoring/public/components/logs/reason.js
@@ -13,7 +13,9 @@ import { Legacy } from '../../legacy_shims';
import { Monospace } from '../metricbeat_migration/instruction_steps/components/monospace/monospace';
export const Reason = ({ reason }) => {
- const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks;
+ const filebeatUrl = Legacy.shims.docLinks.links.filebeat.installation;
+ const elasticsearchUrl = Legacy.shims.docLinks.links.filebeat.elasticsearchModule;
+ const troubleshootUrl = Legacy.shims.docLinks.links.monitoring.troubleshootKibana;
let title = i18n.translate('xpack.monitoring.logs.reason.defaultTitle', {
defaultMessage: 'No log data found',
});
@@ -23,10 +25,7 @@ export const Reason = ({ reason }) => {
defaultMessage="We did not find any log data and we are unable to diagnose why. {link}"
values={{
link: (
-
+
{
defaultMessage="Set up {link}, then configure your Elasticsearch output to your monitoring cluster."
values={{
link: (
-
+
{i18n.translate('xpack.monitoring.logs.reason.noIndexPatternLink', {
defaultMessage: 'Filebeat',
})}
@@ -82,10 +78,7 @@ export const Reason = ({ reason }) => {
defaultMessage="Follow {link} to set up Elasticsearch."
values={{
link: (
-
+
{i18n.translate('xpack.monitoring.logs.reason.noTypeLink', {
defaultMessage: 'these directions',
})}
@@ -105,10 +98,7 @@ export const Reason = ({ reason }) => {
values={{
varPaths: var.paths,
link: (
-
+
{i18n.translate('xpack.monitoring.logs.reason.notUsingStructuredLogsLink', {
defaultMessage: 'points to JSON logs',
})}
@@ -127,10 +117,7 @@ export const Reason = ({ reason }) => {
defaultMessage="Check that your {link} is correct."
values={{
link: (
-
+
{i18n.translate('xpack.monitoring.logs.reason.noClusterLink', {
defaultMessage: 'setup',
})}
@@ -149,10 +136,7 @@ export const Reason = ({ reason }) => {
defaultMessage="Check that your {link} is correct."
values={{
link: (
-
+
{i18n.translate('xpack.monitoring.logs.reason.noNodeLink', {
defaultMessage: 'setup',
})}
@@ -171,10 +155,7 @@ export const Reason = ({ reason }) => {
defaultMessage="We found logs, but none for this index. If this problem continues, check that your {link} is correct."
values={{
link: (
-
+
{i18n.translate('xpack.monitoring.logs.reason.noIndexLink', {
defaultMessage: 'setup',
})}
@@ -193,10 +174,7 @@ export const Reason = ({ reason }) => {
defaultMessage="There is an issue reading from your filebeat indices. {link}."
values={{
link: (
-
+
{i18n.translate('xpack.monitoring.logs.reason.correctIndexNameLink', {
defaultMessage: 'Click here for more information',
})}
diff --git a/x-pack/plugins/monitoring/public/components/logs/reason.test.js b/x-pack/plugins/monitoring/public/components/logs/reason.test.js
index 53aad5511e0ae..0d75af1d1048f 100644
--- a/x-pack/plugins/monitoring/public/components/logs/reason.test.js
+++ b/x-pack/plugins/monitoring/public/components/logs/reason.test.js
@@ -15,6 +15,15 @@ jest.mock('../../legacy_shims', () => ({
docLinks: {
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
DOC_LINK_VERSION: 'current',
+ links: {
+ filebeat: {
+ elasticsearchModule: 'jest-metadata-mock-url',
+ installation: 'jest-metadata-mock-url',
+ },
+ monitoring: {
+ troubleshootKibana: 'jest-metadata-mock-url',
+ },
+ },
},
},
},
diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap
index 2f29cd9122a61..1173f36d620d6 100644
--- a/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap
@@ -156,7 +156,7 @@ exports[`Flyout apm part two should show instructions to migrate to metricbeat 1
"children":
({
shims: {
kfetch: jest.fn(),
docLinks: {
- ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
- DOC_LINK_VERSION: 'current',
+ links: {
+ monitoring: {
+ monitorKibana: 'jest-metadata-mock-url',
+ monitorElasticsearch: 'jest-metadata-mock-url',
+ },
+ metricbeat: {
+ install: 'jest-metadata-mock-url',
+ configure: 'jest-metadata-mock-url',
+ },
+ },
},
},
},
diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js
index 1006468d0c736..a0b5468cb9c77 100644
--- a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js
+++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js
@@ -14,10 +14,10 @@ import { Legacy } from '../../../../legacy_shims';
import { getMigrationStatusStep, getSecurityStep } from '../common_instructions';
export function getApmInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) {
- const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks;
- const securitySetup = getSecurityStep(
- `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/configuring-howto-metricbeat.html`
- );
+ const metricbeatConfigUrl = Legacy.shims.docLinks.links.metricbeat.configure;
+ const metricbeatInstallUrl = Legacy.shims.docLinks.links.metricbeat.install;
+ const metricbeatStartUrl = Legacy.shims.docLinks.links.metricbeat.start;
+ const securitySetup = getSecurityStep(metricbeatConfigUrl);
const installMetricbeatStep = {
title: i18n.translate(
@@ -29,10 +29,7 @@ export function getApmInstructionsForEnablingMetricbeat(product, _meta, { esMoni
children: (
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
});
const showIfLegacyOnlyIndices = () => {
- const { ELASTIC_WEBSITE_URL } = Legacy.shims.docLinks;
+ const blogUrl = Legacy.shims.docLinks.links.monitoring.metricbeatBlog;
const toast = Legacy.shims.toastNotifications.addWarning({
title: toMountPoint(
{
-
+
{learnMoreLabel()}
@@ -69,7 +65,7 @@ const showIfLegacyOnlyIndices = () => {
};
const showIfLegacyAndMetricbeatIndices = () => {
- const { ELASTIC_WEBSITE_URL } = Legacy.shims.docLinks;
+ const blogUrl = Legacy.shims.docLinks.links.monitoring.metricbeatBlog;
const toast = Legacy.shims.toastNotifications.addWarning({
title: toMountPoint(
{
-
+
{learnMoreLabel()}
From 2955d65a18169308259c357078b8b843e8ca87a2 Mon Sep 17 00:00:00 2001
From: Constance
Date: Thu, 4 Feb 2021 11:46:47 -0800
Subject: [PATCH 13/42] [Enterprise Search] Refactor MockRouter test helper to
not store payload (#90206)
* Update MockRouter to not pass/set a this.payload
- but instead intelligently validate payloads based on the request keys
* Fix relevance tuning API routes to not need a separate mock router for validating query & body
* Update all remaining tests to no longer pass a payload param to MockRouter
---
.../server/__mocks__/router.mock.ts | 16 +++++++---------
.../server/routes/app_search/analytics.test.ts | 2 --
.../routes/app_search/credentials.test.ts | 4 ----
.../server/routes/app_search/documents.test.ts | 1 -
.../server/routes/app_search/engines.test.ts | 1 -
.../routes/app_search/search_settings.test.ts | 17 ++---------------
.../server/routes/app_search/settings.test.ts | 1 -
.../routes/enterprise_search/telemetry.test.ts | 1 -
.../routes/workplace_search/groups.test.ts | 7 -------
.../routes/workplace_search/overview.test.ts | 1 -
.../routes/workplace_search/security.test.ts | 2 --
.../routes/workplace_search/settings.test.ts | 2 --
.../routes/workplace_search/sources.test.ts | 17 -----------------
13 files changed, 9 insertions(+), 63 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts
index 7fde7934cf7ad..88cf30bb2a549 100644
--- a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts
+++ b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts
@@ -23,7 +23,6 @@ type PayloadType = 'params' | 'query' | 'body';
interface IMockRouter {
method: MethodType;
path: string;
- payload?: PayloadType;
}
interface IMockRouterRequest {
body?: object;
@@ -39,11 +38,10 @@ export class MockRouter {
public payload?: PayloadType;
public response = httpServerMock.createResponseFactory();
- constructor({ method, path, payload }: IMockRouter) {
+ constructor({ method, path }: IMockRouter) {
this.createRouter();
this.method = method;
this.path = path;
- this.payload = payload;
}
public createRouter = () => {
@@ -62,16 +60,17 @@ export class MockRouter {
*/
public validateRoute = (request: MockRouterRequest) => {
- if (!this.payload) throw new Error('Cannot validate wihout a payload type specified.');
-
const route = this.findRouteRegistration();
const [config] = route;
const validate = config.validate as RouteValidatorConfig<{}, {}, {}>;
+ const payloads = Object.keys(request) as PayloadType[];
- const payloadValidation = validate[this.payload] as { validate(request: KibanaRequest): void };
- const payloadRequest = request[this.payload] as KibanaRequest;
+ payloads.forEach((payload: PayloadType) => {
+ const payloadValidation = validate[payload] as { validate(request: KibanaRequest): void };
+ const payloadRequest = request[payload] as KibanaRequest;
- payloadValidation.validate(payloadRequest);
+ payloadValidation.validate(payloadRequest);
+ });
};
public shouldValidate = (request: MockRouterRequest) => {
@@ -99,7 +98,6 @@ export class MockRouter {
// const mockRouter = new MockRouter({
// method: 'get',
// path: '/api/app_search/test',
-// payload: 'body'
// });
//
// beforeEach(() => {
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts
index 3d63e4044e75b..8e4a7dba165b1 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts
@@ -18,7 +18,6 @@ describe('analytics routes', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/app_search/engines/{engineName}/analytics/queries',
- payload: 'query',
});
registerAnalyticsRoutes({
@@ -71,7 +70,6 @@ describe('analytics routes', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/app_search/engines/{engineName}/analytics/queries/{query}',
- payload: 'query',
});
registerAnalyticsRoutes({
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts
index 7a513b1c76b4e..d9e84d3e62f28 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts
@@ -18,7 +18,6 @@ describe('credentials routes', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/app_search/credentials',
- payload: 'query',
});
registerCredentialsRoutes({
@@ -54,7 +53,6 @@ describe('credentials routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/app_search/credentials',
- payload: 'body',
});
registerCredentialsRoutes({
@@ -167,7 +165,6 @@ describe('credentials routes', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/app_search/credentials/details',
- payload: 'query',
});
registerCredentialsRoutes({
@@ -191,7 +188,6 @@ describe('credentials routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/app_search/credentials/{name}',
- payload: 'body',
});
registerCredentialsRoutes({
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts
index fdae51444bb54..af54d340ad150 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/documents.test.ts
@@ -18,7 +18,6 @@ describe('documents routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/app_search/engines/{engineName}/documents',
- payload: 'body',
});
registerDocumentsRoutes({
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts
index e874a188a10f7..abd26e18c7b9d 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts
@@ -29,7 +29,6 @@ describe('engine routes', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/app_search/engines',
- payload: 'query',
});
registerEnginesRoutes({
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts
index 92a695af12aaa..d8f677e2f0d82 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/search_settings.test.ts
@@ -87,7 +87,6 @@ describe('search settings routes', () => {
const mockRouter = new MockRouter({
method: 'put',
path: '/api/app_search/engines/{engineName}/search_settings',
- payload: 'body',
});
beforeEach(() => {
@@ -149,7 +148,6 @@ describe('search settings routes', () => {
const mockRouter = new MockRouter({
method: 'post',
path: '/api/app_search/engines/{engineName}/search_settings_search',
- payload: 'body',
});
beforeEach(() => {
@@ -188,29 +186,18 @@ describe('search settings routes', () => {
});
describe('validates query', () => {
- const queryRouter = new MockRouter({
- method: 'post',
- path: '/api/app_search/engines/{engineName}/search_settings_search',
- payload: 'query',
- });
-
it('correctly', () => {
- registerSearchSettingsRoutes({
- ...mockDependencies,
- router: queryRouter.router,
- });
-
const request = {
query: {
query: 'foo',
},
};
- queryRouter.shouldValidate(request);
+ mockRouter.shouldValidate(request);
});
it('missing required fields', () => {
const request = { query: {} };
- queryRouter.shouldThrow(request);
+ mockRouter.shouldThrow(request);
});
});
});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts
index 5d56bbf4fcd11..6df9a4f16d710 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts
@@ -41,7 +41,6 @@ describe('log settings routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/app_search/log_settings',
- payload: 'body',
});
registerSettingsRoutes({
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts
index f41ad367839c3..08c398ba3eb0d 100644
--- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts
@@ -29,7 +29,6 @@ describe('Enterprise Search Telemetry API', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/enterprise_search/stats',
- payload: 'body',
});
registerTelemetryRoute({
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts
index e67ca4c064886..68a9ae725f8a4 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/groups.test.ts
@@ -26,7 +26,6 @@ describe('groups routes', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/workplace_search/groups',
- payload: 'query',
});
registerGroupsRoute({
@@ -50,7 +49,6 @@ describe('groups routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/groups',
- payload: 'body',
});
registerGroupsRoute({
@@ -85,7 +83,6 @@ describe('groups routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/groups/search',
- payload: 'body',
});
registerSearchGroupsRoute({
@@ -163,7 +160,6 @@ describe('groups routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/groups/{id}',
- payload: 'body',
});
registerGroupRoute({
@@ -246,7 +242,6 @@ describe('groups routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/groups/{id}/share',
- payload: 'body',
});
registerShareGroupRoute({
@@ -282,7 +277,6 @@ describe('groups routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/groups/{id}/assign',
- payload: 'body',
});
registerAssignGroupRoute({
@@ -318,7 +312,6 @@ describe('groups routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/groups/{id}/boosts',
- payload: 'body',
});
registerBoostsGroupRoute({
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts
index 1afb85b389b42..bdf885648dff7 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/overview.test.ts
@@ -18,7 +18,6 @@ describe('Overview route', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/workplace_search/overview',
- payload: 'query',
});
registerOverviewRoute({
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts
index f2117a8bc948a..a1615499c56a2 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/security.test.ts
@@ -45,7 +45,6 @@ describe('security routes', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/workplace_search/org/security/source_restrictions',
- payload: 'body',
});
registerSecuritySourceRestrictionsRoute({
@@ -72,7 +71,6 @@ describe('security routes', () => {
mockRouter = new MockRouter({
method: 'patch',
path: '/api/workplace_search/org/security/source_restrictions',
- payload: 'body',
});
registerSecuritySourceRestrictionsRoute({
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts
index cf654918beb49..00a5b6c75df0a 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts
@@ -45,7 +45,6 @@ describe('settings routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/org/settings/customize',
- payload: 'body',
});
registerOrgSettingsCustomizeRoute({
@@ -76,7 +75,6 @@ describe('settings routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/org/settings/oauth_application',
- payload: 'body',
});
registerOrgSettingsOauthApplicationRoute({
diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts
index 2ae10e85ea9c0..a2fbe759f1a11 100644
--- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts
@@ -154,7 +154,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/account/create_source',
- payload: 'body',
});
registerAccountCreateSourceRoute({
@@ -194,7 +193,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/account/sources/{id}/documents',
- payload: 'body',
});
registerAccountSourceDocumentsRoute({
@@ -281,7 +279,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'patch',
path: '/api/workplace_search/account/sources/{id}/settings',
- payload: 'body',
});
registerAccountSourceSettingsRoute({
@@ -364,7 +361,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/account/sources/{id}/searchable',
- payload: 'body',
});
registerAccountSourceSearchableRoute({
@@ -422,7 +418,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/account/sources/{id}/display_settings/config',
- payload: 'body',
});
registerAccountSourceDisplaySettingsConfig({
@@ -489,7 +484,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/account/sources/{id}/schemas',
- payload: 'body',
});
registerAccountSourceSchemasRoute({
@@ -667,7 +661,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/org/create_source',
- payload: 'body',
});
registerOrgCreateSourceRoute({
@@ -707,7 +700,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/org/sources/{id}/documents',
- payload: 'body',
});
registerOrgSourceDocumentsRoute({
@@ -794,7 +786,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'patch',
path: '/api/workplace_search/org/sources/{id}/settings',
- payload: 'body',
});
registerOrgSourceSettingsRoute({
@@ -877,7 +868,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/org/sources/{id}/searchable',
- payload: 'body',
});
registerOrgSourceSearchableRoute({
@@ -935,7 +925,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/org/sources/{id}/display_settings/config',
- payload: 'body',
});
registerOrgSourceDisplaySettingsConfig({
@@ -1002,7 +991,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/org/sources/{id}/schemas',
- payload: 'body',
});
registerOrgSourceSchemasRoute({
@@ -1102,7 +1090,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/org/settings/connectors',
- payload: 'body',
});
registerOrgSourceOauthConfigurationsRoute({
@@ -1133,7 +1120,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/org/settings/connectors',
- payload: 'body',
});
registerOrgSourceOauthConfigurationsRoute({
@@ -1187,7 +1173,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'post',
path: '/api/workplace_search/org/settings/connectors/{serviceType}',
- payload: 'body',
});
registerOrgSourceOauthConfigurationRoute({
@@ -1218,7 +1203,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'put',
path: '/api/workplace_search/org/settings/connectors/{serviceType}',
- payload: 'body',
});
registerOrgSourceOauthConfigurationRoute({
@@ -1272,7 +1256,6 @@ describe('sources routes', () => {
mockRouter = new MockRouter({
method: 'get',
path: '/api/workplace_search/sources/create',
- payload: 'query',
});
registerOauthConnectorParamsRoute({
From 284842dc88d0befdbcff59907b57b6c6633b40c0 Mon Sep 17 00:00:00 2001
From: Scotty Bollinger
Date: Thu, 4 Feb 2021 13:58:44 -0600
Subject: [PATCH 14/42] [Workplace Search] Fix Source Settings bug (#90242)
* Remove comment
Verified that this works as expected
* Replaces usage from SourceLogic to AddSourceLogic
* Remove unused duplicate code
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../components/source_settings.tsx | 15 +++---
.../content_sources/source_logic.test.ts | 48 +------------------
.../views/content_sources/source_logic.ts | 42 ----------------
.../settings/components/source_config.tsx | 11 ++---
4 files changed, 14 insertions(+), 102 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
index dbde764a56861..2fa00c7f029f1 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx
@@ -48,30 +48,31 @@ import { ViewContentHeader } from '../../../components/shared/view_content_heade
import { SourceDataItem } from '../../../types';
import { AppLogic } from '../../../app_logic';
+import { AddSourceLogic } from '../components/add_source/add_source_logic';
import { staticSourceData } from '../source_data';
import { SourceLogic } from '../source_logic';
export const SourceSettings: React.FC = () => {
- const {
- updateContentSource,
- removeContentSource,
- resetSourceState,
- getSourceConfigData,
- } = useActions(SourceLogic);
+ const { updateContentSource, removeContentSource, resetSourceState } = useActions(SourceLogic);
+ const { getSourceConfigData } = useActions(AddSourceLogic);
const {
contentSource: { name, id, serviceType },
buttonLoading,
- sourceConfigData: { configuredFields },
} = useValues(SourceLogic);
+ const {
+ sourceConfigData: { configuredFields },
+ } = useValues(AddSourceLogic);
+
const { isOrganization } = useValues(AppLogic);
useEffect(() => {
getSourceConfigData(serviceType);
return resetSourceState;
}, []);
+
const {
configuration: { isPublicKey },
editPath,
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts
index bf5ec5a949b8d..15df7ddc99395 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts
@@ -18,11 +18,7 @@ jest.mock('../../app_logic', () => ({
AppLogic: { values: { isOrganization: true } },
}));
-import {
- fullContentSources,
- sourceConfigData,
- contentItems,
-} from '../../__mocks__/content_sources.mock';
+import { fullContentSources, contentItems } from '../../__mocks__/content_sources.mock';
import { meta } from '../../__mocks__/meta.mock';
import { DEFAULT_META } from '../../../shared/constants';
@@ -46,7 +42,6 @@ describe('SourceLogic', () => {
const defaultValues = {
contentSource: {},
contentItems: [],
- sourceConfigData: {},
dataLoading: true,
sectionLoading: true,
buttonLoading: false,
@@ -88,13 +83,6 @@ describe('SourceLogic', () => {
expect(setSuccessMessage).toHaveBeenCalled();
});
- it('setSourceConfigData', () => {
- SourceLogic.actions.setSourceConfigData(sourceConfigData);
-
- expect(SourceLogic.values.sourceConfigData).toEqual(sourceConfigData);
- expect(SourceLogic.values.dataLoading).toEqual(false);
- });
-
it('setSearchResults', () => {
SourceLogic.actions.setSearchResults(searchServerResponse);
@@ -402,40 +390,6 @@ describe('SourceLogic', () => {
});
});
- describe('getSourceConfigData', () => {
- const serviceType = 'github';
-
- it('calls API and sets values', async () => {
- AppLogic.values.isOrganization = true;
-
- const setSourceConfigDataSpy = jest.spyOn(SourceLogic.actions, 'setSourceConfigData');
- const promise = Promise.resolve(contentSource);
- http.get.mockReturnValue(promise);
- SourceLogic.actions.getSourceConfigData(serviceType);
-
- expect(http.get).toHaveBeenCalledWith(
- `/api/workplace_search/org/settings/connectors/${serviceType}`
- );
- await promise;
- expect(setSourceConfigDataSpy).toHaveBeenCalled();
- });
-
- it('handles error', async () => {
- const error = {
- response: {
- error: 'this is an error',
- status: 400,
- },
- };
- const promise = Promise.reject(error);
- http.get.mockReturnValue(promise);
- SourceLogic.actions.getSourceConfigData(serviceType);
- await expectedAsyncError(promise);
-
- expect(flashAPIErrors).toHaveBeenCalledWith(error);
- });
- });
-
it('resetSourceState', () => {
SourceLogic.actions.resetSourceState();
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts
index 1eef715350848..c1f5d6033543f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts
@@ -27,7 +27,6 @@ import { ContentSourceFullData, Meta, DocumentSummaryItem, SourceContentItem } f
export interface SourceActions {
onInitializeSource(contentSource: ContentSourceFullData): ContentSourceFullData;
onUpdateSourceName(name: string): string;
- setSourceConfigData(sourceConfigData: SourceConfigData): SourceConfigData;
setSearchResults(searchResultsResponse: SearchResultsResponse): SearchResultsResponse;
initializeFederatedSummary(sourceId: string): { sourceId: string };
onUpdateSummary(summary: DocumentSummaryItem[]): DocumentSummaryItem[];
@@ -41,28 +40,9 @@ export interface SourceActions {
resetSourceState(): void;
removeContentSource(sourceId: string): { sourceId: string };
initializeSource(sourceId: string): { sourceId: string };
- getSourceConfigData(serviceType: string): { serviceType: string };
setButtonNotLoading(): void;
}
-interface SourceConfigData {
- serviceType: string;
- name: string;
- configured: boolean;
- categories: string[];
- needsPermissions?: boolean;
- privateSourcesEnabled: boolean;
- configuredFields: {
- publicKey: string;
- privateKey: string;
- consumerKey: string;
- baseUrl?: string;
- clientId?: string;
- clientSecret?: string;
- };
- accountContextOnly?: boolean;
-}
-
interface SourceValues {
contentSource: ContentSourceFullData;
dataLoading: boolean;
@@ -71,7 +51,6 @@ interface SourceValues {
contentItems: SourceContentItem[];
contentMeta: Meta;
contentFilterValue: string;
- sourceConfigData: SourceConfigData;
}
interface SearchResultsResponse {
@@ -84,7 +63,6 @@ export const SourceLogic = kea>({
actions: {
onInitializeSource: (contentSource: ContentSourceFullData) => contentSource,
onUpdateSourceName: (name: string) => name,
- setSourceConfigData: (sourceConfigData: SourceConfigData) => sourceConfigData,
onUpdateSummary: (summary: object[]) => summary,
setSearchResults: (searchResultsResponse: SearchResultsResponse) => searchResultsResponse,
setContentFilterValue: (contentFilterValue: string) => contentFilterValue,
@@ -96,7 +74,6 @@ export const SourceLogic = kea>({
removeContentSource: (sourceId: string) => ({
sourceId,
}),
- getSourceConfigData: (serviceType: string) => ({ serviceType }),
resetSourceState: () => true,
setButtonNotLoading: () => false,
},
@@ -115,17 +92,10 @@ export const SourceLogic = kea>({
}),
},
],
- sourceConfigData: [
- {} as SourceConfigData,
- {
- setSourceConfigData: (_, sourceConfigData) => sourceConfigData,
- },
- ],
dataLoading: [
true,
{
onInitializeSource: () => false,
- setSourceConfigData: () => false,
resetSourceState: () => false,
},
],
@@ -133,7 +103,6 @@ export const SourceLogic = kea>({
false,
{
setButtonNotLoading: () => false,
- setSourceConfigData: () => false,
resetSourceState: () => false,
removeContentSource: () => true,
},
@@ -181,7 +150,6 @@ export const SourceLogic = kea>({
actions.initializeFederatedSummary(sourceId);
}
} catch (e) {
- // TODO: Verify this works once components are there. Not sure if the catch gives a status code.
if (e.response.status === 404) {
KibanaLogic.values.navigateToUrl(NOT_FOUND_PATH);
} else {
@@ -260,16 +228,6 @@ export const SourceLogic = kea>({
actions.setButtonNotLoading();
}
},
- getSourceConfigData: async ({ serviceType }) => {
- const route = `/api/workplace_search/org/settings/connectors/${serviceType}`;
-
- try {
- const response = await HttpLogic.values.http.get(route);
- actions.setSourceConfigData(response);
- } catch (e) {
- flashAPIErrors(e);
- }
- },
onUpdateSourceName: (name: string) => {
setSuccessMessage(
i18n.translate(
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
index 79f418a48dabc..4b59e0f3401c5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx
@@ -15,7 +15,6 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { Loading } from '../../../../shared/loading';
import { SourceDataItem } from '../../../types';
import { staticSourceData } from '../../content_sources/source_data';
-import { SourceLogic } from '../../content_sources/source_logic';
import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic';
import { AddSourceHeader } from '../../content_sources/components/add_source/add_source_header';
@@ -31,18 +30,18 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => {
const [confirmModalVisible, setConfirmModalVisibility] = useState(false);
const { configuration, serviceType } = staticSourceData[sourceIndex] as SourceDataItem;
const { deleteSourceConfig } = useActions(SettingsLogic);
- const { getSourceConfigData } = useActions(SourceLogic);
- const { saveSourceConfig } = useActions(AddSourceLogic);
+ const { saveSourceConfig, getSourceConfigData } = useActions(AddSourceLogic);
const {
sourceConfigData: { name, categories },
- dataLoading: sourceDataLoading,
- } = useValues(SourceLogic);
+ dataLoading,
+ } = useValues(AddSourceLogic);
useEffect(() => {
getSourceConfigData(serviceType);
}, []);
- if (sourceDataLoading) return ;
+ if (dataLoading) return ;
+
const hideConfirmModal = () => setConfirmModalVisibility(false);
const showConfirmModal = () => setConfirmModalVisibility(true);
const saveUpdatedConfig = () => saveSourceConfig(true);
From 9e7e1e17088ea33df8b62f2dc6d9f68494b7f270 Mon Sep 17 00:00:00 2001
From: John Schulz
Date: Thu, 4 Feb 2021 15:16:45 -0500
Subject: [PATCH 15/42] [Fleet] Managed Agent Policy (#88688)
## Summary
Introduces the concept of a managed agent policy. Resolves most of the acceptance criteria from #76843. Remaining to be done in follow up PRs
- [x] Define hosted Agent Policy concept in Fleet.
- [x] Flag in policy? **_yes, added `is_managed: boolean`_ in agent policy SO**
- [x] Should not built only for cloud, an admin should be able to set theses restrictions.
- [x] We should have an API to configure it _**Can `POST` and `PUT` to `/api/fleet/agent_policies/{policy_id}`**_
- [x] Integration should be editable, we expect integration author to do the right thing and limit what can be edited.
- [x] Research if we can ensure the right behavior of Hosted Agent policy and restrict the super user.
- [ ] Capabilities restrictions
- [ ] An Agent enrolled in an Hosted Agent policy should not be able to be upgraded.
- [x] An Agent enrolled in an Hosted Agent policy should not be able to be unenrolled.
- [ ] No Agents cannot be enrolled into this policy by the user.
- Hide the enrollment key?
- Need to figure out the workflow.
- [x] An Agent enrolled in an Hosted Agent policy should not be able to be reassigned to a different configuration.
- [x] As a user I should be prevented to do theses action. _**No user-level checks. Only Agent Policy. No UI changes, but API errors are shown for failed actions like reassigning**_
- [x] As an API user I should receive error messages.
- [x] If making a single "flag" is easier/faster let's do it. _**Currently single `is_managed` property on agent policy SO.**_
Checks are implemented in service layer (is agent enrolled in a managed policy?)
No UI-specific changes added but UI is affected because HTTP requests (like `api/fleet/agents/{agentId}/reassign`) can fail. See screenshots below.
Tests at service (`yarn test:jest`) and http (`yarn test ftr`) layers for each of create policy, update policy, unenroll agent, and reassign agent
Bulk actions currently filter out restricted items. A follow-up PR will change them to throw an error and cause the request to fail.
## Managed Policy
Can create (`POST`) and update (`PUT`) an agent policy with an `is_managed` property. Each new saved object will have an `is_managed` property (default `false`)
HTTP commands
#### Create (`is_managed: false` by default)
```
curl --user elastic:changeme -X POST localhost:5601/api/fleet/agent_policies -H 'Content-Type: application/json' -d'{ "name": "User created policy", "namespace": "default"}' -H 'kbn-xsrf: true'
{"item":{"id":"edc236a0-5cbb-11eb-ab2c-0134aecb4ce8","name":"User created policy","namespace":"default","is_managed":false,"revision":1,"updated_at":"2021-01-22T14:12:58.250Z","updated_by":"elastic"}}
```
#### Create with `is_managed: true`
```
curl --user elastic:changeme -X POST localhost:5601/api/fleet/agent_policies -H 'Content-Type: application/json' -d'{ "name": "User created policy", "namespace": "default"}' -H 'kbn-xsrf: true'
{"item":{"id":"67c785b0-662e-11eb-bf6b-4790dc0178c0","name":"User created policy","namespace":"default","is_managed":false,"revision":1,"updated_at":"2021-02-03T14:45:06.059Z","updated_by":"elastic"}}
```
#### Update with `is_managed: true`
```
curl --user elastic:changeme -X PUT -H 'Content-Type: application/json' -H 'kbn-xsrf: 1234' localhost:5601/api/fleet/agent_policies/67c785b0-662e-11eb-bf6b-4790dc0178c0 -d '{ "name":"User created policy","namespace":"default","is_managed":true }'
{"item":{"id":"67c785b0-662e-11eb-bf6b-4790dc0178c0","name":"User created policy","namespace":"default","is_managed":true,"revision":2,"updated_at":"2021-02-03T14:47:28.471Z","updated_by":"elastic","package_policies":[]}}
```
## Enroll behavior
is not changed/addressed in this PR. Agents can still be enrolled in managed policies
## Unenroll Agent from managed policy behavior
#### Enrolled in managed agent policy, cannot be unenrolled
```
curl --user elastic:changeme -X POST http://localhost:5601/api/fleet/agents/441d4a40-6710-11eb-8f57-db14e8e41cff/unenroll -H 'kbn-xsrf: 1234' | jq
{
"statusCode": 400,
"error": "Bad Request",
"message": "Cannot unenroll 441d4a40-6710-11eb-8f57-db14e8e41cff from a managed agent policy af9b4970-6701-11eb-b55a-899b78cb64da"
}
```
Screenshots for managed & unmanaged policies
#### Enrolled in managed agent policy, cannot be unenrolled
#### Enrolled agent policy is not managed, agent can be unenrolled
## Reassign agent
#### No agent can be reassigned to a managed policy
```
curl --user elastic:changeme -X 'PUT' 'http://localhost:5601/api/fleet/agents/482760d0-6710-11eb-8f57-db14e8e41cff/reassign' -H 'kbn-xsrf: xxx' -H 'Content-Type: application/json' -d '{"policy_id":"af9b4970-6701-11eb-b55a-899b78cb64da"}'
{
"statusCode": 400,
"error": "Bad Request",
"message": "Cannot reassign an agent to managed agent policy 94129590-6707-11eb-b55a-899b78cb64da"
}
```
Screenshots
#### Enrolled in managed agent policy, cannot be reassigned
```
curl --user elastic:changeme -X 'PUT' 'http://localhost:5601/api/fleet/agents/482760d0-6710-11eb-8f57-db14e8e41cff/reassign' -H 'kbn-xsrf: xxx' -H 'Content-Type: application/json' -d '{"policy_id":"af9b4970-6701-11eb-b55a-899b78cb64da"}'
{
"statusCode": 400,
"error": "Bad Request",
"message": "Cannot reassign an agent from managed agent policy 94129590-6707-11eb-b55a-899b78cb64da"
}
```
Screenshots
#### Enrolled agent policy is unmanaged, agent can be reassigned to another unmanaged policy
Screenshots
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
- [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
---
.../fleet/common/constants/agent_policy.ts | 1 +
.../fleet/common/types/models/agent_policy.ts | 2 +
.../epm/screens/detail/index.test.tsx | 2 +
x-pack/plugins/fleet/server/errors/index.ts | 2 +
.../server/routes/agent/unenroll_handler.ts | 2 +-
.../fleet/server/saved_objects/index.ts | 4 +-
.../saved_objects/migrations/to_v7_12_0.ts | 15 +-
.../server/services/agent_policy.test.ts | 87 +++++++++++-
.../fleet/server/services/agent_policy.ts | 1 +
.../fleet/server/services/agents/crud.ts | 18 ++-
.../server/services/agents/reassign.test.ts | 132 ++++++++++++++++++
.../fleet/server/services/agents/reassign.ts | 46 +++++-
.../server/services/agents/unenroll.test.ts | 125 +++++++++++++++++
.../fleet/server/services/agents/unenroll.ts | 45 ++++--
.../fleet/server/services/agents/update.ts | 2 +-
.../fleet/server/types/models/agent_policy.ts | 2 +
.../common/endpoint/generate_data.ts | 1 +
.../apis/agent_policy/agent_policy.ts | 53 ++++++-
.../apis/agents/reassign.ts | 35 ++++-
.../apis/agents/unenroll.ts | 70 ++++++++--
20 files changed, 604 insertions(+), 41 deletions(-)
create mode 100644 x-pack/plugins/fleet/server/services/agents/reassign.test.ts
create mode 100644 x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts
index 363607aae2b46..96b6249585bfc 100644
--- a/x-pack/plugins/fleet/common/constants/agent_policy.ts
+++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts
@@ -24,6 +24,7 @@ export const DEFAULT_AGENT_POLICY: Omit<
status: agentPolicyStatuses.Active,
package_policies: [],
is_default: true,
+ is_managed: false,
monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>,
};
diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
index 5e86e8e6acb70..5f41b0f70ca74 100644
--- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
@@ -17,6 +17,7 @@ export interface NewAgentPolicy {
namespace: string;
description?: string;
is_default?: boolean;
+ is_managed?: boolean; // Optional when creating a policy
monitoring_enabled?: Array>;
}
@@ -24,6 +25,7 @@ export interface AgentPolicy extends NewAgentPolicy {
id: string;
status: ValueOf;
package_policies: string[] | PackagePolicy[];
+ is_managed: boolean; // required for created policy
updated_at: string;
updated_by: string;
revision: number;
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx
index 30588c10178da..b60d3b5eb1f2d 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.test.tsx
@@ -687,6 +687,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
'e8a37031-2907-44f6-89d2-98bd493f60dc',
],
is_default: true,
+ is_managed: false,
monitoring_enabled: ['logs', 'metrics'],
revision: 6,
updated_at: '2020-12-09T13:46:31.840Z',
@@ -701,6 +702,7 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
status: 'active',
package_policies: ['e8a37031-2907-44f6-89d2-98bd493f60cd'],
is_default: false,
+ is_managed: false,
monitoring_enabled: ['logs', 'metrics'],
revision: 2,
updated_at: '2020-12-09T13:46:31.840Z',
diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts
index ee30c01ac8eec..a903de0138039 100644
--- a/x-pack/plugins/fleet/server/errors/index.ts
+++ b/x-pack/plugins/fleet/server/errors/index.ts
@@ -32,3 +32,5 @@ export class PackageCacheError extends IngestManagerError {}
export class PackageOperationNotSupportedError extends IngestManagerError {}
export class FleetAdminUserInvalidError extends IngestManagerError {}
export class ConcurrentInstallOperationError extends IngestManagerError {}
+export class AgentReassignmentError extends IngestManagerError {}
+export class AgentUnenrollmentError extends IngestManagerError {}
diff --git a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts
index 0365d9f5a29fe..614ccd8a26624 100644
--- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts
+++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts
@@ -24,7 +24,7 @@ export const postAgentUnenrollHandler: RequestHandler<
if (request.body?.force === true) {
await AgentService.forceUnenrollAgent(soClient, esClient, request.params.agentId);
} else {
- await AgentService.unenrollAgent(soClient, request.params.agentId);
+ await AgentService.unenrollAgent(soClient, esClient, request.params.agentId);
}
const body: PostAgentUnenrollResponse = {};
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index c61dd1b8e4a19..d50db8d9809f4 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -32,7 +32,7 @@ import {
migrateSettingsToV7100,
migrateAgentActionToV7100,
} from './migrations/to_v7_10_0';
-import { migrateAgentToV7120 } from './migrations/to_v7_12_0';
+import { migrateAgentToV7120, migrateAgentPolicyToV7120 } from './migrations/to_v7_12_0';
/*
* Saved object types and mappings
@@ -161,6 +161,7 @@ const getSavedObjectTypes = (
description: { type: 'text' },
namespace: { type: 'keyword' },
is_default: { type: 'boolean' },
+ is_managed: { type: 'boolean' },
status: { type: 'keyword' },
package_policies: { type: 'keyword' },
updated_at: { type: 'date' },
@@ -171,6 +172,7 @@ const getSavedObjectTypes = (
},
migrations: {
'7.10.0': migrateAgentPolicyToV7100,
+ '7.12.0': migrateAgentPolicyToV7120,
},
},
[ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE]: {
diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts
index 1635f38cd5522..49a0d6fc7737f 100644
--- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_12_0.ts
@@ -5,8 +5,8 @@
* 2.0.
*/
-import { SavedObjectMigrationFn } from 'kibana/server';
-import { Agent } from '../../types';
+import type { SavedObjectMigrationFn } from 'kibana/server';
+import type { Agent, AgentPolicy } from '../../types';
export const migrateAgentToV7120: SavedObjectMigrationFn = (
agentDoc
@@ -15,3 +15,14 @@ export const migrateAgentToV7120: SavedObjectMigrationFn,
+ AgentPolicy
+> = (agentPolicyDoc) => {
+ const isV12 = 'is_managed' in agentPolicyDoc.attributes;
+ if (!isV12) {
+ agentPolicyDoc.attributes.is_managed = false;
+ }
+ return agentPolicyDoc;
+};
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts
index b70041e66dcd9..800d4f479bfde 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts
@@ -8,17 +8,16 @@
import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
import { agentPolicyService } from './agent_policy';
import { agentPolicyUpdateEventHandler } from './agent_policy_update';
-import { Output } from '../types';
+import type { AgentPolicy, NewAgentPolicy, Output } from '../types';
function getSavedObjectMock(agentPolicyAttributes: any) {
const mock = savedObjectsClientMock.create();
-
mock.get.mockImplementation(async (type: string, id: string) => {
return {
type,
id,
references: [],
- attributes: agentPolicyAttributes,
+ attributes: agentPolicyAttributes as AgentPolicy,
};
});
mock.find.mockImplementation(async (options) => {
@@ -69,10 +68,59 @@ function getAgentPolicyUpdateMock() {
>;
}
+function getAgentPolicyCreateMock() {
+ const soClient = savedObjectsClientMock.create();
+ soClient.create.mockImplementation(async (type, attributes) => {
+ return {
+ attributes: (attributes as unknown) as NewAgentPolicy,
+ id: 'mocked',
+ type: 'mocked',
+ references: [],
+ };
+ });
+ return soClient;
+}
describe('agent policy', () => {
beforeEach(() => {
getAgentPolicyUpdateMock().mockClear();
});
+
+ describe('create', () => {
+ it('is_managed present and false by default', async () => {
+ // ignore unrelated unique name constraint
+ agentPolicyService.requireUniqueName = async () => {};
+ const soClient = getAgentPolicyCreateMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ await expect(
+ agentPolicyService.create(soClient, esClient, {
+ name: 'No is_managed provided',
+ namespace: 'default',
+ })
+ ).resolves.toHaveProperty('is_managed', false);
+
+ const [, attributes] = soClient.create.mock.calls[0];
+ expect(attributes).toHaveProperty('is_managed', false);
+ });
+
+ it('should set is_managed property, if given', async () => {
+ // ignore unrelated unique name constraint
+ agentPolicyService.requireUniqueName = async () => {};
+ const soClient = getAgentPolicyCreateMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ await expect(
+ agentPolicyService.create(soClient, esClient, {
+ name: 'is_managed: true provided',
+ namespace: 'default',
+ is_managed: true,
+ })
+ ).resolves.toHaveProperty('is_managed', true);
+
+ const [, attributes] = soClient.create.mock.calls[0];
+ expect(attributes).toHaveProperty('is_managed', true);
+ });
+ });
+
describe('bumpRevision', () => {
it('should call agentPolicyUpdateEventHandler with updated event once', async () => {
const soClient = getSavedObjectMock({
@@ -208,4 +256,37 @@ describe('agent policy', () => {
});
});
});
+
+ describe('update', () => {
+ it('should update is_managed property, if given', async () => {
+ // ignore unrelated unique name constraint
+ agentPolicyService.requireUniqueName = async () => {};
+ const soClient = savedObjectsClientMock.create();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ soClient.get.mockResolvedValue({
+ attributes: {},
+ id: 'mocked',
+ type: 'mocked',
+ references: [],
+ });
+ await agentPolicyService.update(soClient, esClient, 'mocked', {
+ name: 'mocked',
+ namespace: 'default',
+ is_managed: false,
+ });
+ // soClient.update is called with updated values
+ let calledWith = soClient.update.mock.calls[0];
+ expect(calledWith[2]).toHaveProperty('is_managed', false);
+
+ await agentPolicyService.update(soClient, esClient, 'mocked', {
+ name: 'is_managed: true provided',
+ namespace: 'default',
+ is_managed: true,
+ });
+ // soClient.update is called with updated values
+ calledWith = soClient.update.mock.calls[1];
+ expect(calledWith[2]).toHaveProperty('is_managed', true);
+ });
+ });
});
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 4a3319941b575..dfe5c19bc417b 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -140,6 +140,7 @@ class AgentPolicyService {
SAVED_OBJECT_TYPE,
{
...agentPolicy,
+ is_managed: agentPolicy.is_managed ?? false,
revision: 1,
updated_at: new Date().toISOString(),
updated_by: options?.user?.username || 'system',
diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts
index 9382a8bb61647..36506d0590595 100644
--- a/x-pack/plugins/fleet/server/services/agents/crud.ts
+++ b/x-pack/plugins/fleet/server/services/agents/crud.ts
@@ -12,7 +12,7 @@ import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
import { AgentSOAttributes, Agent, ListWithKuery } from '../../types';
import { escapeSearchQueryPhrase } from '../saved_object';
import { savedObjectToAgent } from './saved_objects';
-import { appContextService } from '../../services';
+import { appContextService, agentPolicyService } from '../../services';
import * as crudServiceSO from './crud_so';
import * as crudServiceFleetServer from './crud_fleet_server';
@@ -86,6 +86,22 @@ export async function getAgents(soClient: SavedObjectsClientContract, agentIds:
return agents;
}
+export async function getAgentPolicyForAgent(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ agentId: string
+) {
+ const agent = await getAgent(soClient, esClient, agentId);
+ if (!agent.policy_id) {
+ return;
+ }
+
+ const agentPolicy = await agentPolicyService.get(soClient, agent.policy_id, false);
+ if (agentPolicy) {
+ return agentPolicy;
+ }
+}
+
export async function getAgentByAccessAPIKeyId(
soClient: SavedObjectsClientContract,
accessAPIKeyId: string
diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.test.ts b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts
new file mode 100644
index 0000000000000..7338c440483ea
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/agents/reassign.test.ts
@@ -0,0 +1,132 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
+import type { SavedObject } from 'kibana/server';
+import type { Agent, AgentPolicy } from '../../types';
+import { AgentReassignmentError } from '../../errors';
+import { reassignAgent, reassignAgents } from './reassign';
+
+const agentInManagedSO = {
+ id: 'agent-in-managed-policy',
+ attributes: { policy_id: 'managed-agent-policy' },
+} as SavedObject;
+const agentInManagedSO2 = {
+ id: 'agent-in-managed-policy2',
+ attributes: { policy_id: 'managed-agent-policy' },
+} as SavedObject;
+const agentInUnmanagedSO = {
+ id: 'agent-in-unmanaged-policy',
+ attributes: { policy_id: 'unmanaged-agent-policy' },
+} as SavedObject;
+const agentInUnmanagedSO2 = {
+ id: 'agent-in-unmanaged-policy2',
+ attributes: { policy_id: 'unmanaged-agent-policy' },
+} as SavedObject;
+const unmanagedAgentPolicySO = {
+ id: 'unmanaged-agent-policy',
+ attributes: { is_managed: false },
+} as SavedObject;
+const managedAgentPolicySO = {
+ id: 'managed-agent-policy',
+ attributes: { is_managed: true },
+} as SavedObject;
+
+describe('reassignAgent (singular)', () => {
+ it('can reassign from unmanaged policy to unmanaged', async () => {
+ const soClient = createClientMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ await reassignAgent(soClient, esClient, agentInUnmanagedSO.id, agentInUnmanagedSO2.id);
+
+ // calls ES update with correct values
+ expect(soClient.update).toBeCalledTimes(1);
+ const calledWith = soClient.update.mock.calls[0];
+ expect(calledWith[1]).toBe(agentInUnmanagedSO.id);
+ expect(calledWith[2]).toHaveProperty('policy_id', agentInUnmanagedSO2.id);
+ });
+
+ it('cannot reassign from unmanaged policy to managed', async () => {
+ const soClient = createClientMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ await expect(
+ reassignAgent(
+ soClient,
+ esClient,
+ agentInUnmanagedSO.id,
+ agentInManagedSO.attributes.policy_id!
+ )
+ ).rejects.toThrowError(AgentReassignmentError);
+
+ // does not call ES update
+ expect(soClient.update).toBeCalledTimes(0);
+ });
+
+ it('cannot reassign from managed policy', async () => {
+ const soClient = createClientMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ await expect(
+ reassignAgent(soClient, esClient, agentInManagedSO.id, agentInManagedSO2.id)
+ ).rejects.toThrowError(AgentReassignmentError);
+ // does not call ES update
+ expect(soClient.update).toBeCalledTimes(0);
+
+ await expect(
+ reassignAgent(soClient, esClient, agentInManagedSO.id, agentInUnmanagedSO.id)
+ ).rejects.toThrowError(AgentReassignmentError);
+ // does not call ES update
+ expect(soClient.update).toBeCalledTimes(0);
+ });
+});
+
+describe('reassignAgents (plural)', () => {
+ it('agents in managed policies are not updated', async () => {
+ const soClient = createClientMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ const idsToReassign = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO.id];
+ await reassignAgents(soClient, esClient, { agentIds: idsToReassign }, agentInUnmanagedSO.id);
+
+ // calls ES update with correct values
+ const calledWith = soClient.bulkUpdate.mock.calls[0][0];
+ const expectedResults = [agentInUnmanagedSO.id, agentInUnmanagedSO.id];
+ expect(calledWith.length).toBe(expectedResults.length); // only 2 are unmanaged
+ expect(calledWith.map(({ id }) => id)).toEqual(expectedResults);
+ });
+});
+
+function createClientMock() {
+ const soClientMock = savedObjectsClientMock.create();
+
+ // need to mock .create & bulkCreate due to (bulk)createAgentAction(s) in reassignAgent(s)
+ soClientMock.create.mockResolvedValue(agentInUnmanagedSO);
+ soClientMock.bulkCreate.mockImplementation(async ([{ type, attributes }]) => {
+ return {
+ saved_objects: [await soClientMock.create(type, attributes)],
+ };
+ });
+
+ soClientMock.get.mockImplementation(async (_, id) => {
+ switch (id) {
+ case unmanagedAgentPolicySO.id:
+ return unmanagedAgentPolicySO;
+ case managedAgentPolicySO.id:
+ return managedAgentPolicySO;
+ case agentInManagedSO.id:
+ return agentInManagedSO;
+ case agentInUnmanagedSO.id:
+ default:
+ return agentInUnmanagedSO;
+ }
+ });
+
+ soClientMock.bulkGet.mockImplementation(async (options) => {
+ return {
+ saved_objects: await Promise.all(options!.map(({ type, id }) => soClientMock.get(type, id))),
+ };
+ });
+
+ return soClientMock;
+}
diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts
index fbd91c05dfb4a..9f4373ab553ec 100644
--- a/x-pack/plugins/fleet/server/services/agents/reassign.ts
+++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts
@@ -5,12 +5,13 @@
* 2.0.
*/
-import { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server';
+import type { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server';
import Boom from '@hapi/boom';
import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
-import { AgentSOAttributes } from '../../types';
+import type { AgentSOAttributes } from '../../types';
+import { AgentReassignmentError } from '../../errors';
import { agentPolicyService } from '../agent_policy';
-import { getAgents, listAllAgents } from './crud';
+import { getAgentPolicyForAgent, getAgents, listAllAgents } from './crud';
import { createAgentAction, bulkCreateAgentActions } from './actions';
export async function reassignAgent(
@@ -19,11 +20,13 @@ export async function reassignAgent(
agentId: string,
newAgentPolicyId: string
) {
- const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId);
- if (!agentPolicy) {
+ const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId);
+ if (!newAgentPolicy) {
throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`);
}
+ await reassignAgentIsAllowed(soClient, esClient, agentId, newAgentPolicyId);
+
await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, {
policy_id: newAgentPolicyId,
policy_revision: null,
@@ -36,6 +39,29 @@ export async function reassignAgent(
});
}
+export async function reassignAgentIsAllowed(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ agentId: string,
+ newAgentPolicyId: string
+) {
+ const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId);
+ if (agentPolicy?.is_managed) {
+ throw new AgentReassignmentError(
+ `Cannot reassign an agent from managed agent policy ${agentPolicy.id}`
+ );
+ }
+
+ const newAgentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId);
+ if (newAgentPolicy?.is_managed) {
+ throw new AgentReassignmentError(
+ `Cannot reassign an agent to managed agent policy ${newAgentPolicy.id}`
+ );
+ }
+
+ return true;
+}
+
export async function reassignAgents(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
@@ -63,7 +89,15 @@ export async function reassignAgents(
showInactive: false,
})
).agents;
- const agentsToUpdate = agents.filter((agent) => agent.policy_id !== newAgentPolicyId);
+ // And which are allowed to unenroll
+ const settled = await Promise.allSettled(
+ agents.map((agent) =>
+ reassignAgentIsAllowed(soClient, esClient, agent.id, newAgentPolicyId).then((_) => agent)
+ )
+ );
+ const agentsToUpdate = agents.filter(
+ (agent, index) => settled[index].status === 'fulfilled' && agent.policy_id !== newAgentPolicyId
+ );
// Update the necessary agents
const res = await soClient.bulkUpdate(
diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
new file mode 100644
index 0000000000000..b8c1b7befb443
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/agents/unenroll.test.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
+import type { SavedObject } from 'kibana/server';
+import type { Agent, AgentPolicy } from '../../types';
+import { AgentUnenrollmentError } from '../../errors';
+import { unenrollAgent, unenrollAgents } from './unenroll';
+
+const agentInManagedSO = {
+ id: 'agent-in-managed-policy',
+ attributes: { policy_id: 'managed-agent-policy' },
+} as SavedObject;
+const agentInUnmanagedSO = {
+ id: 'agent-in-unmanaged-policy',
+ attributes: { policy_id: 'unmanaged-agent-policy' },
+} as SavedObject;
+const agentInUnmanagedSO2 = {
+ id: 'agent-in-unmanaged-policy2',
+ attributes: { policy_id: 'unmanaged-agent-policy' },
+} as SavedObject;
+const unmanagedAgentPolicySO = {
+ id: 'unmanaged-agent-policy',
+ attributes: { is_managed: false },
+} as SavedObject;
+const managedAgentPolicySO = {
+ id: 'managed-agent-policy',
+ attributes: { is_managed: true },
+} as SavedObject;
+
+describe('unenrollAgent (singular)', () => {
+ it('can unenroll from unmanaged policy', async () => {
+ const soClient = createClientMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ await unenrollAgent(soClient, esClient, agentInUnmanagedSO.id);
+
+ // calls ES update with correct values
+ expect(soClient.update).toBeCalledTimes(1);
+ const calledWith = soClient.update.mock.calls[0];
+ expect(calledWith[1]).toBe(agentInUnmanagedSO.id);
+ expect(calledWith[2]).toHaveProperty('unenrollment_started_at');
+ });
+
+ it('cannot unenroll from managed policy', async () => {
+ const soClient = createClientMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ await expect(unenrollAgent(soClient, esClient, agentInManagedSO.id)).rejects.toThrowError(
+ AgentUnenrollmentError
+ );
+ // does not call ES update
+ expect(soClient.update).toBeCalledTimes(0);
+ });
+});
+
+describe('unenrollAgents (plural)', () => {
+ it('can unenroll from an unmanaged policy', async () => {
+ const soClient = createClientMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ const idsToUnenroll = [agentInUnmanagedSO.id, agentInUnmanagedSO2.id];
+ await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll });
+
+ // calls ES update with correct values
+ const calledWith = soClient.bulkUpdate.mock.calls[0][0];
+ expect(calledWith.length).toBe(idsToUnenroll.length);
+ expect(calledWith.map(({ id }) => id)).toEqual(idsToUnenroll);
+ for (const params of calledWith) {
+ expect(params.attributes).toHaveProperty('unenrollment_started_at');
+ }
+ });
+ it('cannot unenroll from a managed policy', async () => {
+ const soClient = createClientMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+ const idsToUnenroll = [agentInUnmanagedSO.id, agentInManagedSO.id, agentInUnmanagedSO2.id];
+ await unenrollAgents(soClient, esClient, { agentIds: idsToUnenroll });
+
+ // calls ES update with correct values
+ const calledWith = soClient.bulkUpdate.mock.calls[0][0];
+ const onlyUnmanaged = [agentInUnmanagedSO.id, agentInUnmanagedSO2.id];
+ expect(calledWith.length).toBe(onlyUnmanaged.length);
+ expect(calledWith.map(({ id }) => id)).toEqual(onlyUnmanaged);
+ for (const params of calledWith) {
+ expect(params.attributes).toHaveProperty('unenrollment_started_at');
+ }
+ });
+});
+
+function createClientMock() {
+ const soClientMock = savedObjectsClientMock.create();
+
+ // need to mock .create & bulkCreate due to (bulk)createAgentAction(s) in unenrollAgent(s)
+ soClientMock.create.mockResolvedValue(agentInUnmanagedSO);
+ soClientMock.bulkCreate.mockImplementation(async ([{ type, attributes }]) => {
+ return {
+ saved_objects: [await soClientMock.create(type, attributes)],
+ };
+ });
+
+ soClientMock.get.mockImplementation(async (_, id) => {
+ switch (id) {
+ case unmanagedAgentPolicySO.id:
+ return unmanagedAgentPolicySO;
+ case managedAgentPolicySO.id:
+ return managedAgentPolicySO;
+ case agentInManagedSO.id:
+ return agentInManagedSO;
+ case agentInUnmanagedSO2.id:
+ return agentInUnmanagedSO2;
+ case agentInUnmanagedSO.id:
+ default:
+ return agentInUnmanagedSO;
+ }
+ });
+
+ soClientMock.bulkGet.mockImplementation(async (options) => {
+ return {
+ saved_objects: await Promise.all(options!.map(({ type, id }) => soClientMock.get(type, id))),
+ };
+ });
+
+ return soClientMock;
+}
diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts
index a20b742d1425e..e2fa83cf32b63 100644
--- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts
+++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts
@@ -4,16 +4,36 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
-import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
-import { AgentSOAttributes } from '../../types';
+import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
+import type { AgentSOAttributes } from '../../types';
+import { AgentUnenrollmentError } from '../../errors';
import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
-import { getAgent } from './crud';
import * as APIKeyService from '../api_keys';
import { createAgentAction, bulkCreateAgentActions } from './actions';
-import { getAgents, listAllAgents } from './crud';
+import { getAgent, getAgentPolicyForAgent, getAgents, listAllAgents } from './crud';
+
+async function unenrollAgentIsAllowed(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ agentId: string
+) {
+ const agentPolicy = await getAgentPolicyForAgent(soClient, esClient, agentId);
+ if (agentPolicy?.is_managed) {
+ throw new AgentUnenrollmentError(
+ `Cannot unenroll ${agentId} from a managed agent policy ${agentPolicy.id}`
+ );
+ }
+
+ return true;
+}
+
+export async function unenrollAgent(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ agentId: string
+) {
+ await unenrollAgentIsAllowed(soClient, esClient, agentId);
-export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) {
const now = new Date().toISOString();
await createAgentAction(soClient, {
agent_id: agentId,
@@ -36,7 +56,6 @@ export async function unenrollAgents(
kuery: string;
}
) {
- // Filter to agents that do not already unenrolled, or unenrolling
const agents =
'agentIds' in options
? await getAgents(soClient, options.agentIds)
@@ -46,9 +65,19 @@ export async function unenrollAgents(
showInactive: false,
})
).agents;
- const agentsToUpdate = agents.filter(
+
+ // Filter to agents that are not already unenrolled, or unenrolling
+ const agentsEnrolled = agents.filter(
(agent) => !agent.unenrollment_started_at && !agent.unenrolled_at
);
+ // And which are allowed to unenroll
+ const settled = await Promise.allSettled(
+ agentsEnrolled.map((agent) =>
+ unenrollAgentIsAllowed(soClient, esClient, agent.id).then((_) => agent)
+ )
+ );
+ const agentsToUpdate = agentsEnrolled.filter((_, index) => settled[index].status === 'fulfilled');
+
const now = new Date().toISOString();
// Create unenroll action for each agent
diff --git a/x-pack/plugins/fleet/server/services/agents/update.ts b/x-pack/plugins/fleet/server/services/agents/update.ts
index f6b4b44004761..21087be392bcd 100644
--- a/x-pack/plugins/fleet/server/services/agents/update.ts
+++ b/x-pack/plugins/fleet/server/services/agents/update.ts
@@ -29,7 +29,7 @@ export async function unenrollForAgentPolicyId(
hasMore = false;
}
for (const agent of agents) {
- await unenrollAgent(soClient, agent.id);
+ await unenrollAgent(soClient, esClient, agent.id);
}
}
}
diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
index 209bfb4b7398a..5891320c2544b 100644
--- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
@@ -13,6 +13,7 @@ const AgentPolicyBaseSchema = {
name: schema.string({ minLength: 1 }),
namespace: NamespaceSchema,
description: schema.maybe(schema.string()),
+ is_managed: schema.maybe(schema.boolean()),
monitoring_enabled: schema.maybe(
schema.arrayOf(
schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)])
@@ -27,6 +28,7 @@ export const NewAgentPolicySchema = schema.object({
export const AgentPolicySchema = schema.object({
...AgentPolicyBaseSchema,
id: schema.string(),
+ is_managed: schema.boolean(),
status: schema.oneOf([
schema.literal(agentPolicyStatuses.Active),
schema.literal(agentPolicyStatuses.Inactive),
diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
index d4bae9d88d262..ba64814cd1daf 100644
--- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts
@@ -1277,6 +1277,7 @@ export class EndpointDocGenerator {
status: agentPolicyStatuses.Active,
description: 'Some description',
namespace: 'default',
+ is_managed: false,
monitoring_enabled: ['logs', 'metrics'],
revision: 2,
updated_at: '2020-07-22T16:36:49.196Z',
diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
index 0d0749aa8e913..9f016ab044a90 100644
--- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
+++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts
@@ -26,8 +26,10 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('POST /api/fleet/agent_policies', () => {
- it('should work with valid values', async () => {
- await supertest
+ it('should work with valid minimum required values', async () => {
+ const {
+ body: { item: createdPolicy },
+ } = await supertest
.post(`/api/fleet/agent_policies`)
.set('kbn-xsrf', 'xxxx')
.send({
@@ -35,6 +37,28 @@ export default function ({ getService }: FtrProviderContext) {
namespace: 'default',
})
.expect(200);
+
+ const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`);
+ const json = getRes.body;
+ expect(json.item.is_managed).to.equal(false);
+ });
+
+ it('sets given is_managed value', async () => {
+ const {
+ body: { item: createdPolicy },
+ } = await supertest
+ .post(`/api/fleet/agent_policies`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'TEST2',
+ namespace: 'default',
+ is_managed: true,
+ })
+ .expect(200);
+
+ const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`);
+ const json = getRes.body;
+ expect(json.item.is_managed).to.equal(true);
});
it('should return a 400 with an empty namespace', async () => {
@@ -108,6 +132,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(newPolicy).to.eql({
name: 'Copied policy',
description: 'Test',
+ is_managed: false,
namespace: 'default',
monitoring_enabled: ['logs', 'metrics'],
revision: 1,
@@ -161,6 +186,7 @@ export default function ({ getService }: FtrProviderContext) {
});
describe('PUT /api/fleet/agent_policies/{agentPolicyId}', () => {
+ let agentPolicyId: undefined | string;
it('should work with valid values', async () => {
const {
body: { item: originalPolicy },
@@ -173,11 +199,11 @@ export default function ({ getService }: FtrProviderContext) {
namespace: 'default',
})
.expect(200);
-
+ agentPolicyId = originalPolicy.id;
const {
body: { item: updatedPolicy },
} = await supertest
- .put(`/api/fleet/agent_policies/${originalPolicy.id}`)
+ .put(`/api/fleet/agent_policies/${agentPolicyId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Updated name',
@@ -193,12 +219,31 @@ export default function ({ getService }: FtrProviderContext) {
name: 'Updated name',
description: 'Updated description',
namespace: 'default',
+ is_managed: false,
revision: 2,
updated_by: 'elastic',
package_policies: [],
});
});
+ it('sets given is_managed value', async () => {
+ const {
+ body: { item: createdPolicy },
+ } = await supertest
+ .put(`/api/fleet/agent_policies/${agentPolicyId}`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'TEST2',
+ namespace: 'default',
+ is_managed: true,
+ })
+ .expect(200);
+
+ const getRes = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`);
+ const json = getRes.body;
+ expect(json.item.is_managed).to.equal(true);
+ });
+
it('should return a 409 if policy already exists with name given', async () => {
const sharedBody = {
name: 'Initial name',
diff --git a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts
index e17e779e4217b..a31fa862f7420 100644
--- a/x-pack/test/fleet_api_integration/apis/agents/reassign.ts
+++ b/x-pack/test/fleet_api_integration/apis/agents/reassign.ts
@@ -16,10 +16,10 @@ export default function (providerContext: FtrProviderContext) {
describe('fleet_reassign_agent', () => {
setupFleetAndAgents(providerContext);
- before(async () => {
+ beforeEach(async () => {
await esArchiver.loadIfNeeded('fleet/agents');
});
- after(async () => {
+ afterEach(async () => {
await esArchiver.unload('fleet/agents');
});
@@ -31,7 +31,7 @@ export default function (providerContext: FtrProviderContext) {
policy_id: 'policy2',
})
.expect(200);
- const { body } = await supertest.get(`/api/fleet/agents/agent1`).set('kbn-xsrf', 'xxx');
+ const { body } = await supertest.get(`/api/fleet/agents/agent1`);
expect(body.item.policy_id).to.eql('policy2');
});
@@ -88,5 +88,34 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(404);
});
+
+ it('can reassign from unmanaged policy to unmanaged', async () => {
+ // policy2 is not managed
+ // reassign succeeds
+ await supertest
+ .put(`/api/fleet/agents/agent1/reassign`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ policy_id: 'policy2',
+ })
+ .expect(200);
+ });
+ it('cannot reassign from unmanaged policy to managed', async () => {
+ // agent1 is enrolled in policy1. set policy1 to managed
+ await supertest
+ .put(`/api/fleet/agent_policies/policy1`)
+ .set('kbn-xsrf', 'xxx')
+ .send({ name: 'Test policy', namespace: 'default', is_managed: true })
+ .expect(200);
+
+ // reassign fails
+ await supertest
+ .put(`/api/fleet/agents/agent1/reassign`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ policy_id: 'policy2',
+ })
+ .expect(400);
+ });
});
}
diff --git a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts
index 3cafc86602d3b..85bcce824dd51 100644
--- a/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts
+++ b/x-pack/test/fleet_api_integration/apis/agents/unenroll.ts
@@ -65,17 +65,28 @@ export default function (providerContext: FtrProviderContext) {
await esArchiver.unload('fleet/agents');
});
- it('should allow to unenroll single agent', async () => {
+ it('/agents/{agent_id}/unenroll should fail for managed policy', async () => {
+ // set policy to managed
await supertest
- .post(`/api/fleet/agents/agent1/unenroll`)
+ .put(`/api/fleet/agent_policies/policy1`)
.set('kbn-xsrf', 'xxx')
- .send({
- force: true,
- })
+ .send({ name: 'Test policy', namespace: 'default', is_managed: true })
+ .expect(200);
+
+ await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').expect(400);
+ });
+
+ it('/agents/{agent_id}/unenroll should allow from unmanaged policy', async () => {
+ // set policy to unmanaged
+ await supertest
+ .put(`/api/fleet/agent_policies/policy1`)
+ .set('kbn-xsrf', 'xxx')
+ .send({ name: 'Test policy', namespace: 'default', is_managed: false })
.expect(200);
+ await supertest.post(`/api/fleet/agents/agent1/unenroll`).set('kbn-xsrf', 'xxx').expect(200);
});
- it('should invalidate related API keys', async () => {
+ it('/agents/{agent_id}/unenroll { force: true } should invalidate related API keys', async () => {
await supertest
.post(`/api/fleet/agents/agent1/unenroll`)
.set('kbn-xsrf', 'xxx')
@@ -97,7 +108,44 @@ export default function (providerContext: FtrProviderContext) {
expect(outputAPIKeys[0].invalidated).eql(true);
});
- it('should allow to unenroll multiple agents by id', async () => {
+ it('/agents/{agent_id}/bulk_unenroll should not allow unenroll from managed policy', async () => {
+ // set policy to managed
+ await supertest
+ .put(`/api/fleet/agent_policies/policy1`)
+ .set('kbn-xsrf', 'xxx')
+ .send({ name: 'Test policy', namespace: 'default', is_managed: true })
+ .expect(200);
+
+ // try to unenroll
+ await supertest
+ .post(`/api/fleet/agents/bulk_unenroll`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ agents: ['agent2', 'agent3'],
+ })
+ // http request succeeds
+ .expect(200);
+
+ // but agents are still enrolled
+ const [agent2data, agent3data] = await Promise.all([
+ supertest.get(`/api/fleet/agents/agent2`),
+ supertest.get(`/api/fleet/agents/agent3`),
+ ]);
+ expect(typeof agent2data.body.item.unenrollment_started_at).to.eql('undefined');
+ expect(typeof agent2data.body.item.unenrolled_at).to.eql('undefined');
+ expect(agent2data.body.item.active).to.eql(true);
+ expect(typeof agent3data.body.item.unenrollment_started_at).to.be('undefined');
+ expect(typeof agent3data.body.item.unenrolled_at).to.be('undefined');
+ expect(agent2data.body.item.active).to.eql(true);
+ });
+
+ it('/agents/{agent_id}/bulk_unenroll should allow to unenroll multiple agents by id from an unmanaged policy', async () => {
+ // set policy to unmanaged
+ await supertest
+ .put(`/api/fleet/agent_policies/policy1`)
+ .set('kbn-xsrf', 'xxx')
+ .send({ name: 'Test policy', namespace: 'default', is_managed: false })
+ .expect(200);
await supertest
.post(`/api/fleet/agents/bulk_unenroll`)
.set('kbn-xsrf', 'xxx')
@@ -106,8 +154,8 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(200);
const [agent2data, agent3data] = await Promise.all([
- supertest.get(`/api/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'),
- supertest.get(`/api/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'),
+ supertest.get(`/api/fleet/agents/agent2`),
+ supertest.get(`/api/fleet/agents/agent3`),
]);
expect(typeof agent2data.body.item.unenrollment_started_at).to.eql('string');
expect(agent2data.body.item.active).to.eql(true);
@@ -115,7 +163,7 @@ export default function (providerContext: FtrProviderContext) {
expect(agent2data.body.item.active).to.eql(true);
});
- it('should allow to unenroll multiple agents by kuery', async () => {
+ it('/agents/{agent_id}/bulk_unenroll should allow to unenroll multiple agents by kuery', async () => {
await supertest
.post(`/api/fleet/agents/bulk_unenroll`)
.set('kbn-xsrf', 'xxx')
@@ -125,7 +173,7 @@ export default function (providerContext: FtrProviderContext) {
})
.expect(200);
- const { body } = await supertest.get(`/api/fleet/agents`).set('kbn-xsrf', 'xxx');
+ const { body } = await supertest.get(`/api/fleet/agents`);
expect(body.total).to.eql(0);
});
});
From 808bd44463c0e7a2a6c1a8ee3c6f32a3477bab8f Mon Sep 17 00:00:00 2001
From: Lisa Cawley
Date: Thu, 4 Feb 2021 12:49:46 -0800
Subject: [PATCH 16/42] Use doc link services in index pattern management
(#89937)
---
src/core/public/doc_links/doc_links_service.ts | 1 +
.../components/edit_index_pattern/edit_index_pattern.tsx | 9 +++------
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 4cb2969a63908..da35373f57322 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -121,6 +121,7 @@ export class DocLinksService {
addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`,
kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`,
elasticsearch: {
+ mapping: `${ELASTICSEARCH_DOCS}mapping.html`,
remoteClusters: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html`,
remoteClustersProxy: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#proxy-mode`,
remoteClusersProxySettings: `${ELASTICSEARCH_DOCS}modules-remote-clusters.html#remote-cluster-proxy-settings`,
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
index 4f34bc6aa73b4..4dff5f1e0b598 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx
@@ -149,7 +149,8 @@ export const EditIndexPattern = withRouter(
chrome.docTitle.change(indexPattern.title);
const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0));
-
+ const kibana = useKibana();
+ const docsUrl = kibana.services.docLinks!.links.elasticsearch.mapping;
return (
@@ -182,11 +183,7 @@ export const EditIndexPattern = withRouter(
defaultMessage="This page lists every field in the {indexPatternTitle} index and the field's associated core type as recorded by Elasticsearch. To change a field type, use the Elasticsearch"
values={{ indexPatternTitle:
{indexPattern.title} }}
/>{' '}
-
+
{mappingAPILink}
From 35fd85b8fa2350294452489457fed05b526382a4 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 4 Feb 2021 14:16:30 -0800
Subject: [PATCH 17/42] Elastic Maps Server config is `host` not `hostname`
(#90234)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
docs/maps/connect-to-ems.asciidoc | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc
index a5b8010f21f97..8e4695bfc6662 100644
--- a/docs/maps/connect-to-ems.asciidoc
+++ b/docs/maps/connect-to-ems.asciidoc
@@ -86,7 +86,7 @@ endif::[]
[cols="2*<"]
|===
-| [[ems-hostname]]`hostname`
+| [[ems-host]]`host`
| Specifies the host of the backend server. To allow remote users to connect, set the value to the IP address or DNS name of the {hosted-ems} container. *Default: _your-hostname_*. <>.
| `port`
@@ -199,7 +199,7 @@ TIP: The available basemaps and boundaries can be explored from the `/maps` endp
[[elastic-maps-server-kibana]]
==== Kibana configuration
-With {hosted-ems} running, add the `map.emsUrl` configuration key in your <> file pointing to the root of the service. This setting will point {kib} to request EMS basemaps and boundaries from {hosted-ems}. Typically this will be the URL to the <> of {hosted-ems}. For example, `map.emsUrl: https://my-ems-server:8080`.
+With {hosted-ems} running, add the `map.emsUrl` configuration key in your <> file pointing to the root of the service. This setting will point {kib} to request EMS basemaps and boundaries from {hosted-ems}. Typically this will be the URL to the <> of {hosted-ems}. For example, `map.emsUrl: https://my-ems-server:8080`.
[float]
From bb3ed33ccc899802f82f0a02eea41b93642fde77 Mon Sep 17 00:00:00 2001
From: Stacey Gammon
Date: Thu, 4 Feb 2021 17:22:22 -0500
Subject: [PATCH 18/42] RFC for automatically generated typescript API
documentation for every plugins public services, types, and functionality
(#86704)
* wip RFC for API doc infra
* update
* update
* rfc
* rfc
* Update RFC
* Update RFC post Arch Review
* add pr link
* Update based on review feedback
* Update 0014_api_documentation.md
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
rfcs/images/api_doc_pick.png | Bin 0 -> 82547 bytes
rfcs/images/api_doc_tech.png | Bin 0 -> 4826 bytes
rfcs/images/api_doc_tech_compare.png | Bin 0 -> 21047 bytes
rfcs/images/api_docs.png | Bin 0 -> 465680 bytes
rfcs/images/api_docs_package_current.png | Bin 0 -> 48116 bytes
rfcs/images/api_info.png | Bin 0 -> 117893 bytes
rfcs/images/current_api_doc_links.png | Bin 0 -> 41607 bytes
rfcs/images/new_api_docs_with_links.png | Bin 0 -> 72543 bytes
rfcs/images/repeat_primitive_signature.png | Bin 0 -> 58109 bytes
rfcs/images/repeat_type_links.png | Bin 0 -> 51818 bytes
rfcs/text/0014_api_documentation.md | 442 +++++++++++++++++++++
11 files changed, 442 insertions(+)
create mode 100644 rfcs/images/api_doc_pick.png
create mode 100644 rfcs/images/api_doc_tech.png
create mode 100644 rfcs/images/api_doc_tech_compare.png
create mode 100644 rfcs/images/api_docs.png
create mode 100644 rfcs/images/api_docs_package_current.png
create mode 100644 rfcs/images/api_info.png
create mode 100644 rfcs/images/current_api_doc_links.png
create mode 100644 rfcs/images/new_api_docs_with_links.png
create mode 100644 rfcs/images/repeat_primitive_signature.png
create mode 100644 rfcs/images/repeat_type_links.png
create mode 100644 rfcs/text/0014_api_documentation.md
diff --git a/rfcs/images/api_doc_pick.png b/rfcs/images/api_doc_pick.png
new file mode 100644
index 0000000000000000000000000000000000000000..825fa47b266cb9ca39eed58d0bcbd939a3fbe981
GIT binary patch
literal 82547
zcmeFZgw(jX;_bcZ4>9YZ6+fHcz00SQIA8;PM|VCY6snn7~t4q@nS
zzOB!B-silJ=e+;G_nYh5v)8P>SKaGgci;0tRaq7vmjV|J4Gmvj?u9xU+U+JZv|Fop
zZUZ%R8JzvV4{1v&DOGtXDOy!Wdvi-02pSqowCSr?_vN3kbeWjEdezm>&Wh{krvCnY
znEETfp0=*GFKz9gdfU>I3=HPU?#*LpenHDo`&?@+IV$O(OKLWD5c|@td1QaJ5OIID
ztLX|tpXd3oEj~ss4KMcw?X8~UGc_Cx3{%{wQUfD(wAXHE6ZF!OvbQFjBsZP!cwxLx
z$26h2Rg8n%UCySUnYL|dabqdx-YYY^HxzPh^||k2
zMndo3+e$N5)s0KeP|Kdbg9p{b8fY>TqrzgqV34|fTQx~a)348(ZKV2{h5xr*XB_}&&AS0{cd&o#a_a%LgQt@p{1yNvsbGpC3%jC^X
zW;;3AI3GFLwb9MZ&B-zF#fg#P_tAf$O#p3|&Tw+DJ1!f5YbZoV-dsrujSVQ@LBqU7
zfrbT?ZULA0Ez18X%iLl?!}wK?j)oRuiH7;-JIcWQ=O+rdezy779U~?f4IB7%AGq8<
zq5u2s+fAP^{$0Mc3Oqx5rXeLS58O3O9U%}qr#JS_hgLYkKn0G2oURiZ8VS?S>z2Ga
z<1WxY(o$2$Sx4!mu&KQ*r-_;UYY3;it;5fL&_vyZfub$M*@V{J*2d0B*j
z`DZnVp7z%(&eme|I!davQudAzT0Tx*&ZqR^xU{siqK;8u^RIbA+%5kzlAY6^%K{b%`uPOJ#rYKUKYasDMSs=`t6I84
zY;;~&+5#{G#t`Qc;N}(m)!?^B{~7XMO|_gLj#BovKuc%w|3LlE#(#bI-wl6_sr#QX
zc?J3ZI^u~UhJnoaPH6v#JYufPZI4Pzx#5K|159;sq{Es&F@Xm*c
zQ7kwgx-F~b?*Su5d7+zReZEgblHw)M7XMF8Md>BySC*cA?O>+RG7K~ncOMT&L-%+=
z%gCsc{RO9aziVlp|L@mIn}~U*si{q4Vg)P65E#w^tKhJwGBTJ_>Te5IX?vA1Y+a|XSp@~hFF{{VNyi#84Db4bUAu=s
z=y!6Nqe#&~>FuiRoL8@ni*>7tg0TD#3?+-Hsy7DDXTA#^=={?tm_`(iOibj)56N%N
zC>_sU5ACuuGA?iW`QOkdwDl&6UJyyrHexx7><)nrm+r^(m45^sR$f>R-_*0b!umpF
zLWgJX&M7XrP5v8L_Nygpu#G-^_z)8nm9+Ay_ki`yE-?uSTUJ(9MhLu0X+EAwTv-Oq
z|H++Y*LjFfGL0qPgbV~sLd}@Js(-8WP}mmltdKY=@XOgT*Fnc`^Phi2+%?vw_^vE4
zFwh2ybN4RKJD8+lU>mz?%;7sUbaLXZtu|CHS4b9On)rQv8~6FFWn^1D{U<->A9zNC
zw~}~v3_J#=nSVzg2hKZY`t#6u!@a!;N>|;sg$0va#fS8}a7~N@VGI2@Z@8!&KTTL{
zbEk5$lw>^XLnaP3nXX-SWz1Fj!vINfB4FU&^EnB0nD@T_qOI9Sv{vScFADj;LYCQR!EqpQ4}S6EZGE;mt4HOq<%c_aJINbf
zO?$DlXIpbCMsvz5>hTckn*gIoa*aFPH!WdNtu@=R=PQamy;Se;GTmXe2fYY;q;zjv
zCV@PBPh_}S7l+(i{u&=cr31USDLoo)<@f$kxEKS}^hHX7VoV3*^|vIFwI-uGO$b!y
zK&pf+W=ZRVJHZ3d*d4a?|4sDTJQ$)l{1dfZErYc>g4;2
zB!{AtH9<{7d$}_!Fn!#vc>7C;z42T@g}0Z*>|JMJU$VzM+D|HX4p#K_6fzU8vA8nU
zCezalEmiAlp6smPVx@N)8;~(vbyjzibt;D8kzMxVo#*9eva7pL4NUTt+jZ
zV@1;Im5Fn4o2MfC8S;~^+Wy$|=>5c~b0^Jg2TO7vC9U-_!J~?o2HR<(L2alsZ=2~u
zkH@dB(k16zGQ`|xIe$Z&We@*9uz#M|`=p6ZqlO9P+8)v`0u>z`#)_JNeJt_iIp5&e
zWh2p0KPKd0pnJwTswPC_?$g9AL!I8x-Ex&hRn%+B!A-(gNs+4)M&3Vu3X@LG8Lt$EMt?wsjzGL3;h
zc&W(6tee53_Ar0S4a;l;&T;IOjI2}-Wy=@bSCFyX?zsd>{A9~Ovi~a
zfA9b@k-)d!5^tBtmsl%|b=@rB%OGEK<<{2ZJD3yzuQ2v^-eJn8yyw1BC5Y@T`2#Uf
z@BNKUx6(X4P1r?&Ee-^-oici?rdGDG(s!JW9H_ijrrqs9sf1qqM%4WelxXMu!Ox#(
z{~$G~uLukZlGjN(DWdb+={%?JojcChYQi#_G#{ekfL=`W#HZb9nl+$c5EFL^lEDzE
zidvh5(r63K4Y;B1c5UrkS!*w8f;5zT{R+zCKuOn?RKm%u4^Ry%ftGn>4y3B
zEhzV-s-)e_P2oBX-Y4GqPJH$20bw&Hv516j2HINea7ThMXW2CytTkYU^7n7hA_<6;h%BkekyIooj42e}IxN=Qs(Lv3+
zrF}vUY%dgKFKF7P4=n?|D@TaYAT@fHCm)`&lE(VBB}`Zl7aX7yxvw{;Dc~j4eWhik
zNET_8@ZaAZQwdL0?#^pY-IB_jK|a5B%VPh&kN0(Q%$B8c8@~j@Ke2zu>D&2StE5k~
z-@aza#t2sYfHjg|#ZR!~C(#}8i-oLYJb+K_vVp7K8>q6$+%`ygLK^Sd
znsI<0q7u?oE))%SMzXe_B{5R?m+B$eDLDoVq8YV9b2AiC^&VW@78(k~$fB8>dM%df
z0yQwMbi)9%z{)CZ?M5AIuj%rzpEuaw#|<3wA~+Avf4vF3b{=7B!fj^~_+*NTo${C}
zE+AS>z1GIWQiDU&lV5SGdbl91(Jy6iZNe_3mXIf)kCA-e`u&rMw{ps9;vB?ymBpMa
zEDU9Wbf2FgitEQlt-tO>@1GDH3EKra
zpX~$vAY|E$7i5J`hyd_sAGuPHGDI#uTgF}MH5$k)+H#5
zHS1JYWKWh{UF_FZ%_0&T^7fT4FkDBo*+SY42>(E+ozW{nRbfrQ=nlm4hko5UD+x-R~}Xx!yHz
zD%J{*kKZNoC)UDLsP0?s9`3Fc_nA;`bx^ZQ!E9OE!_m6xoiCrhG%wLvosM5U54~2(
z^<@SBaNQA5+rRdnJMl6z!(e^1fcsojrItiR(nwY7yv$3o&
zvoo0;+z=!==CYw(!o==L>9pt(XtcxmF*?KeyrnE-Zu-upuOIC=M>)H&P2?jXm+zZK
znKv7^@K*vBISIvf&sw?Q7fBOePYo#jOJv^&>>|m?FDiEiPN+J*Cu`UVa}Y($eTZSW
ziN%mTfjXxk2I#RudlwcUTiW)N?E&J;{7>E}n(C+`*CgM~#z@^%Oj&b7J^(X|{}Jo8vY1&u8@8)Fc=+w!5qit5SS4G+|kE~`YZb}cVUIaVr2%Gwrp#G;2B
zrzMQu-BA%85P~}L?4J8dRTY@q`@QH}i0%6DMx1_7>`a>}bnWnshpF=U
z&e`n8)ZREs!_VeMI8CCn5mR|ib;FnU-Pga@_;DIN7IEE9NUmH<-Z_~po|0Q1Vq$v2
ztEpFOL*E&h83pYHr*={pYpUROylAY~q5POw5>9wP4p@=z&nf$>1eVhZ1dn~>{fov4UoiO>1nfk<8?yH*KpwKWoc
zg6fG#Z!~K^4~N@c5IFsi_OUP3<5#J@zge!9)%XL}+Mh+4lbyZ%<*v12KgDPm?lw#9
zbvmU*nBUuj0SR`q;XDq<(=N3_ort
z%t2?@UBgmK9D2TP=3U-56TKfapIAK5wOn83@kN-@xX9J^;pVaUMto=@E|#H_S*g%H
z6TIlPb9(3Fa}2u!&a709ao5-gLAKu7vT7LLdPIVHeR=VH4&KwSD%b`t
zQ`Lgh9BKKKz2H6O)~m+pS>@s{z54Hmul7lXat)%qTltMzj8}_9*#QBXfPmfhv<%%p-s*xVHqx~v#08rncYA!rF4MX=N1G2NkIdt8v~Q2K8`T}#8@-X1YhE*S`m%m~
zL$2j#TNKjM<6~hql*VLtRYp0kV@0e|m^5U9bCUk@?ByIkB;B@z7%bsz|Jv}2_b46X
zk&vz;Eqhbd0CpH#yEgH=6{M1UVG)s^M_s*v?pEA#Vw-g)QMu^Zh!Y8P%398${AI>r
z02kvd(K^qr?!maNfH6DLq(6IR%fUDmO)JkObtQSsW1srEl;kuEmOlOffN%*?&T59VU=
zDQgwh^kK1z;jj3bnM&J=kC?Vrmb*2~qej;DG92eH2=h0#BM`Mce=uJpNgZu`?!3Ue
zhu_o00ca7NOGb_!ot?~GQTf5WARdc+E2De_*8X7>v-H}uBj>x@5)8wB=ijD}uD$FP
zkY0^bAv<+T{Ohw@;`P~Ee8=Jg@m>wnbXVuc{B9_RkO#BwiY@2o`UB>xu5;qexhOF=
zPqE|eWy0lAkcj#nw`$dm9m~kvHhvFxx#q|`A2?>T&%kA7e%JYGbVuU
z7X&uP5+eK?sgwE^I*1UA`uHSr#@F8SZd0(c4Qr0C0{QIA5T%O(MVI<*B{11a$%tp$
z9h0eflIM&6z(<__T8W7TBZ-?l;-1PhtEnJX4DD`?iDH>=QWW?)S
z1RFC#adW-9#WrinlB)+H`!j~0)t|6I2r4m`n5@3*iO8|)mo#lsYmT%qul4k2N_SN;
zl}UDxKNp^YR?qGpz0j@bC%!M-PQT_s5jP_|0nsQnz}G!`lN*@c&+0N;+xtjM2T@-x
ziUBhpMfDhq8JLn#hu=*YY8RMl7x5(6LwXO9>o&1M>1B7;TZu^JKd^vb?H;C)B{wX7
z@@b5<=b-24X%ugiun9{jqp?qrV!p1N5(M)`J(?+OaJJv{^!2<9R{63bBI__Qbtl|-
z>xZWuP07=@bVE~lz-yw8JNiS
z#KFn*dDvQF8!Aby`Dh`3xDOX5=5>&ZhG5^JGr420cMGuwQGLKv(AH3Bo^@)_g_i}?%FQTa0C3-K2E*7_mz
zBXl^W-zHjT6ZvElsgZ5PvL_cS`|QGc{Tj{38xw{NV&hte=O<#j?{#eO@!t%Pi)IfDx2h+QFy;X~w+g=UbHMUb?sgqO0VH?e`*2}Jf^N&?0
z5XiT45&t%J_RzGCzsstaMt`*RwvPaMx@qlP>MT0!n@?(wORU?3mXTYv&10II^`a5S
z+eW0V9Pn3HY!{yTnO0_+vi0btrZU9f1v>W4CB9y*#3P^VO@ko!K`n
zwxyFoTn!QWO!2`_U4c}C4ndj%e@hjy$5!pA>uHYE
zE)|XLXRGbwlvsbjj5Ek>9?ZVG|0qw*z65b9sM~1$&L=Xzb3}kWAHY;f3V~?*ZHHP1
zPb=&X1IXHt7n^%hRZVA&s7VUdndyR}szQ<{R7IVWCRsivAa+3AA{%vD;I>`A-8k7(
zIq|W!D#n+?P>OYof|Q-o)gGR@z8f>RP_|LWV-YZou5Mi>dJ$O3UQg~W=BL5FJ~w%k
zA-dLFxq8zB@=t`o;=%7S<#C>twDkz*!CD9O8Dk|*zf2!}Dfp~jWcGH=gj~?Ms!WeQa`*L!+ecQtrjgUkSskh$8s5wwjqH;4E0m+FMRRYTWp$K?5Uo}`kSQPW
z+hq}g;Q8p3yfGo)(=_&S7sGGN@bic%G0fc8r4+lAXEo-B(jT^cmKw7E&QeK8#r?ML
z{jEQ7u;jql^KBK^h%wJ#PYKc}lhIh^3L&c5;LLgk=@8K7IjHUxBkT^1XT71>J27rm
z)X_mh|>T*k&oBY}SidknwI=5$^BK*~}o*Q$qQ%UgpOs`h8
zxiQNF_OUq}OrzwsrH6kbqEli4wta_E5M0#tqIsQTS8oVb_|otJTTWiSX4adhvCbV@
zmuKoN(@r6ffh@ySv9lsgA!Bq{*Ju4LRfV;ZNul!ha{~&LhzSy=D_>E%-bYgR
zJyEnUOd|mw3c9%`N{uOfmGnJDf(OjIR+lm{snTgg2|*#bfr=uLA@alElgZDe`LXA7
z-#;~p`sk*`eR%8BA~v<|8bhQG7!dUy-}or3mR=#ZPM$^jO_kOg7_&n+m=5K^&~$1=
z`@Kh@gH}5OI|-xO#u&N_E9CB$H;5ChdQHg0Wb3OI@*&l^OwM&IBgF2;xiu_KKWqWB
z^cy+%jN6H=<2MLLzZAmhb#~4QbS*eH1!g_kidqiZV>RjdqeX1_%OqdF6X|ue=Hupr
z3L%4qj{{cu_^D9&oOAIt%F4-&;#Pm;M^IP{
z`&4a(ipy<(|C2uD6Z8
zh3r&@cpnhMH9Z#UY^GZKTk?58xf%JZ0Od=|dKRy)Wu|yTHpSE&AJj+>?GLPWePv#a
zl;q~+9kOkOS!As(n*|}Yc0zjkv~F~2*%ZT{R;wisAJ>#QUvsOA-}Le!$M8&j3yQcw+26I02FYT@cjYw>r@e{`j)lAQ}
z5$+#UaAV&4v#0kD8^W~niflGW(~As^?lz^Fl=7Lao@p;Y3FX&1^x!s7a`A9Vkr7|J
zxy#*;FE2Qmk~|Ko2VLWV4QYUHF&*!|KmK3==B*Qct;x6kJy7^X7AK9bu$FH+&s+&E
z>f}vrCC7}hk&kKUlQWBW-V(p8!>y}7E>XI^~#`nB$6M?2H^o!Rl`Tz_%1
zfJZj3)yVf2M?ktkKt>l*Qu}L!A7&GL_)-6+b+VO{;+~-kMBizKT(J3S)q1GOpeO9)^A3@Apl+uEe9}o}Lz158oRq(%PsWt>WIyiIfnSx;*c7yOrGA`;#m7nyp~}
zX8i~Jo|(et#?EfSqw7k&-FjF*=gMTK9=
zH9mA)T1pungb1xsJNeF5`=z(DmqK4JysBNmD6K&Zgu;5{ic4FRubE}2PX(cD8nqWy
z_N*Y|t#c~jdz5TUAq`(-t#zK;UQ9QUkzbag3xA4fUUUX*mTCtlh|kyp&QQU_~U_k#dF<*!xjwLp3i_$Ewo6MrUf~jFOlYt
zygHfYOf+7+I`{;mxrR^_{a3<2NFJ;1W)UYH`8SJwc>%toX~6fhH{MIBCI3m%S~SfIitBFmm(W
zSX`Hw!L51c{EK0Gnu>kD&K)OCOrwmfctqk@RY~#Udb~6jx8o|w6lx8dJv2D7;UQIx
zY($#yo3ca}1HEf6Ht0~>w8`bJ!0rY&pMtpFkCfQ0=0<%5WN?{<`BCE7PnsS0jcW*X
z7wOd8?4faX$&b7cODAKL4z{Z(9!~qLtc?z9W_}L60kKL&u3yxZxt3qZf8>>m>F`=l1kS8gwq92=u94&;3(TX
z(jr0GN(t5mSc(n8xAcetlHgOJAPiEisqXehU
z^pKJ^i|jexRLX9Cc9=#q-lv64hj2fW=oWtw;W;*paG@4w9L;&CQ;>__DVLL+Y~>QK
zuZ}-*W7K*X(Wvpf8&!s)&B<`lI)}b(IP^WlIWyf3s7k4$I(Q^LwDsyXKVUUPC&sgw
zAL5n%Z?_M?gXE-WKlvx_HA8=-k=NHhWVsg)?bSCngVe-Up@X(6&vJR-?!#8K+v)zu
z5O&k$3_=5cnTn(s?qXS{`U2-Pp|t654{l-&vE3%uA{(;e@miManr%x7W9Q?HZTcJ+u7+Q{pa7g08w0B!
zX=LO_u_z%y3^J5>DRS@tm*S-S`{cuPsf}8@!DF9|r+?6Wetm4qCrm=~gn3b_M5%t?
z(6kPgPPw^Njgcz%`F-5ey!Q(RZM$RAQVDDWWzMMwR&b*784FAs3RlAmwemir=IQNx
z+nHc))NI^Z91y}0Lh|J4n{Xe)beQ$Z(zh9Lj0`E(>yexw=K^1Ke#+5Tqp}>;JM!SF
z#X|ey2df7o7u!N&3Xca|u38a<6^9&7a=pB?^s@)yVe#7Ud?PE_fI{@
z-C5^lGnHZiW9$b+H=oJ$owG|6joJBtQy*K2Ku4rARQiMtqf?2DqVY{KIW8o&G2GsMa23
zXyc0VH_Vwa#^DOQh9zyix_sI{%{>#$8huOaSMEpE^80|brq8@Rm>^EL+VFEhty6Cz
zh&c$p`?6HL^rwCO36at7u}%I>boaiuDbA^va6O8<{&+R56A%!<*wgt`>v}d4;MA>K
zVpD*?=~Mk!&n~s8Ym3)n9*WJG62$`8C|#^`jBE5q<;MI&nb>;9nD2U(z8M7V-o=}+
z0-x)w{rK*U)BR!fx`EibyGXXZRBHdMb#TZd^rCqJTTFY1Li{O@KUmcd>@fefP%{;Q
z#_!MLC=oB?O$sGm^f3ZCZm%x?0dI@DW_g?Bp}Xaa8nJ{rQG%
z8ShN%D~mXT%TRW*5ub*F!M*VubGc>RQD?xXVrTNYhkVvgSc=_tLDwVyqN&+@N{Vhv&Qg4eOn@-jJPvr-icT&;!XT8Cn
zpchq23qP_Q50_0unS{=hA*sL$dHUNi{izzh`6X?NkwPb+Gzmq~o4sF_`0hDY
z;`ne%fjEVDwv>Z3-GgZSUzQW89}-uK
zX(u*<+XH0l7uChw9S3#3XTAc3ob{ht`h332`5-v;h+MukcEjJ~ctJn8OnV-s39uCd0R>P#_mzU5%1T4v05Wph
zlGSb04J(2|&+x3pD<~coYi~95BG1)Ti`uI32)iA+|xNFdBJ+=|NSbT0BxD
z>c`SQSFG0Y{oOk%DMDpKN{-ql6m^U)@(>ROn7&G!(qRY_#RJ3n6|xiuhnoyXe>L3|
zYmfcHHM6;E4E63Lk)9L_MJZPN{IrjN4dH4)d9rhQ3CT21ZpK+R^hp+=r~xKi`y-KYU$-+ZK=~5;Umh$B~S23lo6uP
zNa$lFu&dKu)`n`qrcQt=YOoRdon9-NB6vO-A#*KPt20f1>L6Q)$4z?)-bY8Z
zL*>uJ>Tj;Sa-Us2d?yLS&hheFIezeH{_z8S_0o)pi0JqzU5a!=*JOGRm1`%Dpu-==
z4K2Aq$OR?l=O0(>m=!o*FNQstF7GadE6%O?B=ROC3a1@Y1bDa7=}}*qXWGoPjy4b<
zXsv0oy$W+VIhymEx{!6YVpTC-#eKVp(Ge;RGa6>l(BiVB#K-okF60=fc1ll;Z|iL+
zBWM(Sdrczb;G9|A>)l-bTq)Vxn#fXZ*2jJDn-@LwthemMNowiwem~XrXZ4Q)16{@u
zwEaSuxvphdgYV~nz@2HOLbmwBy4%X_lhX2Drkou19}$&&^ZBN5tEmo6EyL=b(I~nt
zg)T9C17(CbFYhp0=?ST=8)cq)=G*2TzUANsX|q$}*@5cw33ow*?F->&tEKvuTX18S
z@y}p87EtZT+kT}yYzZAA2C-Hh)Uilz$DA`O3sqj-^EyB@!xz1osOa3%T|as}t8OGb
z>TC#V#WFgNq-i*M#3C=Vt6HnfQGb&$#WZpnf+HPT2fiw()HbrjN~k_9`m&GLgi9P2
zy-hf6iDv=UP|gr#x=YFttY9&SR4pPJn9mPqbF
z`pHBmtN~tXV0j6C`dB7U`9j$@S!haBDMUJr%clsFaz{2XI>$H))S6~q1_oT~+bt+R
z^E#K!@wXl~?^MiYFXI->Qw8kl?OV@MCOC^FemUIXY~5#nL#tn50knF(wP1`5mPS3M
z8p1QO!HH5V@8bqkFUYYseiM50k4MseJ2;6AO!vctf){zFn{Q)L5(f1in`SxobkVbu
z%Ex#zEroyY6^tSzExoOGWSq1|eRVR_W-~E+2M1rAL8+&sUMm0+D2G9pes|-QP5GY4
zJu?Ij^nf3-mhGg}KS$2WloJEBtREISHi+!NqBM_oJamp4;f!YC3#%{X-cO!<<^32*
zg&W%wbRX)vC+Kx5XaJ&j{J)=cSMrDUAd{9Q1PT);X?KBj~th8?oJs(173ah{)sKEdk-G>!dJ?y
zUR=rI?7SpHosF#k3bP5!m-SgV^`>K|Qip0F>*#Yt2Lh1C@bmm5Zd;FKKN}ynz|Nzf
zP?4(mJkUfd8$n%}qL8Z=lzXN3(L%Mxy&Ko#s{f?F-7~#A)~Gz?+`GRF2+_?cJ8^Xb
zLbqJOU5cQHniHS%i9~MmO+l}zsZecIOC56#f7~YD8`BKY^YK{i+Cn22Ban5M6d;Y^
zgG&rJ47b-4HXd1r1Uti8_5*lrhq@*q+Huxx7Fe&3Z*_)doMfmGYZsg2ch2^~^nOOm
zL1v?P{ze+yy!Vqp3Xs!e(th}`4yM7{#}5h)R&aOMNoWMT<2^RA6GZNiV!5!M!qE0UcUuU{PMYW
zxZ{!V*;^TfqsY;$+uCB(P5KInY$jWdUSeb|G$0f^x;r`0{SeyA}HGszoqb*KVU?NtL2DGlfWa|?s9x6o+Wy-JEg
zpH}haew6+N{M_niz-+vhJVN$&9TuG(hy(_!tHX&rR}A8TAjY7O5I}h9xhJbj2miSn
zG=NDyen}K_KN8I!ykSu|3cd&zYN(o&DTXz1NXfQ5nX1`|0bC@Ct+LJFb3qK)vn%T8
zNm5OL8~<;sr6PSjukq_cA1aHPW!IwCp9WtLKH@Eo85U6~PH=hCPPTM?(Bn8#pg!DF
z7sK?Ck{Cf$Xs5_tMo6}!!d^sA&(L?)a!3=hJmNS%|7@b!3-(apOBo!}TNm_xedgV4
zcq=ba?zm$>Mk0G0UN)O_LMwY
zql@yx1bkwN_Li(ZdKq#j_^X`&BljHE<2#y~mgr|o8)!W@QvQ~~A3`@$U+b;4de0ro
z)Kigmul!e3FOF@Q|L?BBQ&3c?f8uPgwx{JT)Y#^>;~+?i?+30XB+zxAw
zy50SUCL;M*zeu~IUIyxK!dxuxnt9Db6OlsAF)=ZHH*gAX%kRYZ?>ip1mJCd+besA_
zuheJS0vkdLyK8oI*B#||1D}j+Rrcf1<)raD4nK<|Wz%{Bs+}EI>xt(`UtOngKiUu(
zvElioLRhGqW0kTc^j`84mgD&e}4g@ZTqS(hz
z6K~Fq0n9EQwfJNVIFit3K)=FtCOD!-KlS`Fea6o0#9kA1Dc$l_an#7PX81jbrv=NA
zcXz)DpK9fM7ktRz(lXW@n_Tn_pk#a=eiH#~#d3qB_Pe6kioNW21LoD41|jpe?{&%C
zx)OPI+xD@hyg-$
zy$Ps)`E|IY3)t5^y9xWZ^Vk)Mj%j3+!KP(oq&gl1Bt2=<-WxR>OC6>&TneL`I{fJK
zYpLJM*}stOYlPhUnZ5ISC;C7q;u*3fz6;iuAMJn^lC+?^fn-j$(&WZ%*q(z{B&AmJ
z`~jeBm?UawuAcGJpmsTtvt>0yJ6VRJN`%*MHa;Lg{%0~5voaJ+A^7Myn!f?*9Ww~H
ztd-pEZ^r+X5Y`m{&`Du;;S%uc7%qF+A@aejmS4b`e)v6WGG0>UhCH~?0pC`T^$QFxd-!ypmJ0$*+nntS%0L4Ehm*zKc{pBTKVL)_R
z53ws}=