From a469bf877af43b57d295803ad54b6ffff08bf26c Mon Sep 17 00:00:00 2001
From: Frank Hassanabad
Date: Thu, 17 Sep 2020 14:44:26 -0600
Subject: [PATCH 01/10] [Security Solution][Detection Engine] Fixes degraded
state when signals detected are between 0 and 100 (#77658) (#77797)
## Summary
Fixes: https://github.com/elastic/kibana/issues/77342
No easy way to unit test/end to end test this as it was operating correctly before and passed all of our tests, it was just running in a slow state if you had between 0 and 100 signals. The best bet is that you hand run the tests from 77342 or use a large look back time to ensure de-duplicate does not run as outlined in 77342.
Also this PR removes a TODO block, a complexity linter issue we had, a few await that were there by accident, and pushes down arrays to make things to be cleaner.
---
.../signals/search_after_bulk_create.test.ts | 2 +-
.../signals/search_after_bulk_create.ts | 60 +++++++------------
.../signals/signal_rule_alert_type.ts | 4 +-
.../detection_engine/signals/utils.test.ts | 34 +----------
.../lib/detection_engine/signals/utils.ts | 30 ++++------
5 files changed, 36 insertions(+), 94 deletions(-)
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 58dcd7f6bd1c1..0cf0c3880fc98 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
@@ -559,7 +559,7 @@ describe('searchAfterAndBulkCreate', () => {
// I don't like testing log statements since logs change but this is the best
// way I can think of to ensure this section is getting hit with this test case.
expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[15][0]).toContain(
- 'sortIds was empty on filteredEvents'
+ 'sortIds was empty on searchResult name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"'
);
});
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
index e90e5996877f8..be1c44de593a4 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts
@@ -3,8 +3,6 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-/* eslint-disable complexity */
-
import moment from 'moment';
import { AlertServices } from '../../../../../alerts/server';
@@ -25,7 +23,7 @@ interface SearchAfterAndBulkCreateParams {
previousStartedAt: Date | null | undefined;
ruleParams: RuleTypeParams;
services: AlertServices;
- listClient: ListClient | undefined; // TODO: undefined is for temporary development, remove before merged
+ listClient: ListClient;
exceptionsList: ExceptionListItemSchema[];
logger: Logger;
id: string;
@@ -91,7 +89,7 @@ export const searchAfterAndBulkCreate = async ({
};
// sortId tells us where to start our next consecutive search_after query
- let sortId;
+ let sortId: string | undefined;
// signalsCreatedCount keeps track of how many signals we have created,
// to ensure we don't exceed maxSignals
@@ -155,7 +153,7 @@ export const searchAfterAndBulkCreate = async ({
// yields zero hits, but there were hits using the previous
// sortIds.
// e.g. totalHits was 156, index 50 of 100 results, do another search-after
- // this time with a new sortId, index 22 of the remainding 56, get another sortId
+ // this time with a new sortId, index 22 of the remaining 56, get another sortId
// search with that sortId, total is still 156 but the hits.hits array is empty.
if (totalHits === 0 || searchResult.hits.hits.length === 0) {
logger.debug(
@@ -178,16 +176,13 @@ export const searchAfterAndBulkCreate = async ({
// filter out the search results that match with the values found in the list.
// the resulting set are signals to be indexed, given they are not duplicates
// of signals already present in the signals index.
- const filteredEvents: SignalSearchResponse =
- listClient != null
- ? await filterEventsAgainstList({
- listClient,
- exceptionsList,
- logger,
- eventSearchResult: searchResult,
- buildRuleMessage,
- })
- : searchResult;
+ const filteredEvents: SignalSearchResponse = await filterEventsAgainstList({
+ listClient,
+ exceptionsList,
+ logger,
+ eventSearchResult: searchResult,
+ buildRuleMessage,
+ });
// only bulk create if there are filteredEvents leftover
// if there isn't anything after going through the value list filter
@@ -234,33 +229,18 @@ export const searchAfterAndBulkCreate = async ({
logger.debug(
buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`)
);
+ }
- if (
- filteredEvents.hits.hits[0].sort != null &&
- filteredEvents.hits.hits[0].sort.length !== 0
- ) {
- sortId = filteredEvents.hits.hits[0].sort
- ? filteredEvents.hits.hits[0].sort[0]
- : undefined;
- } else {
- logger.debug(buildRuleMessage('sortIds was empty on filteredEvents'));
- toReturn.success = true;
- break;
- }
+ // we are guaranteed to have searchResult hits at this point
+ // because we check before if the totalHits or
+ // searchResult.hits.hits.length is 0
+ const lastSortId = searchResult.hits.hits[searchResult.hits.hits.length - 1].sort;
+ if (lastSortId != null && lastSortId.length !== 0) {
+ sortId = lastSortId[0];
} else {
- // we are guaranteed to have searchResult hits at this point
- // because we check before if the totalHits or
- // searchResult.hits.hits.length is 0
- if (
- searchResult.hits.hits[0].sort != null &&
- searchResult.hits.hits[0].sort.length !== 0
- ) {
- sortId = searchResult.hits.hits[0].sort ? searchResult.hits.hits[0].sort[0] : undefined;
- } else {
- logger.debug(buildRuleMessage('sortIds was empty on searchResult'));
- toReturn.success = true;
- break;
- }
+ logger.debug(buildRuleMessage('sortIds was empty on searchResult'));
+ toReturn.success = true;
+ break;
}
} catch (exc) {
logger.error(buildRuleMessage(`[-] search_after and bulk threw an error ${exc}`));
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index 2b6587300a581..d48b5b434c9c0 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -159,7 +159,7 @@ export const signalRulesAlertType = ({
}
}
try {
- const { listClient, exceptionsClient } = await getListsClient({
+ const { listClient, exceptionsClient } = getListsClient({
services,
updatedByUser,
spaceId,
@@ -168,7 +168,7 @@ export const signalRulesAlertType = ({
});
const exceptionItems = await getExceptions({
client: exceptionsClient,
- lists: exceptionsList,
+ lists: exceptionsList ?? [],
});
if (isMlRule(type)) {
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 d053a8e1089ad..9d22ba9dcc02b 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
@@ -558,7 +558,7 @@ describe('utils', () => {
});
test('it successfully returns list and exceptions list client', async () => {
- const { listClient, exceptionsClient } = await getListsClient({
+ const { listClient, exceptionsClient } = getListsClient({
services: alertServices,
savedObjectClient: alertServices.savedObjectsClient,
updatedByUser: 'some_user',
@@ -569,18 +569,6 @@ describe('utils', () => {
expect(listClient).toBeDefined();
expect(exceptionsClient).toBeDefined();
});
-
- test('it throws if "lists" is undefined', async () => {
- await expect(() =>
- getListsClient({
- services: alertServices,
- savedObjectClient: alertServices.savedObjectsClient,
- updatedByUser: 'some_user',
- spaceId: '',
- lists: undefined,
- })
- ).rejects.toThrowError('lists plugin unavailable during rule execution');
- });
});
describe('getSignalTimeTuples', () => {
@@ -743,24 +731,6 @@ describe('utils', () => {
expect(exceptions).toEqual([getExceptionListItemSchemaMock()]);
});
- test('it throws if "client" is undefined', async () => {
- await expect(() =>
- getExceptions({
- client: undefined,
- lists: getListArrayMock(),
- })
- ).rejects.toThrowError('lists plugin unavailable during rule execution');
- });
-
- test('it returns empty array if "lists" is undefined', async () => {
- const exceptions = await getExceptions({
- client: listMock.getExceptionListClient(),
- lists: undefined,
- });
-
- expect(exceptions).toEqual([]);
- });
-
test('it throws if "getExceptionListClient" fails', async () => {
const err = new Error('error fetching list');
listMock.getExceptionListClient = () =>
@@ -799,7 +769,7 @@ describe('utils', () => {
const exceptions = await getExceptions({
client: listMock.getExceptionListClient(),
- lists: undefined,
+ lists: [],
});
expect(exceptions).toEqual([]);
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
index dc09c6d5386fc..4a6ea96e1854b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts
@@ -11,7 +11,7 @@ import { Logger, SavedObjectsClientContract } from '../../../../../../../src/cor
import { AlertServices, parseDuration } from '../../../../../alerts/server';
import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server';
import { ExceptionListItemSchema } from '../../../../../lists/common/schemas';
-import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists';
+import { ListArray } from '../../../../common/detection_engine/schemas/types/lists';
import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types';
import { BuildRuleMessage } from './rule_messages';
import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates';
@@ -118,7 +118,7 @@ export const getGapMaxCatchupRatio = ({
};
};
-export const getListsClient = async ({
+export const getListsClient = ({
lists,
spaceId,
updatedByUser,
@@ -130,20 +130,16 @@ export const getListsClient = async ({
updatedByUser: string | null;
services: AlertServices;
savedObjectClient: SavedObjectsClientContract;
-}): Promise<{
- listClient: ListClient | undefined;
- exceptionsClient: ExceptionListClient | undefined;
-}> => {
+}): {
+ listClient: ListClient;
+ exceptionsClient: ExceptionListClient;
+} => {
if (lists == null) {
throw new Error('lists plugin unavailable during rule execution');
}
- const listClient = await lists.getListClient(
- services.callCluster,
- spaceId,
- updatedByUser ?? 'elastic'
- );
- const exceptionsClient = await lists.getExceptionListClient(
+ const listClient = lists.getListClient(services.callCluster, spaceId, updatedByUser ?? 'elastic');
+ const exceptionsClient = lists.getExceptionListClient(
savedObjectClient,
updatedByUser ?? 'elastic'
);
@@ -155,14 +151,10 @@ export const getExceptions = async ({
client,
lists,
}: {
- client: ExceptionListClient | undefined;
- lists: ListArrayOrUndefined;
+ client: ExceptionListClient;
+ lists: ListArray;
}): Promise => {
- if (client == null) {
- throw new Error('lists plugin unavailable during rule execution');
- }
-
- if (lists != null && lists.length > 0) {
+ if (lists.length > 0) {
try {
const listIds = lists.map(({ list_id: listId }) => listId);
const namespaceTypes = lists.map(({ namespace_type: namespaceType }) => namespaceType);
From 8c571888cf657763974880e4c47a569b4b0e936f Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Thu, 17 Sep 2020 17:24:27 -0400
Subject: [PATCH 02/10] Fix isInitialRequest (#76984) (#77809)
---
.../public/request/use_request.test.helpers.tsx | 9 ++++++++-
src/plugins/es_ui_shared/public/request/use_request.ts | 7 +++++--
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
index 7a42ed7fad427..b175066b81c8e 100644
--- a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
+++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import { act } from 'react-dom/test-utils';
import { mount, ReactWrapper } from 'enzyme';
import sinon from 'sinon';
@@ -111,6 +111,13 @@ export const createUseRequestHelpers = (): UseRequestHelpers => {
requestConfig
);
+ // Force a re-render of the component to stress-test the useRequest hook and verify its
+ // state remains unaffected.
+ const [, setState] = useState(false);
+ useEffect(() => {
+ setState(true);
+ }, []);
+
hookResult.isInitialRequest = isInitialRequest;
hookResult.isLoading = isLoading;
hookResult.error = error;
diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts
index e04f84a67b8a3..9d40291423cac 100644
--- a/src/plugins/es_ui_shared/public/request/use_request.ts
+++ b/src/plugins/es_ui_shared/public/request/use_request.ts
@@ -49,7 +49,7 @@ export const useRequest = (
// Consumers can use isInitialRequest to implement a polling UX.
const requestCountRef = useRef(0);
- const isInitialRequest = requestCountRef.current === 0;
+ const isInitialRequestRef = useRef(true);
const pollIntervalIdRef = useRef(null);
const clearPollInterval = useCallback(() => {
@@ -98,6 +98,9 @@ export const useRequest = (
return;
}
+ // Surface to consumers that at least one request has resolved.
+ isInitialRequestRef.current = false;
+
setError(responseError);
// If there's an error, keep the data from the last request in case it's still useful to the user.
if (!responseError) {
@@ -146,7 +149,7 @@ export const useRequest = (
}, [clearPollInterval]);
return {
- isInitialRequest,
+ isInitialRequest: isInitialRequestRef.current,
isLoading,
error,
data,
From 962bc173772d7fe7f25843049084454635465de0 Mon Sep 17 00:00:00 2001
From: Constance
Date: Thu, 17 Sep 2020 14:58:34 -0700
Subject: [PATCH 03/10] [Enterprise Search][tech debt] Add Kea logic paths for
easier debugging/defaults (#77698) (#77820)
* Add Kea logic paths for easier debugging/defaults
* PR feedback: prefer snake_case to match file names, use exact file name for last path key
* Document Kea + casing logic
---
x-pack/plugins/enterprise_search/README.md | 8 ++++++++
.../public/applications/app_search/app_logic.ts | 1 +
.../shared/flash_messages/flash_messages_logic.ts | 1 +
.../public/applications/shared/http/http_logic.ts | 1 +
.../public/applications/workplace_search/app_logic.ts | 1 +
.../workplace_search/views/overview/overview_logic.ts | 1 +
6 files changed, 13 insertions(+)
diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md
index 31ee304fe2247..ba14be5564be1 100644
--- a/x-pack/plugins/enterprise_search/README.md
+++ b/x-pack/plugins/enterprise_search/README.md
@@ -13,6 +13,14 @@ This plugin's goal is to provide a Kibana user interface to the Enterprise Searc
2. Update `config/kibana.dev.yml` with `enterpriseSearch.host: 'http://localhost:3002'`
3. For faster QA/development, run Enterprise Search on [elasticsearch-native auth](https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm) and log in as the `elastic` superuser on Kibana.
+### Kea
+
+Enterprise Search uses [Kea.js](https://github.com/keajs/kea) to manage our React/Redux state for us. Kea state is handled in our `*Logic` files and exposes [values](https://kea.js.org/docs/guide/concepts#values) and [actions](https://kea.js.org/docs/guide/concepts#actions) for our components to get and set state with.
+
+#### Debugging Kea
+
+To debug Kea state in-browser, Kea recommends [Redux Devtools](https://kea.js.org/docs/guide/debugging). To facilitate debugging, we use the [path](https://kea.js.org/docs/guide/debugging/#setting-the-path-manually) key with `snake_case`d paths. The path key should always end with the logic filename (e.g. `['enterprise_search', 'some_logic']`) to make it easy for devs to quickly find/jump to files via IDE tooling.
+
## Testing
### Unit tests
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
index 3f71759390879..9388d61041b13 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
@@ -16,6 +16,7 @@ export interface IAppActions {
}
export const AppLogic = kea>({
+ path: ['enterprise_search', 'app_search', 'app_logic'],
actions: {
initializeAppData: (props) => props,
},
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts
index 3ae48f352b2c1..37a8f16acad6d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts
@@ -32,6 +32,7 @@ const convertToArray = (messages: IFlashMessage | IFlashMessage[]) =>
!Array.isArray(messages) ? [messages] : messages;
export const FlashMessagesLogic = kea>({
+ path: ['enterprise_search', 'flash_messages_logic'],
actions: {
setFlashMessages: (messages) => ({ messages: convertToArray(messages) }),
clearFlashMessages: () => null,
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts
index 5e2b5a9ed6b06..72380142fe399 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts
@@ -26,6 +26,7 @@ export interface IHttpActions {
}
export const HttpLogic = kea>({
+ path: ['enterprise_search', 'http_logic'],
actions: {
initializeHttp: (props) => props,
initializeHttpInterceptors: () => null,
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts
index f88a00f63f487..94bd1d529b65f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts
@@ -22,6 +22,7 @@ export interface IAppActions {
}
export const AppLogic = kea>({
+ path: ['enterprise_search', 'workplace_search', 'app_logic'],
actions: {
initializeAppData: ({ workplaceSearch, isFederatedAuth }) => ({
workplaceSearch,
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts
index 787d5295db1cf..a156b8a8009f9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts
@@ -31,6 +31,7 @@ export interface IOverviewValues extends IOverviewServerData {
}
export const OverviewLogic = kea>({
+ path: ['enterprise_search', 'workplace_search', 'overview_logic'],
actions: {
setServerData: (serverData) => serverData,
initializeOverview: () => null,
From d7c65f94a05c0d17c857d59f065080a495bfb12b Mon Sep 17 00:00:00 2001
From: "Devin W. Hurley"
Date: Thu, 17 Sep 2020 18:39:51 -0400
Subject: [PATCH 04/10] [7.x] [Security Solution] [Detections] Remove file
validation on import route (#77770) (#77821)
* utlize schema.any() for validation on file in body of import rules request, adds new functional tests and unit tests to make sure we can reach and don't go past bounds. These tests would have helped uncover performance issues io-ts gave us with validating the import rules file object
* fix type check failure
* updates getSimpleRule and getSimpleRuleAsNdjson to accept an enabled param defaulted to false
* updates comments in e2e tests for import rules route
* fix tests after adding enabled boolean in test utils
---
.../routes/rules/import_rules_route.test.ts | 25 ++++++++
.../routes/rules/import_rules_route.ts | 10 +--
.../basic/tests/import_rules.ts | 62 +++++++++++++++++--
.../security_and_spaces/tests/import_rules.ts | 12 ++--
.../detection_engine_api_integration/utils.ts | 8 ++-
5 files changed, 99 insertions(+), 18 deletions(-)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
index 81c9387c6f39c..a033c16cd5e99 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
@@ -55,6 +55,18 @@ describe('import_rules_route', () => {
expect(response.status).toEqual(200);
});
+ test('returns 500 if more than 10,000 rules are imported', async () => {
+ const ruleIds = new Array(10001).fill(undefined).map((_, index) => `rule-${index}`);
+ const multiRequest = getImportRulesRequest(buildHapiStream(ruleIdsToNdJsonString(ruleIds)));
+ const response = await server.inject(multiRequest, context);
+
+ expect(response.status).toEqual(500);
+ expect(response.body).toEqual({
+ message: "Can't import more than 10000 rules",
+ status_code: 500,
+ });
+ });
+
test('returns 404 if alertClient is not available on the route', async () => {
context.alerting!.getAlertsClient = jest.fn();
const response = await server.inject(request, context);
@@ -229,6 +241,19 @@ describe('import_rules_route', () => {
});
});
+ test('returns 200 if many rules are imported successfully', async () => {
+ const ruleIds = new Array(9999).fill(undefined).map((_, index) => `rule-${index}`);
+ const multiRequest = getImportRulesRequest(buildHapiStream(ruleIdsToNdJsonString(ruleIds)));
+ const response = await server.inject(multiRequest, context);
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({
+ errors: [],
+ success: true,
+ success_count: 9999,
+ });
+ });
+
test('returns 200 with errors if all rules are missing rule_ids and import fails on validation', async () => {
const rulesWithoutRuleIds = ['rule-1', 'rule-2'].map((ruleId) =>
getImportRulesWithIdSchemaMock(ruleId)
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
index 60bb8c79243d7..0f44b50d4bc74 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -6,13 +6,12 @@
import { chunk } from 'lodash/fp';
import { extname } from 'path';
+import { schema } from '@kbn/config-schema';
import { validate } from '../../../../../common/validate';
import {
importRulesQuerySchema,
ImportRulesQuerySchemaDecoded,
- importRulesPayloadSchema,
- ImportRulesPayloadSchemaDecoded,
ImportRulesSchemaDecoded,
} from '../../../../../common/detection_engine/schemas/request/import_rules_schema';
import {
@@ -48,7 +47,7 @@ import { PartialFilter } from '../../types';
type PromiseFromStreams = ImportRulesSchemaDecoded | Error;
-const CHUNK_PARSED_OBJECT_SIZE = 10;
+const CHUNK_PARSED_OBJECT_SIZE = 50;
export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupPlugins['ml']) => {
router.post(
@@ -58,10 +57,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP
query: buildRouteValidation(
importRulesQuerySchema
),
- body: buildRouteValidation<
- typeof importRulesPayloadSchema,
- ImportRulesPayloadSchemaDecoded
- >(importRulesPayloadSchema),
+ body: schema.any(), // validation on file object is accomplished later in the handler.
},
options: {
tags: ['access:securitySolution'],
diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts
index 108ca365bc14f..c6294cfe6ec28 100644
--- a/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/basic/tests/import_rules.ts
@@ -129,7 +129,7 @@ export default ({ getService }: FtrProviderContext): void => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
- .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
+ .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson')
.expect(200);
const { body } = await supertest
@@ -192,6 +192,56 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
+ // import is very slow in 7.10+ due to the alerts client find api
+ // when importing 100 rules it takes about 30 seconds for this
+ // test to complete so at 10 rules completing in about 10 seconds
+ // I figured this is enough to make sure the import route is doing its job.
+ it('should be able to import 10 rules', async () => {
+ const ruleIds = new Array(10).fill(undefined).map((_, index) => `rule-${index}`);
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ .set('kbn-xsrf', 'true')
+ .attach('file', getSimpleRuleAsNdjson(ruleIds, false), 'rules.ndjson')
+ .expect(200);
+
+ expect(body).to.eql({
+ errors: [],
+ success: true,
+ success_count: 10,
+ });
+ });
+
+ // uncomment the below test once we speed up the alerts client find api
+ // in another PR.
+ // it('should be able to import 10000 rules', async () => {
+ // const ruleIds = new Array(10000).fill(undefined).map((_, index) => `rule-${index}`);
+ // const { body } = await supertest
+ // .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ // .set('kbn-xsrf', 'true')
+ // .attach('file', getSimpleRuleAsNdjson(ruleIds, false), 'rules.ndjson')
+ // .expect(200);
+
+ // expect(body).to.eql({
+ // errors: [],
+ // success: true,
+ // success_count: 10000,
+ // });
+ // });
+
+ it('should NOT be able to import more than 10,000 rules', async () => {
+ const ruleIds = new Array(10001).fill(undefined).map((_, index) => `rule-${index}`);
+ const { body } = await supertest
+ .post(`${DETECTION_ENGINE_RULES_URL}/_import`)
+ .set('kbn-xsrf', 'true')
+ .attach('file', getSimpleRuleAsNdjson(ruleIds, false), 'rules.ndjson')
+ .expect(500);
+
+ expect(body).to.eql({
+ status_code: 500,
+ message: "Can't import more than 10000 rules",
+ });
+ });
+
it('should report a conflict if there is an attempt to import two rules with the same rule_id', async () => {
const { body } = await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
@@ -280,7 +330,7 @@ export default ({ getService }: FtrProviderContext): void => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
- .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
+ .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson')
.expect(200);
const simpleRule = getSimpleRule('rule-1');
@@ -372,13 +422,17 @@ export default ({ getService }: FtrProviderContext): void => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
- .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson')
+ .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2'], true), 'rules.ndjson')
.expect(200);
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
- .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson')
+ .attach(
+ 'file',
+ getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3'], true),
+ 'rules.ndjson'
+ )
.expect(200);
const { body: bodyOfRule1 } = await supertest
diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts
index e0b60ae1fbeeb..664077d5a4fab 100644
--- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts
+++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/import_rules.ts
@@ -129,7 +129,7 @@ export default ({ getService }: FtrProviderContext): void => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
- .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
+ .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson')
.expect(200);
const { body } = await supertest
@@ -243,7 +243,7 @@ export default ({ getService }: FtrProviderContext): void => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
- .attach('file', getSimpleRuleAsNdjson(['rule-1']), 'rules.ndjson')
+ .attach('file', getSimpleRuleAsNdjson(['rule-1'], true), 'rules.ndjson')
.expect(200);
const simpleRule = getSimpleRule('rule-1');
@@ -335,13 +335,17 @@ export default ({ getService }: FtrProviderContext): void => {
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
- .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2']), 'rules.ndjson')
+ .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2'], true), 'rules.ndjson')
.expect(200);
await supertest
.post(`${DETECTION_ENGINE_RULES_URL}/_import`)
.set('kbn-xsrf', 'true')
- .attach('file', getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3']), 'rules.ndjson')
+ .attach(
+ 'file',
+ getSimpleRuleAsNdjson(['rule-1', 'rule-2', 'rule-3'], true),
+ 'rules.ndjson'
+ )
.expect(200);
const { body: bodyOfRule1 } = await supertest
diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts
index 4cbbc142edd40..1dba1a154373b 100644
--- a/x-pack/test/detection_engine_api_integration/utils.ts
+++ b/x-pack/test/detection_engine_api_integration/utils.ts
@@ -56,10 +56,12 @@ export const removeServerGeneratedPropertiesIncludingRuleId = (
/**
* This is a typical simple rule for testing that is easy for most basic testing
* @param ruleId
+ * @param enabled Enables the rule on creation or not. Defaulted to false to enable it on import
*/
-export const getSimpleRule = (ruleId = 'rule-1'): CreateRulesSchema => ({
+export const getSimpleRule = (ruleId = 'rule-1', enabled = true): CreateRulesSchema => ({
name: 'Simple Rule Query',
description: 'Simple Rule Query',
+ enabled,
risk_score: 1,
rule_id: ruleId,
severity: 'high',
@@ -360,9 +362,9 @@ export const deleteSignalsIndex = async (
* for testing uploads.
* @param ruleIds Array of strings of rule_ids
*/
-export const getSimpleRuleAsNdjson = (ruleIds: string[]): Buffer => {
+export const getSimpleRuleAsNdjson = (ruleIds: string[], enabled = false): Buffer => {
const stringOfRules = ruleIds.map((ruleId) => {
- const simpleRule = getSimpleRule(ruleId);
+ const simpleRule = getSimpleRule(ruleId, enabled);
return JSON.stringify(simpleRule);
});
return Buffer.from(stringOfRules.join('\n'));
From b6be6e9dfb15f822fd13acc099f45e3f4f86885e Mon Sep 17 00:00:00 2001
From: Christos Nasikas
Date: Fri, 18 Sep 2020 02:21:23 +0300
Subject: [PATCH 05/10] [7.x] [Security Solutions][Cases] Cases Redesign
(#73247) (#77771)
Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
Co-authored-by: Yara Tercero
Co-authored-by: Elastic Machine
---
.../cypress/integration/cases.spec.ts | 9 +-
.../timeline_attach_to_case.spec.ts | 10 +-
.../cypress/screens/case_details.ts | 16 +-
.../cypress/screens/create_new_case.ts | 6 +-
.../cypress/tasks/create_new_case.ts | 4 -
.../cases/components/add_comment/index.tsx | 4 +-
.../cases/components/case_view/index.test.tsx | 15 +-
.../public/cases/components/create/index.tsx | 4 +-
.../cases/components/tag_list/index.test.tsx | 12 +-
.../cases/components/tag_list/index.tsx | 14 +-
.../public/cases/components/tag_list/tags.tsx | 32 ++
.../user_action_tree/helpers.test.tsx | 18 +-
.../components/user_action_tree/helpers.tsx | 52 ++-
.../user_action_tree/index.test.tsx | 349 ++++++++-------
.../components/user_action_tree/index.tsx | 407 ++++++++++++------
.../user_action_avatar.test.tsx | 47 ++
.../user_action_tree/user_action_avatar.tsx | 21 +-
.../user_action_content_toolbar.test.tsx | 55 +++
.../user_action_content_toolbar.tsx | 52 +++
.../user_action_copy_link.test.tsx | 74 ++++
.../user_action_copy_link.tsx | 43 ++
.../user_action_tree/user_action_item.tsx | 197 ---------
.../user_action_markdown.test.tsx | 24 +-
.../user_action_tree/user_action_markdown.tsx | 59 ++-
.../user_action_move_to_reference.test.tsx | 34 ++
.../user_action_move_to_reference.tsx | 37 ++
.../user_action_property_actions.test.tsx | 50 +++
.../user_action_property_actions.tsx | 58 +++
.../user_action_timestamp.test.tsx | 74 ++++
.../user_action_timestamp.tsx | 46 ++
.../user_action_title.test.tsx | 54 ---
.../user_action_tree/user_action_title.tsx | 183 --------
.../user_action_username.test.tsx | 68 +++
.../user_action_tree/user_action_username.tsx | 28 ++
.../user_action_username_with_avatar.test.tsx | 42 ++
.../user_action_username_with_avatar.tsx | 43 ++
.../public/common/components/link_to/index.ts | 23 +-
.../components/link_to/redirect_to_case.tsx | 11 +
.../link_to/redirect_to_timelines.tsx | 6 +
.../components/markdown_editor/eui_form.tsx | 87 ++++
.../components/markdown_editor/form.tsx | 67 ---
.../components/markdown_editor/index.test.tsx | 49 ---
.../components/markdown_editor/index.tsx | 165 +------
.../plugins/timeline/constants.ts | 8 +
.../markdown_editor/plugins/timeline/index.ts | 11 +
.../plugins/timeline/parser.ts | 119 +++++
.../plugins/timeline/plugin.tsx | 87 ++++
.../plugins/timeline/processor.tsx | 34 ++
.../plugins/timeline/translations.ts | 54 +++
.../markdown_editor/plugins/timeline/types.ts | 18 +
.../components/markdown_editor/types.ts | 10 +
.../utils/timeline}/use_timeline_click.tsx | 0
.../rules/step_about_rule/index.tsx | 2 +-
.../use_insert_timeline.tsx | 19 +-
54 files changed, 1888 insertions(+), 1123 deletions(-)
create mode 100644 x-pack/plugins/security_solution/public/cases/components/tag_list/tags.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_avatar.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_content_toolbar.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_content_toolbar.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_copy_link.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_copy_link.tsx
delete mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.tsx
delete mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx
delete mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
delete mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx
delete mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/constants.ts
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/index.ts
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/parser.ts
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/types.ts
create mode 100644 x-pack/plugins/security_solution/public/common/components/markdown_editor/types.ts
rename x-pack/plugins/security_solution/public/{cases/components/utils => common/utils/timeline}/use_timeline_click.tsx (100%)
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
index 6194d6892d799..a45b1fd18a4b6 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
@@ -24,17 +24,16 @@ import {
ALL_CASES_TAGS_COUNT,
} from '../screens/all_cases';
import {
- ACTION,
CASE_DETAILS_DESCRIPTION,
CASE_DETAILS_PAGE_TITLE,
CASE_DETAILS_PUSH_TO_EXTERNAL_SERVICE_BTN,
CASE_DETAILS_STATUS,
CASE_DETAILS_TAGS,
- CASE_DETAILS_USER_ACTION,
+ CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME,
+ CASE_DETAILS_USER_ACTION_DESCRIPTION_EVENT,
CASE_DETAILS_USERNAMES,
PARTICIPANTS,
REPORTER,
- USER,
} from '../screens/case_details';
import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens/timeline';
@@ -84,8 +83,8 @@ describe('Cases', () => {
const expectedTags = case1.tags.join('');
cy.get(CASE_DETAILS_PAGE_TITLE).should('have.text', case1.name);
cy.get(CASE_DETAILS_STATUS).should('have.text', 'open');
- cy.get(CASE_DETAILS_USER_ACTION).eq(USER).should('have.text', case1.reporter);
- cy.get(CASE_DETAILS_USER_ACTION).eq(ACTION).should('have.text', 'added description');
+ cy.get(CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME).should('have.text', case1.reporter);
+ cy.get(CASE_DETAILS_USER_ACTION_DESCRIPTION_EVENT).should('have.text', 'added description');
cy.get(CASE_DETAILS_DESCRIPTION).should(
'have.text',
`${case1.description} ${case1.timeline.title}`
diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts
index 6af4d174b9583..3862a89a7d833 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts
@@ -11,7 +11,7 @@ import {
addNewCase,
selectCase,
} from '../tasks/timeline';
-import { DESCRIPTION_INPUT } from '../screens/create_new_case';
+import { DESCRIPTION_INPUT, ADD_COMMENT_INPUT } from '../screens/create_new_case';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { caseTimeline, TIMELINE_CASE_ID } from '../objects/case';
@@ -34,7 +34,7 @@ describe('attach timeline to case', () => {
cy.location('origin').then((origin) => {
cy.get(DESCRIPTION_INPUT).should(
'have.text',
- `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
+ `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:%27${caseTimeline.id}%27,isOpen:!t))`
);
});
});
@@ -46,7 +46,7 @@ describe('attach timeline to case', () => {
cy.location('origin').then((origin) => {
cy.get(DESCRIPTION_INPUT).should(
'have.text',
- `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
+ `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:%27${caseTimeline.id}%27,isOpen:!t))`
);
});
});
@@ -66,9 +66,9 @@ describe('attach timeline to case', () => {
selectCase(TIMELINE_CASE_ID);
cy.location('origin').then((origin) => {
- cy.get(DESCRIPTION_INPUT).should(
+ cy.get(ADD_COMMENT_INPUT).should(
'have.text',
- `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
+ `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:%27${caseTimeline.id}%27,isOpen:!t))`
);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts
index f2cdaa6994356..7b995f5395543 100644
--- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const ACTION = 2;
-
-export const CASE_DETAILS_DESCRIPTION = '[data-test-subj="markdown-root"]';
+export const CASE_DETAILS_DESCRIPTION =
+ '[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]';
export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="header-page-title"]';
@@ -17,14 +16,17 @@ export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status"]';
export const CASE_DETAILS_TAGS = '[data-test-subj="case-tags"]';
-export const CASE_DETAILS_TIMELINE_LINK_MARKDOWN = '[data-test-subj="markdown-timeline-link"]';
+export const CASE_DETAILS_TIMELINE_LINK_MARKDOWN =
+ '[data-test-subj="description-action"] [data-test-subj="user-action-markdown"] button';
+
+export const CASE_DETAILS_USER_ACTION_DESCRIPTION_EVENT =
+ '[data-test-subj="description-action"] .euiCommentEvent__headerEvent';
-export const CASE_DETAILS_USER_ACTION = '[data-test-subj="user-action-title"] .euiFlexItem';
+export const CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME =
+ '[data-test-subj="description-action"] .euiCommentEvent__headerUsername';
export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]';
export const PARTICIPANTS = 1;
export const REPORTER = 0;
-
-export const USER = 1;
diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts
index 9431c054d96a4..4f348b4dcdbd1 100644
--- a/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export const ADD_COMMENT_INPUT = '[data-test-subj="add-comment"] textarea';
+
export const BACK_TO_CASES_BTN = '[data-test-subj="backToCases"]';
-export const DESCRIPTION_INPUT = '[data-test-subj="textAreaInput"]';
+export const DESCRIPTION_INPUT = '[data-test-subj="caseDescription"] textarea';
-export const INSERT_TIMELINE_BTN = '[data-test-subj="insert-timeline-button"]';
+export const INSERT_TIMELINE_BTN = '.euiMarkdownEditorToolbar [aria-label="Insert timeline link"]';
export const LOADING_SPINNER = '[data-test-subj="create-case-loading-spinner"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts
index 1d5d240c5c53d..f5013eed07d29 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts
@@ -13,7 +13,6 @@ import {
INSERT_TIMELINE_BTN,
LOADING_SPINNER,
TAGS_INPUT,
- TIMELINE,
TIMELINE_SEARCHBOX,
TITLE_INPUT,
} from '../screens/create_new_case';
@@ -43,9 +42,6 @@ export const createNewCaseWithTimeline = (newCase: TestCase) => {
cy.get(INSERT_TIMELINE_BTN).click({ force: true });
cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`);
- cy.get(TIMELINE).should('be.visible');
- cy.wait(300);
- cy.get(TIMELINE).eq(0).click({ force: true });
cy.get(SUBMIT_BTN).click({ force: true });
cy.get(LOADING_SPINNER).should('exist');
diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
index ef13c87a92dbb..14c42697dcbb4 100644
--- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
@@ -11,14 +11,14 @@ import styled from 'styled-components';
import { CommentRequest } from '../../../../../case/common/api';
import { usePostComment } from '../../containers/use_post_comment';
import { Case } from '../../containers/types';
-import { MarkdownEditorForm } from '../../../common/components/markdown_editor/form';
+import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form';
import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
import { Form, useForm, UseField, useFormData } from '../../../shared_imports';
import * as i18n from './translations';
import { schema } from './schema';
-import { useTimelineClick } from '../utils/use_timeline_click';
+import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click';
const MySpinner = styled(EuiLoadingSpinner)`
position: absolute;
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
index e1d7d98ba8c51..246df1c94b817 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
@@ -114,34 +114,41 @@ describe('CaseView ', () => {
expect(wrapper.find(`[data-test-subj="case-view-title"]`).first().prop('title')).toEqual(
data.title
);
+
expect(wrapper.find(`[data-test-subj="case-view-status"]`).first().text()).toEqual(
data.status
);
+
expect(
wrapper
- .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag-coke"]`)
+ .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="tag-coke"]`)
.first()
.text()
).toEqual(data.tags[0]);
+
expect(
wrapper
- .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag-pepsi"]`)
+ .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="tag-pepsi"]`)
.first()
.text()
).toEqual(data.tags[1]);
+
expect(wrapper.find(`[data-test-subj="case-view-username"]`).first().text()).toEqual(
data.createdBy.username
);
+
expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false);
+
expect(wrapper.find(`[data-test-subj="case-view-createdAt"]`).first().prop('value')).toEqual(
data.createdAt
);
+
expect(
wrapper
.find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]`)
.first()
- .prop('raw')
- ).toEqual(data.description);
+ .text()
+ ).toBe(data.description);
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
index 3c3cc95218b03..a8babe729fde0 100644
--- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
@@ -31,10 +31,10 @@ import { schema } from './schema';
import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
import * as i18n from '../../translations';
-import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
+import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form';
import { useGetTags } from '../../containers/use_get_tags';
import { getCaseDetailsUrl } from '../../../common/components/link_to';
-import { useTimelineClick } from '../utils/use_timeline_click';
+import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click';
export const CommonUseField = getUseField({ component: Field });
diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx
index 7c3fcde687033..a60167a18762f 100644
--- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx
@@ -58,6 +58,7 @@ describe('TagList ', () => {
fetchTags,
}));
});
+
it('Renders no tags, and then edit', () => {
const wrapper = mount(
@@ -69,6 +70,7 @@ describe('TagList ', () => {
expect(wrapper.find(`[data-test-subj="no-tags"]`).last().exists()).toBeFalsy();
expect(wrapper.find(`[data-test-subj="edit-tags"]`).last().exists()).toBeTruthy();
});
+
it('Edit tag on submit', async () => {
const wrapper = mount(
@@ -81,6 +83,7 @@ describe('TagList ', () => {
await waitFor(() => expect(onSubmit).toBeCalledWith(sampleTags));
});
});
+
it('Tag options render with new tags added', () => {
const wrapper = mount(
@@ -92,6 +95,7 @@ describe('TagList ', () => {
wrapper.find(`[data-test-subj="caseTags"] [data-test-subj="input"]`).first().prop('options')
).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]);
});
+
it('Cancels on cancel', async () => {
const props = {
...defaultProps,
@@ -102,17 +106,19 @@ describe('TagList ', () => {
);
- expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeTruthy();
+
+ expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy();
wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click');
await act(async () => {
- expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeFalsy();
+ expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeFalsy();
wrapper.find(`[data-test-subj="edit-tags-cancel"]`).last().simulate('click');
await waitFor(() => {
wrapper.update();
- expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy();
});
});
});
+
it('Renders disabled button', () => {
const props = { ...defaultProps, disabled: true };
const wrapper = mount(
diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx
index eeb7c49eceab5..4af781e3c31f4 100644
--- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx
@@ -10,8 +10,6 @@ import {
EuiHorizontalRule,
EuiFlexGroup,
EuiFlexItem,
- EuiBadgeGroup,
- EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiButtonIcon,
@@ -25,6 +23,8 @@ import { schema } from './schema';
import { CommonUseField } from '../create';
import { useGetTags } from '../../containers/use_get_tags';
+import { Tags } from './tags';
+
interface TagListProps {
disabled?: boolean;
isLoading: boolean;
@@ -99,15 +99,7 @@ export const TagList = React.memo(
{tags.length === 0 && !isEditTags && {i18n.NO_TAGS}
}
-
- {tags.length > 0 &&
- !isEditTags &&
- tags.map((tag) => (
-
- {tag}
-
- ))}
-
+ {!isEditTags && }
{isEditTags && (
diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/tags.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/tags.tsx
new file mode 100644
index 0000000000000..e257563ce751e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/tags.tsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo } from 'react';
+import { EuiBadgeGroup, EuiBadge, EuiBadgeGroupProps } from '@elastic/eui';
+
+interface TagsProps {
+ tags: string[];
+ color?: string;
+ gutterSize?: EuiBadgeGroupProps['gutterSize'];
+}
+
+const TagsComponent: React.FC = ({ tags, color = 'default', gutterSize }) => {
+ return (
+ <>
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+ >
+ );
+};
+
+export const Tags = memo(TagsComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx
index b5be84db59920..4e5c05f2f1404 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx
@@ -14,7 +14,7 @@ import { connectorsMock } from '../../containers/configure/mock';
describe('User action tree helpers', () => {
const connectors = connectorsMock;
it('label title generated for update tags', () => {
- const action = getUserAction(['title'], 'update');
+ const action = getUserAction(['tags'], 'update');
const result: string | JSX.Element = getLabelTitle({
action,
connectors,
@@ -27,8 +27,11 @@ describe('User action tree helpers', () => {
` ${i18n.TAGS.toLowerCase()}`
);
- expect(wrapper.find(`[data-test-subj="ua-tag"]`).first().text()).toEqual(action.newValue);
+ expect(wrapper.find(`[data-test-subj="tag-${action.newValue}"]`).first().text()).toEqual(
+ action.newValue
+ );
});
+
it('label title generated for update title', () => {
const action = getUserAction(['title'], 'update');
const result: string | JSX.Element = getLabelTitle({
@@ -44,6 +47,7 @@ describe('User action tree helpers', () => {
}"`
);
});
+
it('label title generated for update description', () => {
const action = getUserAction(['description'], 'update');
const result: string | JSX.Element = getLabelTitle({
@@ -55,6 +59,7 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`);
});
+
it('label title generated for update status to open', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: 'open' };
const result: string | JSX.Element = getLabelTitle({
@@ -66,6 +71,7 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.REOPENED_CASE.toLowerCase()} ${i18n.CASE}`);
});
+
it('label title generated for update status to closed', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: 'closed' };
const result: string | JSX.Element = getLabelTitle({
@@ -77,6 +83,7 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.CLOSED_CASE.toLowerCase()} ${i18n.CASE}`);
});
+
it('label title generated for update comment', () => {
const action = getUserAction(['comment'], 'update');
const result: string | JSX.Element = getLabelTitle({
@@ -88,6 +95,7 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`);
});
+
it('label title generated for pushed incident', () => {
const action = getUserAction(['pushed'], 'push-to-service');
const result: string | JSX.Element = getLabelTitle({
@@ -105,6 +113,7 @@ describe('User action tree helpers', () => {
JSON.parse(action.newValue).external_url
);
});
+
it('label title generated for needs update incident', () => {
const action = getUserAction(['pushed'], 'push-to-service');
const result: string | JSX.Element = getLabelTitle({
@@ -122,6 +131,7 @@ describe('User action tree helpers', () => {
JSON.parse(action.newValue).external_url
);
});
+
it('label title generated for update connector', () => {
const action = getUserAction(['connector_id'], 'update');
const result: string | JSX.Element = getLabelTitle({
@@ -136,6 +146,8 @@ describe('User action tree helpers', () => {
` ${i18n.TAGS.toLowerCase()}`
);
- expect(wrapper.find(`[data-test-subj="ua-tag"]`).first().text()).toEqual(action.newValue);
+ expect(wrapper.find(`[data-test-subj="tag-${action.newValue}"]`).first().text()).toEqual(
+ action.newValue
+ );
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx
index e343c3da6cc8b..4d8bb9ba078e5 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx
@@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiBadgeGroup, EuiBadge, EuiLink } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import React from 'react';
import { CaseFullExternalService, Connector } from '../../../../../case/common/api';
import { CaseUserActions } from '../../containers/types';
+import { CaseServices } from '../../containers/use_get_case_user_actions';
import * as i18n from '../case_view/translations';
+import { Tags } from '../tag_list/tags';
interface LabelTitle {
action: CaseUserActions;
@@ -44,22 +46,21 @@ export const getLabelTitle = ({ action, connectors, field, firstPush }: LabelTit
return '';
};
-const getTagsLabelTitle = (action: CaseUserActions) => (
-
-
- {action.action === 'add' && i18n.ADDED_FIELD}
- {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
-
-
- {action.newValue != null &&
- action.newValue.split(',').map((tag) => (
-
- {tag}
-
- ))}
-
-
-);
+const getTagsLabelTitle = (action: CaseUserActions) => {
+ const tags = action.newValue != null ? action.newValue.split(',') : [];
+
+ return (
+
+
+ {action.action === 'add' && i18n.ADDED_FIELD}
+ {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
+
+
+
+
+
+ );
+};
const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean) => {
const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService;
@@ -78,3 +79,20 @@ const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean)
);
};
+
+export const getPushInfo = (
+ caseServices: CaseServices,
+ parsedValue: { connector_id: string; connector_name: string },
+ index: number
+) =>
+ parsedValue != null
+ ? {
+ firstPush: caseServices[parsedValue.connector_id].firstPushIndex === index,
+ parsedConnectorId: parsedValue.connector_id,
+ parsedConnectorName: parsedValue.connector_name,
+ }
+ : {
+ firstPush: false,
+ parsedConnectorId: 'none',
+ parsedConnectorName: 'none',
+ };
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx
index d67c364bbda10..d2bb2fb243458 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx
@@ -6,6 +6,9 @@
import React from 'react';
import { mount } from 'enzyme';
+// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
+import { wait as waitFor } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
import { getFormMock, useFormMock, useFormDataMock } from '../__mock__/form';
@@ -13,9 +16,6 @@ import { useUpdateComment } from '../../containers/use_update_comment';
import { basicCase, basicPush, getUserAction } from '../../containers/mock';
import { UserActionTree } from '.';
import { TestProviders } from '../../../common/mock';
-// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
-import { wait as waitFor } from '@testing-library/react';
-import { act } from 'react-dom/test-utils';
const fetchUserActions = jest.fn();
const onUpdateField = jest.fn();
@@ -66,9 +66,10 @@ describe('UserActionTree ', () => {
expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().prop('name')).toEqual(
defaultProps.data.createdBy.fullName
);
- expect(wrapper.find(`[data-test-subj="user-action-title"] strong`).first().text()).toEqual(
- defaultProps.data.createdBy.username
- );
+
+ expect(
+ wrapper.find(`[data-test-subj="description-action"] figcaption strong`).first().text()
+ ).toEqual(defaultProps.data.createdBy.username);
});
it('Renders service now update line with top and bottom when push is required', async () => {
@@ -76,6 +77,7 @@ describe('UserActionTree ', () => {
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'update'),
];
+
const props = {
...defaultProps,
caseServices: {
@@ -90,20 +92,18 @@ describe('UserActionTree ', () => {
caseUserActions: ourActions,
};
- const wrapper = mount(
-
-
-
-
-
- );
-
await act(async () => {
- wrapper.update();
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toBeTruthy();
});
-
- expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy();
- expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeTruthy();
});
it('Renders service now update line with top only when push is up to date', async () => {
@@ -122,20 +122,17 @@ describe('UserActionTree ', () => {
},
};
- const wrapper = mount(
-
-
-
-
-
- );
-
await act(async () => {
- wrapper.update();
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toBeFalsy();
});
-
- expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy();
- expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeFalsy();
});
it('Outlines comment when update move to link is clicked', async () => {
@@ -145,89 +142,104 @@ describe('UserActionTree ', () => {
caseUserActions: ourActions,
};
- const wrapper = mount(
-
-
-
-
-
- );
-
await act(async () => {
- wrapper.update();
- });
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(
+ wrapper
+ .find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`)
+ .first()
+ .hasClass('outlined')
+ ).toBeFalsy();
- expect(
- wrapper.find(`[data-test-subj="comment-create-action"]`).first().prop('idToOutline')
- ).toEqual('');
- wrapper
- .find(`[data-test-subj="comment-update-action"] [data-test-subj="move-to-link"]`)
- .first()
- .simulate('click');
- expect(
- wrapper.find(`[data-test-subj="comment-create-action"]`).first().prop('idToOutline')
- ).toEqual(ourActions[0].commentId);
+ wrapper
+ .find(
+ `[data-test-subj="comment-update-action-${ourActions[1].actionId}"] [data-test-subj="move-to-link-${props.data.comments[0].id}"]`
+ )
+ .first()
+ .simulate('click');
+
+ await waitFor(() => {
+ wrapper.update();
+ expect(
+ wrapper
+ .find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`)
+ .first()
+ .hasClass('outlined')
+ ).toBeTruthy();
+ });
+ });
});
it('Switches to markdown when edit is clicked and back to panel when canceled', async () => {
- const ourActions = [getUserAction(['comment'], 'create')];
- const props = {
- ...defaultProps,
- caseUserActions: ourActions,
- };
-
- const wrapper = mount(
-
-
-
-
-
- );
-
- await act(async () => {
- wrapper.update();
- });
+ await waitFor(() => {
+ const ourActions = [getUserAction(['comment'], 'create')];
+ const props = {
+ ...defaultProps,
+ caseUserActions: ourActions,
+ };
+
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(
+ wrapper
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ )
+ .exists()
+ ).toEqual(false);
- expect(
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-ellipses"]`
)
- .exists()
- ).toEqual(false);
+ .first()
+ .simulate('click');
- wrapper
- .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`)
- .first()
- .simulate('click');
-
- wrapper
- .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`)
- .first()
- .simulate('click');
+ wrapper.update();
- expect(
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-pencil"]`
)
- .exists()
- ).toEqual(true);
+ .first()
+ .simulate('click');
- wrapper
- .find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]`
- )
- .first()
- .simulate('click');
+ expect(
+ wrapper
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ )
+ .exists()
+ ).toEqual(true);
- expect(
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]`
)
- .exists()
- ).toEqual(false);
+ .first()
+ .simulate('click');
+
+ expect(
+ wrapper
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ )
+ .exists()
+ ).toEqual(false);
+ });
});
it('calls update comment when comment markdown is saved', async () => {
@@ -236,6 +248,7 @@ describe('UserActionTree ', () => {
...defaultProps,
caseUserActions: ourActions,
};
+
const wrapper = mount(
@@ -243,27 +256,35 @@ describe('UserActionTree ', () => {
);
+
wrapper
- .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`)
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-ellipses"]`
+ )
.first()
.simulate('click');
+
wrapper
- .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`)
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-pencil"]`
+ )
.first()
.simulate('click');
+
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]`
)
.first()
.simulate('click');
+
await act(async () => {
await waitFor(() => {
wrapper.update();
expect(
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
)
.exists()
).toEqual(false);
@@ -288,93 +309,101 @@ describe('UserActionTree ', () => {
);
+
wrapper
.find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`)
.first()
.simulate('click');
+
wrapper
.find(`[data-test-subj="description-action"] [data-test-subj="property-actions-pencil"]`)
.first()
.simulate('click');
- wrapper
- .find(
- `[data-test-subj="user-action-description"] [data-test-subj="user-action-save-markdown"]`
- )
- .first()
- .simulate('click');
+
await act(async () => {
- await waitFor(() => {
- expect(
- wrapper
- .find(
- `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]`
- )
- .exists()
- ).toEqual(false);
- expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content });
- });
+ wrapper
+ .find(`[data-test-subj="description-action"] [data-test-subj="user-action-save-markdown"]`)
+ .first()
+ .simulate('click');
});
+
+ wrapper.update();
+
+ expect(
+ wrapper
+ .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown-form"]`)
+ .exists()
+ ).toEqual(false);
+
+ expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content });
});
it('quotes', async () => {
- const commentData = {
- comment: '',
- };
- const formHookMock = getFormMock(commentData);
- const setFieldValue = jest.fn();
- useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } }));
- const props = defaultProps;
- const wrapper = mount(
-
-
-
-
-
- );
-
await act(async () => {
+ const commentData = {
+ comment: '',
+ };
+ const setFieldValue = jest.fn();
+
+ const formHookMock = getFormMock(commentData);
+ useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } }));
+
+ const props = defaultProps;
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ wrapper
+ .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`)
+ .first()
+ .simulate('click');
+
await waitFor(() => {
- wrapper
- .find(
- `[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`
- )
- .first()
- .simulate('click');
wrapper.update();
});
- });
- wrapper
- .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`)
- .first()
- .simulate('click');
+ wrapper
+ .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`)
+ .first()
+ .simulate('click');
- expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`);
+ expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`);
+ });
});
it('Outlines comment when url param is provided', async () => {
- const commentId = 'neat-comment-id';
- const ourActions = [getUserAction(['comment'], 'create')];
- const props = {
- ...defaultProps,
- caseUserActions: ourActions,
- };
-
+ const commentId = 'basic-comment-id';
jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId });
- const wrapper = mount(
-
-
-
-
-
- );
await act(async () => {
- wrapper.update();
- });
+ const ourActions = [getUserAction(['comment'], 'create')];
+ const props = {
+ ...defaultProps,
+ caseUserActions: ourActions,
+ };
+
+ const wrapper = mount(
+
+
+
+
+
+ );
- expect(
- wrapper.find(`[data-test-subj="comment-create-action"]`).first().prop('idToOutline')
- ).toEqual(commentId);
+ await waitFor(() => {
+ wrapper.update();
+ });
+
+ expect(
+ wrapper
+ .find(`[data-test-subj="comment-create-action-${commentId}"]`)
+ .first()
+ .hasClass('outlined')
+ ).toBeTruthy();
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx
index d1263ab13f41b..bada15294de09 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx
@@ -3,25 +3,38 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import classNames from 'classnames';
-import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiCommentList,
+ EuiCommentProps,
+} from '@elastic/eui';
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
-import * as i18n from '../case_view/translations';
+import * as i18n from './translations';
import { Case, CaseUserActions } from '../../containers/types';
import { useUpdateComment } from '../../containers/use_update_comment';
import { useCurrentUser } from '../../../common/lib/kibana';
import { AddComment, AddCommentRefObject } from '../add_comment';
-import { getLabelTitle } from './helpers';
-import { UserActionItem } from './user_action_item';
-import { UserActionMarkdown } from './user_action_markdown';
import { Connector } from '../../../../../case/common/api/cases';
import { CaseServices } from '../../containers/use_get_case_user_actions';
import { parseString } from '../../containers/utils';
import { OnUpdateFields } from '../case_view';
+import { getLabelTitle, getPushInfo } from './helpers';
+import { UserActionAvatar } from './user_action_avatar';
+import { UserActionMarkdown } from './user_action_markdown';
+import { UserActionTimestamp } from './user_action_timestamp';
+import { UserActionCopyLink } from './user_action_copy_link';
+import { UserActionMoveToReference } from './user_action_move_to_reference';
+import { UserActionUsername } from './user_action_username';
+import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar';
+import { UserActionContentToolbar } from './user_action_content_toolbar';
export interface UserActionTreeProps {
caseServices: CaseServices;
@@ -40,6 +53,31 @@ const MyEuiFlexGroup = styled(EuiFlexGroup)`
margin-bottom: 8px;
`;
+const MyEuiCommentList = styled(EuiCommentList)`
+ ${({ theme }) => `
+ & .userAction__comment.outlined .euiCommentEvent {
+ outline: solid 5px ${theme.eui.euiColorVis1_behindText};
+ margin: 0.5em;
+ transition: 0.8s;
+ }
+
+ & .euiComment.isEdit {
+ & .euiCommentEvent {
+ border: none;
+ box-shadow: none;
+ }
+
+ & .euiCommentEvent__body {
+ padding: 0;
+ }
+
+ & .euiCommentEvent__header {
+ display: none;
+ }
+ }
+ `}
+`;
+
const DESCRIPTION_ID = 'description';
const NEW_ID = 'newComment';
@@ -86,8 +124,7 @@ export const UserActionTree = React.memo(
updateCase,
});
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [caseData, handleManageMarkdownEditId, patchComment, updateCase]
+ [caseData.id, fetchUserActions, patchComment, updateCase]
);
const handleOutlineComment = useCallback(
@@ -172,117 +209,246 @@ export const UserActionTree = React.memo(
}
}
}, [commentId, initLoading, isLoadingUserActions, isLoadingIds, handleOutlineComment]);
- return (
- <>
- {i18n.ADDED_DESCRIPTION}>}
- fullName={caseData.createdBy.fullName ?? caseData.createdBy.username ?? ''}
- markdown={MarkdownDescription}
- onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)}
- onQuote={handleManageQuote.bind(null, caseData.description)}
- username={caseData.createdBy.username ?? i18n.UNKNOWN}
- />
- {caseUserActions.map((action, index) => {
- if (action.commentId != null && action.action === 'create') {
- const comment = caseData.comments.find((c) => c.id === action.commentId);
- if (comment != null) {
- return (
-
}>
+
+
+ );
+};
+
+export const UserActionCopyLink = memo(UserActionCopyLinkComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx
deleted file mode 100644
index eeb728aa7d1df..0000000000000
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPanel,
- EuiHorizontalRule,
- EuiText,
-} from '@elastic/eui';
-import React from 'react';
-import styled, { css } from 'styled-components';
-
-import { UserActionAvatar } from './user_action_avatar';
-import { UserActionTitle } from './user_action_title';
-import * as i18n from './translations';
-
-interface UserActionItemProps {
- caseConnectorName?: string;
- createdAt: string;
- 'data-test-subj'?: string;
- disabled: boolean;
- id: string;
- isEditable: boolean;
- isLoading: boolean;
- labelEditAction?: string;
- labelQuoteAction?: string;
- labelTitle?: JSX.Element;
- linkId?: string | null;
- fullName?: string | null;
- markdown?: React.ReactNode;
- onEdit?: (id: string) => void;
- onQuote?: (id: string) => void;
- username: string;
- updatedAt?: string | null;
- outlineComment?: (id: string) => void;
- showBottomFooter?: boolean;
- showTopFooter?: boolean;
- idToOutline?: string | null;
-}
-
-export const UserActionItemContainer = styled(EuiFlexGroup)`
- ${({ theme }) => css`
- & {
- background-image: linear-gradient(
- to right,
- transparent 0,
- transparent 15px,
- ${theme.eui.euiBorderColor} 15px,
- ${theme.eui.euiBorderColor} 17px,
- transparent 17px,
- transparent 100%
- );
- background-repeat: no-repeat;
- background-position: left ${theme.eui.euiSizeXXL};
- margin-bottom: ${theme.eui.euiSizeS};
- }
- .userAction__panel {
- margin-bottom: ${theme.eui.euiSize};
- }
- .userAction__circle {
- flex-shrink: 0;
- margin-right: ${theme.eui.euiSize};
- vertical-align: top;
- }
- .userAction_loadingAvatar {
- position: relative;
- margin-right: ${theme.eui.euiSizeXL};
- top: ${theme.eui.euiSizeM};
- left: ${theme.eui.euiSizeS};
- }
- .userAction__title {
- padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL};
- background: ${theme.eui.euiColorLightestShade};
- border-bottom: ${theme.eui.euiBorderThin};
- border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0;
- }
- .euiText--small * {
- margin-bottom: 0;
- }
- `}
-`;
-
-const MyEuiPanel = styled(EuiPanel)<{ showoutline: string }>`
- flex-grow: 0;
- ${({ theme, showoutline }) =>
- showoutline === 'true'
- ? `
- outline: solid 5px ${theme.eui.euiColorVis1_behindText};
- margin: 0.5em;
- transition: 0.8s;
- `
- : ''}
-`;
-
-const PushedContainer = styled(EuiFlexItem)`
- ${({ theme }) => `
- margin-top: ${theme.eui.euiSizeS};
- margin-bottom: ${theme.eui.euiSizeXL};
- hr {
- margin: 5px;
- height: ${theme.eui.euiBorderWidthThick};
- }
- `}
-`;
-
-const PushedInfoContainer = styled.div`
- margin-left: 48px;
-`;
-
-export const UserActionItem = ({
- caseConnectorName,
- createdAt,
- disabled,
- 'data-test-subj': dataTestSubj,
- id,
- idToOutline,
- isEditable,
- isLoading,
- labelEditAction,
- labelQuoteAction,
- labelTitle,
- linkId,
- fullName,
- markdown,
- onEdit,
- onQuote,
- outlineComment,
- showBottomFooter,
- showTopFooter,
- username,
- updatedAt,
-}: UserActionItemProps) => (
-
-
-
-
- {(fullName && fullName.length > 0) || (username && username.length > 0) ? (
- 0 ? fullName : username ?? ''} />
- ) : (
-
- )}
-
-
- {isEditable && markdown}
- {!isEditable && (
-
- >}
- linkId={linkId}
- onEdit={onEdit}
- onQuote={onQuote}
- outlineComment={outlineComment}
- updatedAt={updatedAt}
- username={username}
- />
- {markdown}
-
- )}
-
-
-
- {showTopFooter && (
-
-
-
- {i18n.ALREADY_PUSHED_TO_SERVICE(`${caseConnectorName}`)}
-
-
-
- {showBottomFooter && (
-
-
- {i18n.REQUIRED_UPDATE_TO_SERVICE(`${caseConnectorName}`)}
-
-
- )}
-
- )}
-
-);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx
index 6cf827ea55f1f..f1f7d40009045 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx
@@ -17,8 +17,9 @@ const onChangeEditable = jest.fn();
const onSaveContent = jest.fn();
const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c';
+const timelineMarkdown = `[timeline](http://localhost:5601/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t))`;
const defaultProps = {
- content: `A link to a timeline [timeline](http://localhost:5601/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t))`,
+ content: `A link to a timeline ${timelineMarkdown}`,
id: 'markdown-id',
isEditable: false,
onChangeEditable,
@@ -40,7 +41,11 @@ describe('UserActionMarkdown ', () => {
);
- wrapper.find(`[data-test-subj="markdown-timeline-link"]`).first().simulate('click');
+
+ wrapper
+ .find(`[data-test-subj="markdown-timeline-link-${timelineId}"]`)
+ .first()
+ .simulate('click');
expect(queryTimelineByIdSpy).toBeCalledWith({
apolloClient: mockUseApolloClient(),
@@ -59,8 +64,19 @@ describe('UserActionMarkdown ', () => {
);
- wrapper.find(`[data-test-subj="preview-tab"]`).first().simulate('click');
- wrapper.find(`[data-test-subj="markdown-timeline-link"]`).first().simulate('click');
+
+ // Preview button of Markdown editor
+ wrapper
+ .find(
+ `[data-test-subj="user-action-markdown-form"] .euiMarkdownEditorToolbar .euiButtonEmpty`
+ )
+ .first()
+ .simulate('click');
+
+ wrapper
+ .find(`[data-test-subj="markdown-timeline-link-${timelineId}"]`)
+ .first()
+ .simulate('click');
expect(queryTimelineByIdSpy).toBeCalledWith({
apolloClient: mockUseApolloClient(),
graphEventId: '',
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
index ac2ad179ec60c..45e46b2d7d2db 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
@@ -4,18 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiButton,
+ EuiMarkdownFormat,
+} from '@elastic/eui';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import * as i18n from '../case_view/translations';
-import { Markdown } from '../../../common/components/markdown';
-import { Form, useForm, UseField, useFormData } from '../../../shared_imports';
+import { Form, useForm, UseField } from '../../../shared_imports';
import { schema, Content } from './schema';
-import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
-import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
-import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
-import { useTimelineClick } from '../utils/use_timeline_click';
+import {
+ MarkdownEditorForm,
+ parsingPlugins,
+ processingPlugins,
+} from '../../../common/components/markdown_editor/eui_form';
const ContentWrapper = styled.div`
padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`};
@@ -43,24 +49,12 @@ export const UserActionMarkdown = ({
});
const fieldName = 'content';
- const { submit, setFieldValue } = form;
- const [{ content: contentFormValue }] = useFormData({ form, watch: [fieldName] });
-
- const onContentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [
- setFieldValue,
- ]);
-
- const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline(
- contentFormValue,
- onContentChange
- );
+ const { submit } = form;
const handleCancelAction = useCallback(() => {
onChangeEditable(id);
}, [id, onChangeEditable]);
- const handleTimelineClick = useTimelineClick();
-
const handleSaveAction = useCallback(async () => {
const { isValid, data } = await submit();
if (isValid) {
@@ -105,29 +99,24 @@ export const UserActionMarkdown = ({
path={fieldName}
component={MarkdownEditorForm}
componentProps={{
+ 'aria-label': 'Cases markdown editor',
+ value: content,
+ id,
bottomRightContent: renderButtons({
cancelAction: handleCancelAction,
saveAction: handleSaveAction,
}),
- onClickTimeline: handleTimelineClick,
- onCursorPositionUpdate: handleCursorChange,
- topRightContent: (
-
- ),
}}
/>
) : (
-
-
+
+
+ {content}
+
);
};
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.test.tsx
new file mode 100644
index 0000000000000..5bb0f50ce25e5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.test.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { UserActionMoveToReference } from './user_action_move_to_reference';
+
+const outlineComment = jest.fn();
+const props = {
+ id: 'move-to-ref-id',
+ outlineComment,
+};
+
+describe('UserActionMoveToReference ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount();
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find(`[data-test-subj="move-to-link-${props.id}"]`).first().exists()
+ ).toBeTruthy();
+ });
+
+ it('calls outlineComment correctly', async () => {
+ wrapper.find(`[data-test-subj="move-to-link-${props.id}"]`).first().simulate('click');
+ expect(outlineComment).toHaveBeenCalledWith(props.id);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.tsx
new file mode 100644
index 0000000000000..39d016dd69520
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo, useCallback } from 'react';
+import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
+
+import * as i18n from './translations';
+
+interface UserActionMoveToReferenceProps {
+ id: string;
+ outlineComment: (id: string) => void;
+}
+
+const UserActionMoveToReferenceComponent = ({
+ id,
+ outlineComment,
+}: UserActionMoveToReferenceProps) => {
+ const handleMoveToLink = useCallback(() => {
+ outlineComment(id);
+ }, [id, outlineComment]);
+
+ return (
+ {i18n.MOVE_TO_ORIGINAL_COMMENT}}>
+
+
+ );
+};
+
+export const UserActionMoveToReference = memo(UserActionMoveToReferenceComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.test.tsx
new file mode 100644
index 0000000000000..bd5da8aca7d4f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.test.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { UserActionPropertyActions } from './user_action_property_actions';
+
+const props = {
+ id: 'property-actions-id',
+ editLabel: 'edit',
+ quoteLabel: 'quote',
+ disabled: false,
+ isLoading: false,
+ onEdit: jest.fn(),
+ onQuote: jest.fn(),
+};
+
+describe('UserActionPropertyActions ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount();
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find('[data-test-subj="user-action-title-loading"]').first().exists()
+ ).toBeFalsy();
+
+ expect(wrapper.find('[data-test-subj="property-actions"]').first().exists()).toBeTruthy();
+ });
+
+ it('it shows the edit and quote buttons', async () => {
+ wrapper.find('[data-test-subj="property-actions-ellipses"]').first().simulate('click');
+ wrapper.find('[data-test-subj="property-actions-pencil"]').exists();
+ wrapper.find('[data-test-subj="property-actions-quote"]').exists();
+ });
+
+ it('it shows the spinner when loading', async () => {
+ wrapper = mount();
+ expect(
+ wrapper.find('[data-test-subj="user-action-title-loading"]').first().exists()
+ ).toBeTruthy();
+
+ expect(wrapper.find('[data-test-subj="property-actions"]').first().exists()).toBeFalsy();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.tsx
new file mode 100644
index 0000000000000..454880e93a27f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.tsx
@@ -0,0 +1,58 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo, useMemo, useCallback } from 'react';
+import { EuiLoadingSpinner } from '@elastic/eui';
+
+import { PropertyActions } from '../property_actions';
+
+interface UserActionPropertyActionsProps {
+ id: string;
+ editLabel: string;
+ quoteLabel: string;
+ disabled: boolean;
+ isLoading: boolean;
+ onEdit: (id: string) => void;
+ onQuote: (id: string) => void;
+}
+
+const UserActionPropertyActionsComponent = ({
+ id,
+ editLabel,
+ quoteLabel,
+ disabled,
+ isLoading,
+ onEdit,
+ onQuote,
+}: UserActionPropertyActionsProps) => {
+ const onEditClick = useCallback(() => onEdit(id), [id, onEdit]);
+ const onQuoteClick = useCallback(() => onQuote(id), [id, onQuote]);
+
+ const propertyActions = useMemo(() => {
+ return [
+ {
+ disabled,
+ iconType: 'pencil',
+ label: editLabel,
+ onClick: onEditClick,
+ },
+ {
+ disabled,
+ iconType: 'quote',
+ label: quoteLabel,
+ onClick: onQuoteClick,
+ },
+ ];
+ }, [disabled, editLabel, quoteLabel, onEditClick, onQuoteClick]);
+ return (
+ <>
+ {isLoading && }
+ {!isLoading && }
+ >
+ );
+};
+
+export const UserActionPropertyActions = memo(UserActionPropertyActionsComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.test.tsx
new file mode 100644
index 0000000000000..a65806520c854
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.test.tsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { TestProviders } from '../../../common/mock';
+import { UserActionTimestamp } from './user_action_timestamp';
+
+jest.mock('@kbn/i18n/react', () => {
+ const originalModule = jest.requireActual('@kbn/i18n/react');
+ const FormattedRelative = jest.fn();
+ FormattedRelative.mockImplementationOnce(() => '2 days ago');
+ FormattedRelative.mockImplementation(() => '20 hours ago');
+
+ return {
+ ...originalModule,
+ FormattedRelative,
+ };
+});
+
+const props = {
+ createdAt: '2020-09-06T14:40:59.889Z',
+ updatedAt: '2020-09-07T14:40:59.889Z',
+};
+
+describe('UserActionTimestamp ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find('[data-test-subj="user-action-title-creation-relative-time"]').first().exists()
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="user-action-title-edited-relative-time"]').first().exists()
+ ).toBeTruthy();
+ });
+
+ it('it shows only the created time when the updated time is missing', async () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="user-action-title-creation-relative-time"]')
+ .first()
+ .exists()
+ ).toBeTruthy();
+ expect(
+ newWrapper.find('[data-test-subj="user-action-title-edited-relative-time"]').first().exists()
+ ).toBeFalsy();
+ });
+
+ it('it shows the timestamp correctly', async () => {
+ const createdText = wrapper
+ .find('[data-test-subj="user-action-title-creation-relative-time"]')
+ .first()
+ .text();
+
+ const updatedText = wrapper
+ .find('[data-test-subj="user-action-title-edited-relative-time"]')
+ .first()
+ .text();
+
+ expect(`${createdText} (${updatedText})`).toBe('2 days ago (20 hours ago)');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.tsx
new file mode 100644
index 0000000000000..72dc5de9cdb3b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo } from 'react';
+import { EuiTextColor } from '@elastic/eui';
+import { FormattedRelative } from '@kbn/i18n/react';
+
+import { LocalizedDateTooltip } from '../../../common/components/localized_date_tooltip';
+import * as i18n from './translations';
+
+interface UserActionAvatarProps {
+ createdAt: string;
+ updatedAt?: string | null;
+}
+
+const UserActionTimestampComponent = ({ createdAt, updatedAt }: UserActionAvatarProps) => {
+ return (
+ <>
+
+
+
+ {updatedAt && (
+
+ {/* be careful of the extra space at the beginning of the parenthesis */}
+ {' ('}
+ {i18n.EDITED_FIELD}{' '}
+
+
+
+ {')'}
+
+ )}
+ >
+ );
+};
+
+export const UserActionTimestamp = memo(UserActionTimestampComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx
deleted file mode 100644
index 0bb02ce69a544..0000000000000
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { mount } from 'enzyme';
-import copy from 'copy-to-clipboard';
-import { Router, routeData, mockHistory } from '../__mock__/router';
-import { caseUserActions as basicUserActions } from '../../containers/mock';
-import { UserActionTitle } from './user_action_title';
-import { TestProviders } from '../../../common/mock';
-
-const outlineComment = jest.fn();
-const onEdit = jest.fn();
-const onQuote = jest.fn();
-
-jest.mock('copy-to-clipboard');
-const defaultProps = {
- createdAt: basicUserActions[0].actionAt,
- disabled: false,
- fullName: basicUserActions[0].actionBy.fullName,
- id: basicUserActions[0].actionId,
- isLoading: false,
- labelEditAction: 'labelEditAction',
- labelQuoteAction: 'labelQuoteAction',
- labelTitle: <>{'cool'}>,
- linkId: basicUserActions[0].commentId,
- onEdit,
- onQuote,
- outlineComment,
- updatedAt: basicUserActions[0].actionAt,
- username: basicUserActions[0].actionBy.username,
-};
-
-describe('UserActionTitle ', () => {
- beforeEach(() => {
- jest.resetAllMocks();
- jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId: '123' });
- });
-
- it('Calls copy when copy link is clicked', async () => {
- const wrapper = mount(
-
-
-
-
-
- );
- wrapper.find(`[data-test-subj="copy-link"]`).first().simulate('click');
- expect(copy).toBeCalledTimes(1);
- });
-});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx
deleted file mode 100644
index 9477299e563a8..0000000000000
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import {
- EuiLoadingSpinner,
- EuiFlexGroup,
- EuiFlexItem,
- EuiText,
- EuiButtonIcon,
- EuiToolTip,
-} from '@elastic/eui';
-import { FormattedRelative } from '@kbn/i18n/react';
-import copy from 'copy-to-clipboard';
-import { isEmpty } from 'lodash/fp';
-import React, { useMemo, useCallback } from 'react';
-import styled from 'styled-components';
-import { useParams } from 'react-router-dom';
-
-import { LocalizedDateTooltip } from '../../../common/components/localized_date_tooltip';
-import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search';
-import { navTabs } from '../../../app/home/home_navigations';
-import { PropertyActions } from '../property_actions';
-import { SecurityPageName } from '../../../app/types';
-import * as i18n from './translations';
-
-const MySpinner = styled(EuiLoadingSpinner)`
- .euiLoadingSpinner {
- margin-top: 1px; // yes it matters!
- }
-`;
-
-interface UserActionTitleProps {
- createdAt: string;
- disabled: boolean;
- id: string;
- isLoading: boolean;
- labelEditAction?: string;
- labelQuoteAction?: string;
- labelTitle: JSX.Element;
- linkId?: string | null;
- fullName?: string | null;
- updatedAt?: string | null;
- username?: string | null;
- onEdit?: (id: string) => void;
- onQuote?: (id: string) => void;
- outlineComment?: (id: string) => void;
-}
-
-export const UserActionTitle = ({
- createdAt,
- disabled,
- fullName,
- id,
- isLoading,
- labelEditAction,
- labelQuoteAction,
- labelTitle,
- linkId,
- onEdit,
- onQuote,
- outlineComment,
- updatedAt,
- username = i18n.UNKNOWN,
-}: UserActionTitleProps) => {
- const { detailName: caseId } = useParams<{ detailName: string }>();
- const urlSearch = useGetUrlSearch(navTabs.case);
- const propertyActions = useMemo(() => {
- return [
- ...(labelEditAction != null && onEdit != null
- ? [
- {
- disabled,
- iconType: 'pencil',
- label: labelEditAction,
- onClick: () => onEdit(id),
- },
- ]
- : []),
- ...(labelQuoteAction != null && onQuote != null
- ? [
- {
- disabled,
- iconType: 'quote',
- label: labelQuoteAction,
- onClick: () => onQuote(id),
- },
- ]
- : []),
- ];
- }, [disabled, id, labelEditAction, onEdit, labelQuoteAction, onQuote]);
-
- const handleAnchorLink = useCallback(() => {
- copy(
- `${window.location.origin}${window.location.pathname}#${SecurityPageName.case}/${caseId}/${id}${urlSearch}`
- );
- }, [caseId, id, urlSearch]);
-
- const handleMoveToLink = useCallback(() => {
- if (outlineComment != null && linkId != null) {
- outlineComment(linkId);
- }
- }, [linkId, outlineComment]);
- return (
-
-
-
-
-
- {fullName ?? username}}>
- {username}
-
-
- {labelTitle}
-
-
-
-
-
- {updatedAt != null && (
-
-
- {'('}
- {i18n.EDITED_FIELD}{' '}
-
-
-
- {')'}
-
-
- )}
-
-
-
-
- {!isEmpty(linkId) && (
-
- {i18n.MOVE_TO_ORIGINAL_COMMENT}}>
-
-
-
- )}
-
- {i18n.COPY_REFERENCE_LINK}}>
-
-
-
- {propertyActions.length > 0 && (
-
- {isLoading && }
- {!isLoading && }
-
- )}
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.test.tsx
new file mode 100644
index 0000000000000..008eb18aef074
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.test.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { UserActionUsername } from './user_action_username';
+
+const props = {
+ username: 'elastic',
+ fullName: 'Elastic',
+};
+
+describe('UserActionUsername ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount();
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find('[data-test-subj="user-action-username-tooltip"]').first().exists()
+ ).toBeTruthy();
+ });
+
+ it('it shows the username', async () => {
+ expect(wrapper.find('[data-test-subj="user-action-username-tooltip"]').text()).toBe('elastic');
+ });
+
+ test('it shows the fullname when hovering the username', () => {
+ // Use fake timers so we don't have to wait for the EuiToolTip timeout
+ jest.useFakeTimers();
+
+ wrapper.find('[data-test-subj="user-action-username-tooltip"]').first().simulate('mouseOver');
+
+ // Run the timers so the EuiTooltip will be visible
+ jest.runAllTimers();
+
+ wrapper.update();
+ expect(wrapper.find('.euiToolTipPopover').text()).toBe('Elastic');
+
+ // Clearing all mocks will also reset fake timers.
+ jest.clearAllMocks();
+ });
+
+ test('it shows the username when hovering the username and the fullname is missing', () => {
+ // Use fake timers so we don't have to wait for the EuiToolTip timeout
+ jest.useFakeTimers();
+
+ const newWrapper = mount();
+ newWrapper
+ .find('[data-test-subj="user-action-username-tooltip"]')
+ .first()
+ .simulate('mouseOver');
+
+ // Run the timers so the EuiTooltip will be visible
+ jest.runAllTimers();
+
+ newWrapper.update();
+ expect(newWrapper.find('.euiToolTipPopover').text()).toBe('elastic');
+
+ // Clearing all mocks will also reset fake timers.
+ jest.clearAllMocks();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx
new file mode 100644
index 0000000000000..dbc153ddbe577
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo } from 'react';
+import { EuiToolTip } from '@elastic/eui';
+import { isEmpty } from 'lodash/fp';
+
+interface UserActionUsernameProps {
+ username: string;
+ fullName?: string;
+}
+
+const UserActionUsernameComponent = ({ username, fullName }: UserActionUsernameProps) => {
+ return (
+ {isEmpty(fullName) ? username : fullName}}
+ data-test-subj="user-action-username-tooltip"
+ >
+ {username}
+
+ );
+};
+
+export const UserActionUsername = memo(UserActionUsernameComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.test.tsx
new file mode 100644
index 0000000000000..f8403738c24ea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.test.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar';
+
+const props = {
+ username: 'elastic',
+ fullName: 'Elastic',
+};
+
+describe('UserActionUsernameWithAvatar ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount();
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find('[data-test-subj="user-action-username-with-avatar"]').first().exists()
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="user-action-username-avatar"]').first().exists()
+ ).toBeTruthy();
+ });
+
+ it('it shows the avatar', async () => {
+ expect(wrapper.find('[data-test-subj="user-action-username-avatar"]').first().text()).toBe('E');
+ });
+
+ it('it shows the avatar without fullName', async () => {
+ const newWrapper = mount();
+ expect(newWrapper.find('[data-test-subj="user-action-username-avatar"]').first().text()).toBe(
+ 'e'
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx
new file mode 100644
index 0000000000000..e2326a3580e6f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiAvatar } from '@elastic/eui';
+import { isEmpty } from 'lodash/fp';
+
+import { UserActionUsername } from './user_action_username';
+
+interface UserActionUsernameWithAvatarProps {
+ username: string;
+ fullName?: string;
+}
+
+const UserActionUsernameWithAvatarComponent = ({
+ username,
+ fullName,
+}: UserActionUsernameWithAvatarProps) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export const UserActionUsernameWithAvatar = memo(UserActionUsernameWithAvatarComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
index 403c8d838fa44..89fcc67bcd15f 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
@@ -16,25 +16,40 @@ export { getDetectionEngineUrl } from './redirect_to_detection_engine';
export { getAppOverviewUrl } from './redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts';
export { getNetworkUrl, getNetworkDetailsUrl } from './redirect_to_network';
-export { getTimelinesUrl, getTimelineTabsUrl } from './redirect_to_timelines';
+export { getTimelinesUrl, getTimelineTabsUrl, getTimelineUrl } from './redirect_to_timelines';
export {
getCaseDetailsUrl,
getCaseUrl,
getCreateCaseUrl,
getConfigureCasesUrl,
+ getCaseDetailsUrlWithCommentId,
} from './redirect_to_case';
+interface FormatUrlOptions {
+ absolute: boolean;
+ skipSearch: boolean;
+}
+
+type FormatUrl = (path: string, options?: Partial) => string;
+
export const useFormatUrl = (page: SecurityPageName) => {
const { getUrlForApp } = useKibana().services.application;
const search = useGetUrlSearch(navTabs[page]);
- const formatUrl = useCallback(
- (path: string) => {
+ const formatUrl = useCallback(
+ (path: string, { absolute = false, skipSearch = false } = {}) => {
const pathArr = path.split('?');
const formattedPath = `${pathArr[0]}${
- isEmpty(pathArr[1]) ? search : `${pathArr[1]}${isEmpty(search) ? '' : `&${search}`}`
+ !skipSearch
+ ? isEmpty(pathArr[1])
+ ? search
+ : `?${pathArr[1]}${isEmpty(search) ? '' : `&${search}`}`
+ : isEmpty(pathArr[1])
+ ? ''
+ : `?${pathArr[1]}`
}`;
return getUrlForApp(`${APP_ID}:${page}`, {
path: formattedPath,
+ absolute,
});
},
[getUrlForApp, page, search]
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
index 7005460999fc7..3ef00635844f6 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
@@ -11,6 +11,17 @@ export const getCaseUrl = (search: string | null) => `${appendSearch(search ?? u
export const getCaseDetailsUrl = ({ id, search }: { id: string; search?: string | null }) =>
`/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`;
+export const getCaseDetailsUrlWithCommentId = ({
+ id,
+ commentId,
+ search,
+}: {
+ id: string;
+ commentId: string;
+ search?: string | null;
+}) =>
+ `/${encodeURIComponent(id)}/${encodeURIComponent(commentId)}${appendSearch(search ?? undefined)}`;
+
export const getCreateCaseUrl = (search?: string | null) =>
`/create${appendSearch(search ?? undefined)}`;
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
index 75a2fa1efa414..58b9f940ceaa6 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { isEmpty } from 'lodash/fp';
import { TimelineTypeLiteral } from '../../../../common/types/timeline';
import { appendSearch } from './helpers';
@@ -11,3 +12,8 @@ export const getTimelinesUrl = (search?: string) => `${appendSearch(search)}`;
export const getTimelineTabsUrl = (tabName: TimelineTypeLiteral, search?: string) =>
`/${tabName}${appendSearch(search)}`;
+
+export const getTimelineUrl = (id: string, graphEventId?: string) =>
+ `?timeline=(id:'${id}',isOpen:!t${
+ isEmpty(graphEventId) ? ')' : `,graphEventId:'${graphEventId}')`
+ }`;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
new file mode 100644
index 0000000000000..481ed7892a8be
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, useCallback } from 'react';
+import styled from 'styled-components';
+import {
+ EuiMarkdownEditor,
+ EuiMarkdownEditorProps,
+ EuiFormRow,
+ EuiFlexItem,
+ EuiFlexGroup,
+ getDefaultEuiMarkdownParsingPlugins,
+ getDefaultEuiMarkdownProcessingPlugins,
+} from '@elastic/eui';
+import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
+
+import * as timelineMarkdownPlugin from './plugins/timeline';
+
+type MarkdownEditorFormProps = EuiMarkdownEditorProps & {
+ id: string;
+ field: FieldHook;
+ dataTestSubj: string;
+ idAria: string;
+ isDisabled?: boolean;
+ bottomRightContent?: React.ReactNode;
+};
+
+const BottomContentWrapper = styled(EuiFlexGroup)`
+ ${({ theme }) => `
+ padding: ${theme.eui.ruleMargins.marginSmall} 0;
+ `}
+`;
+
+export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
+parsingPlugins.push(timelineMarkdownPlugin.parser);
+
+export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins();
+processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer;
+
+export const MarkdownEditorForm: React.FC = ({
+ id,
+ field,
+ dataTestSubj,
+ idAria,
+ bottomRightContent,
+}) => {
+ const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
+ const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]);
+ const onParse = useCallback((err, { messages }) => {
+ setMarkdownErrorMessages(err ? [err] : messages);
+ }, []);
+
+ return (
+
+ <>
+
+ {bottomRightContent && (
+
+ {bottomRightContent}
+
+ )}
+ >
+
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx
deleted file mode 100644
index 2cc3fe05a2215..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiFormRow } from '@elastic/eui';
-import React, { useCallback } from 'react';
-
-import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
-import { CursorPosition, MarkdownEditor } from '.';
-
-interface IMarkdownEditorForm {
- bottomRightContent?: React.ReactNode;
- dataTestSubj: string;
- field: FieldHook;
- idAria: string;
- isDisabled: boolean;
- onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
- onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void;
- placeholder?: string;
- topRightContent?: React.ReactNode;
-}
-export const MarkdownEditorForm = ({
- bottomRightContent,
- dataTestSubj,
- field,
- idAria,
- isDisabled = false,
- onClickTimeline,
- onCursorPositionUpdate,
- placeholder,
- topRightContent,
-}: IMarkdownEditorForm) => {
- const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
-
- const handleContentChange = useCallback(
- (newContent: string) => {
- field.setValue(newContent);
- },
- [field]
- );
-
- return (
-
-
-
- );
-};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx
deleted file mode 100644
index b5e5b01189418..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { mount } from 'enzyme';
-import React from 'react';
-
-import { MarkdownEditor } from '.';
-import { TestProviders } from '../../mock';
-
-describe('Markdown Editor', () => {
- const onChange = jest.fn();
- const onCursorPositionUpdate = jest.fn();
- const defaultProps = {
- content: 'hello world',
- onChange,
- onCursorPositionUpdate,
- };
- beforeEach(() => {
- jest.clearAllMocks();
- });
- test('it calls onChange with correct value', () => {
- const wrapper = mount(
-
-
-
- );
- const newValue = 'a new string';
- wrapper
- .find(`[data-test-subj="textAreaInput"]`)
- .first()
- .simulate('change', { target: { value: newValue } });
- expect(onChange).toBeCalledWith(newValue);
- });
- test('it calls onCursorPositionUpdate with correct args', () => {
- const wrapper = mount(
-
-
-
- );
- wrapper.find(`[data-test-subj="textAreaInput"]`).first().simulate('blur');
- expect(onCursorPositionUpdate).toBeCalledWith({
- start: 0,
- end: 0,
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
index d4ad4a11b60a3..9f4141dbcae7d 100644
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
@@ -4,167 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiLink,
- EuiPanel,
- EuiTabbedContent,
- EuiTextArea,
-} from '@elastic/eui';
-import React, { useMemo, useCallback, ChangeEvent } from 'react';
-import styled, { css } from 'styled-components';
-
-import { Markdown } from '../markdown';
-import * as i18n from './translations';
-import { MARKDOWN_HELP_LINK } from './constants';
-
-const TextArea = styled(EuiTextArea)`
- width: 100%;
-`;
-
-const Container = styled(EuiPanel)`
- ${({ theme }) => css`
- padding: 0;
- background: ${theme.eui.euiColorLightestShade};
- position: relative;
- .markdown-tabs-header {
- position: absolute;
- top: ${theme.eui.euiSizeS};
- right: ${theme.eui.euiSizeS};
- z-index: ${theme.eui.euiZContentMenu};
- }
- .euiTab {
- padding: 10px;
- }
- .markdown-tabs {
- width: 100%;
- }
- .markdown-tabs-footer {
- height: 41px;
- padding: 0 ${theme.eui.euiSizeM};
- .euiLink {
- font-size: ${theme.eui.euiSizeM};
- }
- }
- .euiFormRow__labelWrapper {
- position: absolute;
- top: -${theme.eui.euiSizeL};
- }
- .euiFormErrorText {
- padding: 0 ${theme.eui.euiSizeM};
- }
- `}
-`;
-
-const MarkdownContainer = styled(EuiPanel)`
- min-height: 150px;
- overflow: auto;
-`;
-
-export interface CursorPosition {
- start: number;
- end: number;
-}
-
-/** An input for entering a new case description */
-export const MarkdownEditor = React.memo<{
- bottomRightContent?: React.ReactNode;
- topRightContent?: React.ReactNode;
- content: string;
- isDisabled?: boolean;
- onChange: (description: string) => void;
- onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
- onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void;
- placeholder?: string;
-}>(
- ({
- bottomRightContent,
- topRightContent,
- content,
- isDisabled = false,
- onChange,
- onClickTimeline,
- placeholder,
- onCursorPositionUpdate,
- }) => {
- const handleOnChange = useCallback(
- (evt: ChangeEvent) => {
- onChange(evt.target.value);
- },
- [onChange]
- );
-
- const setCursorPosition = useCallback(
- (e: React.ChangeEvent) => {
- if (onCursorPositionUpdate) {
- onCursorPositionUpdate({
- start: e!.target!.selectionStart ?? 0,
- end: e!.target!.selectionEnd ?? 0,
- });
- }
- },
- [onCursorPositionUpdate]
- );
-
- const tabs = useMemo(
- () => [
- {
- id: 'comment',
- name: i18n.MARKDOWN,
- content: (
-
- ),
- },
- {
- id: 'preview',
- name: i18n.PREVIEW,
- 'data-test-subj': 'preview-tab',
- content: (
-
-
-
- ),
- },
- ],
- [content, handleOnChange, isDisabled, onClickTimeline, placeholder, setCursorPosition]
- );
- return (
-
- {topRightContent && {topRightContent}
}
-
-
-
-
- {i18n.MARKDOWN_SYNTAX_HELP}
-
-
- {bottomRightContent && {bottomRightContent}}
-
-
- );
- }
-);
-
-MarkdownEditor.displayName = 'MarkdownEditor';
+export * from './types';
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/constants.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/constants.ts
new file mode 100644
index 0000000000000..917000a8ba21c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const ID = 'timeline';
+export const PREFIX = `[`;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/index.ts
new file mode 100644
index 0000000000000..701889013ee53
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { plugin } from './plugin';
+import { TimelineParser } from './parser';
+import { TimelineMarkDownRenderer } from './processor';
+
+export { plugin, TimelineParser as parser, TimelineMarkDownRenderer as renderer };
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/parser.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/parser.ts
new file mode 100644
index 0000000000000..d322a2c9e1929
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/parser.ts
@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Plugin } from '@elastic/eui/node_modules/unified';
+import { RemarkTokenizer } from '@elastic/eui';
+import { parse } from 'query-string';
+import { decodeRisonUrlState } from '../../../url_state/helpers';
+import { ID, PREFIX } from './constants';
+import * as i18n from './translations';
+
+export const TimelineParser: Plugin = function () {
+ const Parser = this.Parser;
+ const tokenizers = Parser.prototype.inlineTokenizers;
+ const methods = Parser.prototype.inlineMethods;
+
+ const parseTimeline: RemarkTokenizer = function (eat, value, silent) {
+ let index = 0;
+ const nextChar = value[index];
+
+ if (nextChar !== '[') {
+ return false;
+ }
+
+ if (silent) {
+ return true;
+ }
+
+ function readArg(open: string, close: string) {
+ if (value[index] !== open) {
+ throw new Error(i18n.NO_PARENTHESES);
+ }
+
+ index++;
+
+ let body = '';
+ let openBrackets = 0;
+
+ for (; index < value.length; index++) {
+ const char = value[index];
+
+ if (char === close && openBrackets === 0) {
+ index++;
+ return body;
+ } else if (char === close) {
+ openBrackets--;
+ } else if (char === open) {
+ openBrackets++;
+ }
+
+ body += char;
+ }
+
+ return '';
+ }
+
+ const timelineTitle = readArg('[', ']');
+ const timelineUrl = readArg('(', ')');
+ const now = eat.now();
+
+ if (!timelineTitle) {
+ this.file.info(i18n.NO_TIMELINE_NAME_FOUND, {
+ line: now.line,
+ column: now.column,
+ });
+ return false;
+ }
+
+ try {
+ const timelineSearch = timelineUrl.split('?');
+ const parseTimelineUrlSearch = parse(timelineSearch[1]) as { timeline: string };
+ const { id: timelineId = '', graphEventId = '' } = decodeRisonUrlState(
+ parseTimelineUrlSearch.timeline ?? ''
+ ) ?? { id: null, graphEventId: '' };
+
+ if (!timelineId) {
+ this.file.info(i18n.NO_TIMELINE_ID_FOUND, {
+ line: now.line,
+ column: now.column + timelineUrl.indexOf('id'),
+ });
+ return false;
+ }
+
+ return eat(`[${timelineTitle}](${timelineUrl})`)({
+ type: ID,
+ id: timelineId,
+ title: timelineTitle,
+ graphEventId,
+ });
+ } catch {
+ this.file.info(i18n.TIMELINE_URL_IS_NOT_VALID(timelineUrl), {
+ line: now.line,
+ column: now.column,
+ });
+ }
+
+ return false;
+ };
+
+ const tokenizeTimeline: RemarkTokenizer = function tokenizeTimeline(eat, value, silent) {
+ if (
+ value.startsWith(PREFIX) === false ||
+ (value.startsWith(PREFIX) === true && !value.includes('timelines?timeline=(id'))
+ ) {
+ return false;
+ }
+
+ return parseTimeline.call(this, eat, value, silent);
+ };
+
+ tokenizeTimeline.locator = (value: string, fromIndex: number) => {
+ return value.indexOf(PREFIX, fromIndex);
+ };
+
+ tokenizers.timeline = tokenizeTimeline;
+ methods.splice(methods.indexOf('url'), 0, ID);
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx
new file mode 100644
index 0000000000000..8d2488b269d76
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback, memo } from 'react';
+import {
+ EuiSelectableOption,
+ EuiModalBody,
+ EuiMarkdownEditorUiPlugin,
+ EuiCodeBlock,
+} from '@elastic/eui';
+
+import { TimelineType } from '../../../../../../common/types/timeline';
+import { SelectableTimeline } from '../../../../../timelines/components/timeline/selectable_timeline';
+import { OpenTimelineResult } from '../../../../../timelines/components/open_timeline/types';
+import { getTimelineUrl, useFormatUrl } from '../../../link_to';
+
+import { ID } from './constants';
+import * as i18n from './translations';
+import { SecurityPageName } from '../../../../../app/types';
+
+interface TimelineEditorProps {
+ onClosePopover: () => void;
+ onInsert: (markdown: string, config: { block: boolean }) => void;
+}
+
+const TimelineEditorComponent: React.FC = ({ onClosePopover, onInsert }) => {
+ const { formatUrl } = useFormatUrl(SecurityPageName.timelines);
+
+ const handleGetSelectableOptions = useCallback(
+ ({ timelines }: { timelines: OpenTimelineResult[] }) => [
+ ...timelines.map(
+ (t: OpenTimelineResult, index: number) =>
+ ({
+ description: t.description,
+ favorite: t.favorite,
+ label: t.title,
+ id: t.savedObjectId,
+ key: `${t.title}-${index}`,
+ title: t.title,
+ checked: undefined,
+ } as EuiSelectableOption)
+ ),
+ ],
+ []
+ );
+
+ return (
+
+ {
+ const url = formatUrl(getTimelineUrl(timelineId ?? '', graphEventId), {
+ absolute: true,
+ skipSearch: true,
+ });
+ onInsert(`[${timelineTitle}](${url})`, {
+ block: false,
+ });
+ }}
+ onClosePopover={onClosePopover}
+ timelineType={TimelineType.default}
+ />
+
+ );
+};
+
+const TimelineEditor = memo(TimelineEditorComponent);
+
+export const plugin: EuiMarkdownEditorUiPlugin = {
+ name: ID,
+ button: {
+ label: i18n.INSERT_TIMELINE,
+ iconType: 'timeline',
+ },
+ helpText: (
+
+ {'[title](url)'}
+
+ ),
+ editor: function editor({ node, onSave, onCancel }) {
+ return ;
+ },
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx
new file mode 100644
index 0000000000000..fb72b4368c8ea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback, memo } from 'react';
+import { EuiToolTip, EuiLink, EuiMarkdownAstNodePosition } from '@elastic/eui';
+
+import { useTimelineClick } from '../../../../utils/timeline/use_timeline_click';
+import { TimelineProps } from './types';
+import * as i18n from './translations';
+
+export const TimelineMarkDownRendererComponent: React.FC<
+ TimelineProps & {
+ position: EuiMarkdownAstNodePosition;
+ }
+> = ({ id, title, graphEventId }) => {
+ const handleTimelineClick = useTimelineClick();
+ const onClickTimeline = useCallback(() => handleTimelineClick(id ?? '', graphEventId), [
+ id,
+ graphEventId,
+ handleTimelineClick,
+ ]);
+ return (
+
+
+ {title}
+
+
+ );
+};
+
+export const TimelineMarkDownRenderer = memo(TimelineMarkDownRendererComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts
new file mode 100644
index 0000000000000..5a23b2a742157
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const INSERT_TIMELINE = i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.insertTimelineButtonLabel',
+ {
+ defaultMessage: 'Insert timeline link',
+ }
+);
+
+export const TIMELINE_ID = (timelineId: string) =>
+ i18n.translate('xpack.securitySolution.markdownEditor.plugins.timeline.toolTip.timelineId', {
+ defaultMessage: 'Timeline id: { timelineId }',
+ values: {
+ timelineId,
+ },
+ });
+
+export const NO_TIMELINE_NAME_FOUND = i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.noTimelineNameFoundErrorMsg',
+ {
+ defaultMessage: 'No timeline name found',
+ }
+);
+
+export const NO_TIMELINE_ID_FOUND = i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.noTimelineIdFoundErrorMsg',
+ {
+ defaultMessage: 'No timeline id found',
+ }
+);
+
+export const TIMELINE_URL_IS_NOT_VALID = (timelineUrl: string) =>
+ i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.toolTip.timelineUrlIsNotValidErrorMsg',
+ {
+ defaultMessage: 'Timeline URL is not valid => {timelineUrl}',
+ values: {
+ timelineUrl,
+ },
+ }
+ );
+
+export const NO_PARENTHESES = i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.noParenthesesErrorMsg',
+ {
+ defaultMessage: 'Expected left parentheses',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/types.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/types.ts
new file mode 100644
index 0000000000000..8b9111fc9fc7d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/types.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ID } from './constants';
+
+export interface TimelineConfiguration {
+ id: string | null;
+ title: string;
+ graphEventId?: string;
+ [key: string]: string | null | undefined;
+}
+
+export interface TimelineProps extends TimelineConfiguration {
+ type: typeof ID;
+}
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/types.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/types.ts
new file mode 100644
index 0000000000000..030def21ac36f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/types.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface CursorPosition {
+ start: number;
+ end: number;
+}
diff --git a/x-pack/plugins/security_solution/public/cases/components/utils/use_timeline_click.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_timeline_click.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/cases/components/utils/use_timeline_click.tsx
rename to x-pack/plugins/security_solution/public/common/utils/timeline/use_timeline_click.tsx
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
index d2c84883fa99b..66f95f5ce15d2 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
@@ -36,7 +36,7 @@ import { schema } from './schema';
import * as I18n from './translations';
import { StepContentWrapper } from '../step_content_wrapper';
import { NextStep } from '../next_step';
-import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/form';
+import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/eui_form';
import { SeverityField } from '../severity_mapping';
import { RiskScoreField } from '../risk_score_mapping';
import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx
index 55c0709bd5543..f1f419fd4b52a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx
@@ -4,17 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { isEmpty } from 'lodash/fp';
import { useCallback, useState, useEffect } from 'react';
import { useDispatch, useSelector, shallowEqual } from 'react-redux';
-import { useBasePath } from '../../../../common/lib/kibana';
+import { SecurityPageName } from '../../../../../common/constants';
+import { getTimelineUrl, useFormatUrl } from '../../../../common/components/link_to';
import { CursorPosition } from '../../../../common/components/markdown_editor';
import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline';
import { setInsertTimeline } from '../../../store/timeline/actions';
export const useInsertTimeline = (value: string, onChange: (newValue: string) => void) => {
- const basePath = window.location.origin + useBasePath();
const dispatch = useDispatch();
+ const { formatUrl } = useFormatUrl(SecurityPageName.timelines);
const [cursorPosition, setCursorPosition] = useState({
start: 0,
end: 0,
@@ -24,21 +24,22 @@ export const useInsertTimeline = (value: string, onChange: (newValue: string) =>
const handleOnTimelineChange = useCallback(
(title: string, id: string | null, graphEventId?: string) => {
- const builtLink = `${basePath}/app/security/timelines?timeline=(id:'${id}'${
- !isEmpty(graphEventId) ? `,graphEventId:'${graphEventId}'` : ''
- },isOpen:!t)`;
+ const url = formatUrl(getTimelineUrl(id ?? '', graphEventId), {
+ absolute: true,
+ skipSearch: true,
+ });
const newValue: string = [
value.slice(0, cursorPosition.start),
cursorPosition.start === cursorPosition.end
- ? `[${title}](${builtLink})`
- : `[${value.slice(cursorPosition.start, cursorPosition.end)}](${builtLink})`,
+ ? `[${title}](${url})`
+ : `[${value.slice(cursorPosition.start, cursorPosition.end)}](${url})`,
value.slice(cursorPosition.end),
].join('');
onChange(newValue);
},
- [value, onChange, basePath, cursorPosition]
+ [value, onChange, cursorPosition, formatUrl]
);
const handleCursorChange = useCallback((cp: CursorPosition) => {
From 52e78e24374479e208cb7bd1e572fb8570682826 Mon Sep 17 00:00:00 2001
From: Lisa Cawley
Date: Thu, 17 Sep 2020 17:19:27 -0700
Subject: [PATCH 06/10] Fixes typo in data recognizer text (#77691) (#77837)
---
.../modules/siem_auditbeat/ml/linux_rare_user_compiler.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json
index 245b7e0819c7d..bb0323ed9ae78 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json
@@ -1,6 +1,6 @@
{
"job_type": "anomaly_detector",
- "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privliege elevation via locally run exploits or malware activity.",
+ "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privilege elevation via locally run exploits or malware activity.",
"groups": [
"security",
"auditbeat",
From ae6f0fa9b1b5688804bb99fcacc136e41a69767b Mon Sep 17 00:00:00 2001
From: Spencer
Date: Thu, 17 Sep 2020 21:28:29 -0700
Subject: [PATCH 07/10] [7.x] remove visual aspects of baseline job (#77815)
(#77842)
Co-authored-by: spalger
Co-authored-by: Elastic Machine
Co-authored-by: spalger
Co-authored-by: Elastic Machine
---
.ci/Jenkinsfile_baseline_capture | 12 ++++++------
...kins_visual_regression.sh => jenkins_baseline.sh} | 7 -------
...isual_regression.sh => jenkins_xpack_baseline.sh} | 11 -----------
vars/kibanaPipeline.groovy | 8 +++++---
vars/workers.groovy | 4 ++--
5 files changed, 13 insertions(+), 29 deletions(-)
rename test/scripts/{jenkins_visual_regression.sh => jenkins_baseline.sh} (63%)
rename test/scripts/{jenkins_xpack_visual_regression.sh => jenkins_xpack_baseline.sh} (64%)
diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture
index 9a49c19b94df2..33ecfcd84fd3e 100644
--- a/.ci/Jenkinsfile_baseline_capture
+++ b/.ci/Jenkinsfile_baseline_capture
@@ -11,14 +11,14 @@ kibanaPipeline(timeoutMinutes: 120) {
'CI_PARALLEL_PROCESS_NUMBER=1'
]) {
parallel([
- 'oss-visualRegression': {
- workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) {
- kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')()
+ 'oss-baseline': {
+ workers.ci(name: 'oss-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) {
+ kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh')()
}
},
- 'xpack-visualRegression': {
- workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) {
- kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')()
+ 'xpack-baseline': {
+ workers.ci(name: 'xpack-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) {
+ kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh')()
}
},
])
diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_baseline.sh
similarity index 63%
rename from test/scripts/jenkins_visual_regression.sh
rename to test/scripts/jenkins_baseline.sh
index 17345d4301882..e679ac7f31bd1 100755
--- a/test/scripts/jenkins_visual_regression.sh
+++ b/test/scripts/jenkins_baseline.sh
@@ -9,10 +9,3 @@ linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')"
installDir="$PARENT_DIR/install/kibana"
mkdir -p "$installDir"
tar -xzf "$linuxBuild" -C "$installDir" --strip=1
-
-echo " -> running visual regression tests from kibana directory"
-yarn percy exec -t 10000 -- -- \
- node scripts/functional_tests \
- --debug --bail \
- --kibana-install-dir "$installDir" \
- --config test/visual_regression/config.ts;
diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_baseline.sh
similarity index 64%
rename from test/scripts/jenkins_xpack_visual_regression.sh
rename to test/scripts/jenkins_xpack_baseline.sh
index 55d4a524820c5..7577b6927d166 100755
--- a/test/scripts/jenkins_xpack_visual_regression.sh
+++ b/test/scripts/jenkins_xpack_baseline.sh
@@ -14,16 +14,5 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1
mkdir -p "$WORKSPACE/kibana-build-xpack"
cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/
-# cd "$KIBANA_DIR"
-# source "test/scripts/jenkins_xpack_page_load_metrics.sh"
-
cd "$KIBANA_DIR"
source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh"
-
-echo " -> running visual regression tests from x-pack directory"
-cd "$XPACK_DIR"
-yarn percy exec -t 10000 -- -- \
- node scripts/functional_tests \
- --debug --bail \
- --kibana-install-dir "$installDir" \
- --config test/visual_regression/config.ts;
diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy
index 33ecefa13e862..3b5f2f55cc4e6 100644
--- a/vars/kibanaPipeline.groovy
+++ b/vars/kibanaPipeline.groovy
@@ -1,4 +1,4 @@
-def withPostBuildReporting(Closure closure) {
+def withPostBuildReporting(Map params, Closure closure) {
try {
closure()
} finally {
@@ -9,8 +9,10 @@ def withPostBuildReporting(Closure closure) {
print ex
}
- catchErrors {
- runErrorReporter([pwd()] + parallelWorkspaces)
+ if (params.runErrorReporter) {
+ catchErrors {
+ runErrorReporter([pwd()] + parallelWorkspaces)
+ }
}
catchErrors {
diff --git a/vars/workers.groovy b/vars/workers.groovy
index e582e996a78b5..b6ff5b27667dd 100644
--- a/vars/workers.groovy
+++ b/vars/workers.groovy
@@ -118,11 +118,11 @@ def base(Map params, Closure closure) {
// Worker for ci processes. Extends the base worker and adds GCS artifact upload, error reporting, junit processing
def ci(Map params, Closure closure) {
- def config = [ramDisk: true, bootstrapped: true] + params
+ def config = [ramDisk: true, bootstrapped: true, runErrorReporter: true] + params
return base(config) {
kibanaPipeline.withGcsArtifactUpload(config.name) {
- kibanaPipeline.withPostBuildReporting {
+ kibanaPipeline.withPostBuildReporting(config) {
closure()
}
}
From b484269c836eb912ded28b6efc449eed6f0a91a0 Mon Sep 17 00:00:00 2001
From: spalger
Date: Thu, 17 Sep 2020 22:35:21 -0700
Subject: [PATCH 08/10] skip flaky suite (#76239)
(cherry picked from commit 61226103740595ad20e1c96f5ce8dbf627ce5e50)
---
.../security_api_integration/tests/session_idle/cleanup.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts
index c4302b7637923..f288bc925123e 100644
--- a/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts
+++ b/x-pack/test/security_api_integration/tests/session_idle/cleanup.ts
@@ -33,7 +33,8 @@ export default function ({ getService }: FtrProviderContext) {
return (await es.search({ index: '.kibana_security_session*' })).hits.total.value;
}
- describe('Session Idle cleanup', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/76239
+ describe.skip('Session Idle cleanup', () => {
beforeEach(async () => {
await es.cluster.health({ index: '.kibana_security_session*', waitForStatus: 'green' });
await es.deleteByQuery({
From 1567be984d0ac04b7865983d219446b1f80be3c6 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 18 Sep 2020 08:58:04 +0300
Subject: [PATCH 09/10] Aligns the y axis settings on horizontal mode (#77585)
(#77781)
---
.../shared_components/toolbar_popover.tsx | 1 +
.../xy_visualization/xy_config_panel.test.tsx | 46 ++++++++++++++++++-
.../xy_visualization/xy_config_panel.tsx | 45 +++++++++++++-----
3 files changed, 79 insertions(+), 13 deletions(-)
diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx
index 98f5878ec927e..07baf29fdd32a 100644
--- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx
+++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx
@@ -56,6 +56,7 @@ export const ToolbarPopover: React.FunctionComponent = ({
onClick={() => {
setOpen(!open);
}}
+ title={title}
hasArrow={false}
isDisabled={isDisabled}
groupPosition={groupPosition}
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
index 7e2e8f0453588..2114d63fcfacd 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers';
import { EuiButtonGroupProps, EuiSuperSelect, EuiButtonGroup } from '@elastic/eui';
-import { LayerContextMenu, XyToolbar } from './xy_config_panel';
+import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel';
import { ToolbarPopover } from '../shared_components';
import { AxisSettingsPopover } from './axis_settings_popover';
import { FramePublicAPI } from '../types';
@@ -171,4 +171,48 @@ describe('XY Config panels', () => {
expect(component.find(AxisSettingsPopover).length).toEqual(3);
});
});
+
+ describe('Dimension Editor', () => {
+ test('shows the correct axis side options when in horizontal mode', () => {
+ const state = testState();
+ const component = mount(
+
+ );
+
+ const options = component
+ .find(EuiButtonGroup)
+ .first()
+ .prop('options') as EuiButtonGroupProps['options'];
+
+ expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Bottom', 'Top']);
+ });
+
+ test('shows the default axis side options when not in horizontal mode', () => {
+ const state = testState();
+ const component = mount(
+
+ );
+
+ const options = component
+ .find(EuiButtonGroup)
+ .first()
+ .prop('options') as EuiButtonGroupProps['options'];
+
+ expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Left', 'Right']);
+ });
+ });
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
index bc98bf53d9f12..4aa5bd62c05a5 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
@@ -274,9 +274,15 @@ export function XyToolbar(props: VisualizationToolbarProps) {
group.groupId === 'left') || {}).length === 0
}
@@ -310,9 +316,15 @@ export function XyToolbar(props: VisualizationToolbarProps) {
toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange}
/>
group.groupId === 'right') || {}).length === 0
}
@@ -345,6 +357,7 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps)
const { state, setState, layerId, accessor } = props;
const index = state.layers.findIndex((l) => l.layerId === layerId);
const layer = state.layers[index];
+ const isHorizontal = isHorizontalChart(state.layers);
const axisMode =
(layer.yConfig &&
layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) ||
@@ -377,15 +390,23 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps)
},
{
id: `${idPrefix}left`,
- label: i18n.translate('xpack.lens.xyChart.axisSide.left', {
- defaultMessage: 'Left',
- }),
+ label: isHorizontal
+ ? i18n.translate('xpack.lens.xyChart.axisSide.bottom', {
+ defaultMessage: 'Bottom',
+ })
+ : i18n.translate('xpack.lens.xyChart.axisSide.left', {
+ defaultMessage: 'Left',
+ }),
},
{
id: `${idPrefix}right`,
- label: i18n.translate('xpack.lens.xyChart.axisSide.right', {
- defaultMessage: 'Right',
- }),
+ label: isHorizontal
+ ? i18n.translate('xpack.lens.xyChart.axisSide.top', {
+ defaultMessage: 'Top',
+ })
+ : i18n.translate('xpack.lens.xyChart.axisSide.right', {
+ defaultMessage: 'Right',
+ }),
},
]}
idSelected={`${idPrefix}${axisMode}`}
From 5242103e4a3d835803ad9039f5dfcbe5665cfbf3 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 18 Sep 2020 08:58:35 +0300
Subject: [PATCH 10/10] Use Search API in TSVB (#76274) (#77767)
* Use Search API for TSVB
* Fixed ci
* Fixed ci
* Use constants
* Fixed tests
* Fixed ci
* Fixed ci
* Back old rollup search
* Fixed test
* Fixed tests
* Fixed issue with series data
* Fixed comments
* Fixed comments
* Fixed unit test
* Deleted unused import
* Fixed comments
Co-authored-by: Elastic Machine
Co-authored-by: Uladzislau Lasitsa
Co-authored-by: Elastic Machine
---
.../vis_type_timeseries/server/index.ts | 10 +--
.../server/lib/get_fields.ts | 17 +----
.../server/lib/get_vis_data.ts | 19 +----
.../search_requests/abstract_request.js | 28 -------
.../search_requests/abstract_request.test.js | 46 -----------
.../search_requests/multi_search_request.js | 49 ------------
.../multi_search_request.test.js | 67 ----------------
.../search_requests/search_request.js | 37 ---------
.../search_requests/search_request.test.js | 76 -------------------
.../search_requests/single_search_request.js | 37 ---------
.../single_search_request.test.js | 59 --------------
.../search_strategies_registry.test.ts | 4 +-
.../abstract_search_strategy.test.js | 69 ++++++++++-------
.../strategies/abstract_search_strategy.ts | 55 ++++++++------
.../strategies/default_search_strategy.js | 13 +---
.../default_search_strategy.test.js | 28 +------
.../server/lib/vis_data/get_annotations.js | 6 +-
.../server/lib/vis_data/get_series_data.js | 10 ++-
.../server/lib/vis_data/get_table_data.js | 10 ++-
.../vis_type_timeseries/server/plugin.ts | 12 ++-
.../vis_type_timeseries/server/routes/vis.ts | 3 +-
x-pack/plugins/data_enhanced/server/index.ts | 2 +
.../register_rollup_search_strategy.test.js | 11 +--
.../register_rollup_search_strategy.ts | 13 ++--
.../rollup_search_request.test.js | 53 -------------
.../rollup_search_request.ts | 28 -------
.../rollup_search_strategy.test.js | 44 +++++++----
.../rollup_search_strategy.ts | 46 ++++++-----
x-pack/plugins/rollup/server/plugin.ts | 20 ++---
x-pack/plugins/rollup/server/types.ts | 8 +-
30 files changed, 193 insertions(+), 687 deletions(-)
delete mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js
delete mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js
delete mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js
delete mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js
delete mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js
delete mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js
delete mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js
delete mode 100644 src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js
delete mode 100644 x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js
delete mode 100644 x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts
diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts
index f460257caf5e3..333ed0ff64fdb 100644
--- a/src/plugins/vis_type_timeseries/server/index.ts
+++ b/src/plugins/vis_type_timeseries/server/index.ts
@@ -21,7 +21,7 @@ import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/serve
import { VisTypeTimeseriesConfig, config as configSchema } from './config';
import { VisTypeTimeseriesPlugin } from './plugin';
-export { VisTypeTimeseriesSetup, Framework } from './plugin';
+export { VisTypeTimeseriesSetup } from './plugin';
export const config: PluginConfigDescriptor = {
deprecations: ({ unused, renameFromRoot }) => [
@@ -39,10 +39,10 @@ export const config: PluginConfigDescriptor = {
export { ValidationTelemetryServiceSetup } from './validation_telemetry';
-// @ts-ignore
-export { AbstractSearchStrategy } from './lib/search_strategies/strategies/abstract_search_strategy';
-// @ts-ignore
-export { AbstractSearchRequest } from './lib/search_strategies/search_requests/abstract_request';
+export {
+ AbstractSearchStrategy,
+ ReqFacade,
+} from './lib/search_strategies/strategies/abstract_search_strategy';
// @ts-ignore
export { DefaultSearchCapabilities } from './lib/search_strategies/default_search_capabilities';
diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
index 0f0d99bff6f1c..777de89672bbe 100644
--- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
@@ -38,6 +38,7 @@ export async function getFields(
// level object passed from here. The layers should be refactored fully at some point, but for now
// this works and we are still using the New Platform services for these vis data portions.
const reqFacade: ReqFacade = {
+ requestContext,
...request,
framework,
payload: {},
@@ -48,22 +49,6 @@ export async function getFields(
},
getUiSettingsService: () => requestContext.core.uiSettings.client,
getSavedObjectsClient: () => requestContext.core.savedObjects.client,
- server: {
- plugins: {
- elasticsearch: {
- getCluster: () => {
- return {
- callWithRequest: async (req: any, endpoint: string, params: any) => {
- return await requestContext.core.elasticsearch.legacy.client.callAsCurrentUser(
- endpoint,
- params
- );
- },
- };
- },
- },
- },
- },
getEsShardTimeout: async () => {
return await framework.globalConfig$
.pipe(
diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
index f697e754a2e00..5eef2b53e2431 100644
--- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
@@ -21,7 +21,7 @@ import { FakeRequest, RequestHandlerContext } from 'kibana/server';
import _ from 'lodash';
import { first, map } from 'rxjs/operators';
import { getPanelData } from './vis_data/get_panel_data';
-import { Framework } from '../index';
+import { Framework } from '../plugin';
import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy';
interface GetVisDataResponse {
@@ -65,28 +65,13 @@ export function getVisData(
// level object passed from here. The layers should be refactored fully at some point, but for now
// this works and we are still using the New Platform services for these vis data portions.
const reqFacade: ReqFacade = {
+ requestContext,
...request,
framework,
pre: {},
payload: request.body,
getUiSettingsService: () => requestContext.core.uiSettings.client,
getSavedObjectsClient: () => requestContext.core.savedObjects.client,
- server: {
- plugins: {
- elasticsearch: {
- getCluster: () => {
- return {
- callWithRequest: async (req: any, endpoint: string, params: any) => {
- return await requestContext.core.elasticsearch.legacy.client.callAsCurrentUser(
- endpoint,
- params
- );
- },
- };
- },
- },
- },
- },
getEsShardTimeout: async () => {
return await framework.globalConfig$
.pipe(
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js
deleted file mode 100644
index abd2a4c65d35c..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-export class AbstractSearchRequest {
- constructor(req, callWithRequest) {
- this.req = req;
- this.callWithRequest = callWithRequest;
- }
-
- search() {
- throw new Error('AbstractSearchRequest: search method should be defined');
- }
-}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js
deleted file mode 100644
index 6f71aa63728d5..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { AbstractSearchRequest } from './abstract_request';
-
-describe('AbstractSearchRequest', () => {
- let searchRequest;
- let req;
- let callWithRequest;
-
- beforeEach(() => {
- req = {};
- callWithRequest = jest.fn();
- searchRequest = new AbstractSearchRequest(req, callWithRequest);
- });
-
- test('should init an AbstractSearchRequest instance', () => {
- expect(searchRequest.req).toBe(req);
- expect(searchRequest.callWithRequest).toBe(callWithRequest);
- expect(searchRequest.search).toBeDefined();
- });
-
- test('should throw an error trying to search', () => {
- try {
- searchRequest.search();
- } catch (error) {
- expect(error instanceof Error).toBe(true);
- expect(error.message).toEqual('AbstractSearchRequest: search method should be defined');
- }
- });
-});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js
deleted file mode 100644
index 9ada39e359589..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { AbstractSearchRequest } from './abstract_request';
-import { UI_SETTINGS } from '../../../../../data/server';
-
-const SEARCH_METHOD = 'msearch';
-
-export class MultiSearchRequest extends AbstractSearchRequest {
- async search(searches) {
- const includeFrozen = await this.req
- .getUiSettingsService()
- .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
- const multiSearchBody = searches.reduce(
- (acc, { body, index }) => [
- ...acc,
- {
- index,
- ignoreUnavailable: true,
- },
- body,
- ],
- []
- );
-
- const { responses } = await this.callWithRequest(this.req, SEARCH_METHOD, {
- body: multiSearchBody,
- rest_total_hits_as_int: true,
- ignore_throttled: !includeFrozen,
- });
-
- return responses;
- }
-}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js
deleted file mode 100644
index c113db76332b7..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { MultiSearchRequest } from './multi_search_request';
-import { UI_SETTINGS } from '../../../../../data/server';
-
-describe('MultiSearchRequest', () => {
- let searchRequest;
- let req;
- let callWithRequest;
- let getServiceMock;
- let includeFrozen;
-
- beforeEach(() => {
- includeFrozen = false;
- getServiceMock = jest.fn().mockResolvedValue(includeFrozen);
- req = {
- getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }),
- };
- callWithRequest = jest.fn().mockReturnValue({ responses: [] });
- searchRequest = new MultiSearchRequest(req, callWithRequest);
- });
-
- test('should init an MultiSearchRequest instance', () => {
- expect(searchRequest.req).toBe(req);
- expect(searchRequest.callWithRequest).toBe(callWithRequest);
- expect(searchRequest.search).toBeDefined();
- });
-
- test('should get the response from elastic msearch', async () => {
- const searches = [
- { body: 'body1', index: 'index' },
- { body: 'body2', index: 'index' },
- ];
-
- const responses = await searchRequest.search(searches);
-
- expect(responses).toEqual([]);
- expect(req.getUiSettingsService).toHaveBeenCalled();
- expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
- expect(callWithRequest).toHaveBeenCalledWith(req, 'msearch', {
- body: [
- { ignoreUnavailable: true, index: 'index' },
- 'body1',
- { ignoreUnavailable: true, index: 'index' },
- 'body2',
- ],
- rest_total_hits_as_int: true,
- ignore_throttled: !includeFrozen,
- });
- });
-});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js
deleted file mode 100644
index e6e3bcb527286..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { AbstractSearchRequest } from './abstract_request';
-
-import { MultiSearchRequest } from './multi_search_request';
-import { SingleSearchRequest } from './single_search_request';
-
-export class SearchRequest extends AbstractSearchRequest {
- getSearchRequestType(searches) {
- const isMultiSearch = Array.isArray(searches) && searches.length > 1;
- const SearchRequest = isMultiSearch ? MultiSearchRequest : SingleSearchRequest;
-
- return new SearchRequest(this.req, this.callWithRequest);
- }
-
- async search(options) {
- const concreteSearchRequest = this.getSearchRequestType(options);
-
- return concreteSearchRequest.search(options);
- }
-}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js
deleted file mode 100644
index 3d35a4aa37c5a..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { SearchRequest } from './search_request';
-import { MultiSearchRequest } from './multi_search_request';
-import { SingleSearchRequest } from './single_search_request';
-
-describe('SearchRequest', () => {
- let searchRequest;
- let req;
- let callWithRequest;
- let getServiceMock;
- let includeFrozen;
-
- beforeEach(() => {
- includeFrozen = false;
- getServiceMock = jest.fn().mockResolvedValue(includeFrozen);
- req = {
- getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }),
- };
- callWithRequest = jest.fn().mockReturnValue({ responses: [] });
- searchRequest = new SearchRequest(req, callWithRequest);
- });
-
- test('should init an AbstractSearchRequest instance', () => {
- expect(searchRequest.req).toBe(req);
- expect(searchRequest.callWithRequest).toBe(callWithRequest);
- expect(searchRequest.search).toBeDefined();
- });
-
- test('should return search value', async () => {
- const concreteSearchRequest = {
- search: jest.fn().mockReturnValue('concreteSearchRequest'),
- };
- const options = {};
- searchRequest.getSearchRequestType = jest.fn().mockReturnValue(concreteSearchRequest);
-
- const result = await searchRequest.search(options);
-
- expect(result).toBe('concreteSearchRequest');
- });
-
- test('should return a MultiSearchRequest for multi searches', () => {
- const searches = [
- { index: 'index', body: 'body' },
- { index: 'index', body: 'body' },
- ];
-
- const result = searchRequest.getSearchRequestType(searches);
-
- expect(result instanceof MultiSearchRequest).toBe(true);
- });
-
- test('should return a SingleSearchRequest for single search', () => {
- const searches = [{ index: 'index', body: 'body' }];
-
- const result = searchRequest.getSearchRequestType(searches);
-
- expect(result instanceof SingleSearchRequest).toBe(true);
- });
-});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js
deleted file mode 100644
index 7d8b60a7e4595..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { AbstractSearchRequest } from './abstract_request';
-import { UI_SETTINGS } from '../../../../../data/server';
-
-const SEARCH_METHOD = 'search';
-
-export class SingleSearchRequest extends AbstractSearchRequest {
- async search([{ body, index }]) {
- const includeFrozen = await this.req
- .getUiSettingsService()
- .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
- const resp = await this.callWithRequest(this.req, SEARCH_METHOD, {
- ignore_throttled: !includeFrozen,
- body,
- index,
- });
-
- return [resp];
- }
-}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js
deleted file mode 100644
index b899814f2fe13..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { SingleSearchRequest } from './single_search_request';
-import { UI_SETTINGS } from '../../../../../data/server';
-
-describe('SingleSearchRequest', () => {
- let searchRequest;
- let req;
- let callWithRequest;
- let getServiceMock;
- let includeFrozen;
-
- beforeEach(() => {
- includeFrozen = false;
- getServiceMock = jest.fn().mockResolvedValue(includeFrozen);
- req = {
- getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }),
- };
- callWithRequest = jest.fn().mockReturnValue({});
- searchRequest = new SingleSearchRequest(req, callWithRequest);
- });
-
- test('should init an SingleSearchRequest instance', () => {
- expect(searchRequest.req).toBe(req);
- expect(searchRequest.callWithRequest).toBe(callWithRequest);
- expect(searchRequest.search).toBeDefined();
- });
-
- test('should get the response from elastic search', async () => {
- const searches = [{ body: 'body', index: 'index' }];
-
- const responses = await searchRequest.search(searches);
-
- expect(responses).toEqual([{}]);
- expect(req.getUiSettingsService).toHaveBeenCalled();
- expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
- expect(callWithRequest).toHaveBeenCalledWith(req, 'search', {
- body: 'body',
- index: 'index',
- ignore_throttled: !includeFrozen,
- });
- });
-});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
index ecd09653b3b48..66ea4f017dd90 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
@@ -65,7 +65,7 @@ describe('SearchStrategyRegister', () => {
});
test('should add a strategy if it is an instance of AbstractSearchStrategy', () => {
- const anotherSearchStrategy = new MockSearchStrategy({}, {} as any, {});
+ const anotherSearchStrategy = new MockSearchStrategy('es');
const addedStrategies = registry.addStrategy(anotherSearchStrategy);
expect(addedStrategies.length).toEqual(2);
@@ -75,7 +75,7 @@ describe('SearchStrategyRegister', () => {
test('should return a MockSearchStrategy instance', async () => {
const req = {};
const indexPattern = '*';
- const anotherSearchStrategy = new MockSearchStrategy({}, {} as any, {});
+ const anotherSearchStrategy = new MockSearchStrategy('es');
registry.addStrategy(anotherSearchStrategy);
const { searchStrategy, capabilities } = (await registry.getViableStrategy(req, indexPattern))!;
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
index 1fbaffd794c89..6773ee482b098 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
@@ -18,24 +18,13 @@
*/
import { AbstractSearchStrategy } from './abstract_search_strategy';
-class SearchRequest {
- constructor(req, callWithRequest) {
- this.req = req;
- this.callWithRequest = callWithRequest;
- }
-}
-
describe('AbstractSearchStrategy', () => {
let abstractSearchStrategy;
- let server;
- let callWithRequestFactory;
let req;
let mockedFields;
let indexPattern;
beforeEach(() => {
- server = {};
- callWithRequestFactory = jest.fn().mockReturnValue('callWithRequest');
mockedFields = {};
req = {
pre: {
@@ -45,16 +34,11 @@ describe('AbstractSearchStrategy', () => {
},
};
- abstractSearchStrategy = new AbstractSearchStrategy(
- server,
- callWithRequestFactory,
- SearchRequest
- );
+ abstractSearchStrategy = new AbstractSearchStrategy('es');
});
test('should init an AbstractSearchStrategy instance', () => {
- expect(abstractSearchStrategy.getCallWithRequestInstance).toBeDefined();
- expect(abstractSearchStrategy.getSearchRequest).toBeDefined();
+ expect(abstractSearchStrategy.search).toBeDefined();
expect(abstractSearchStrategy.getFieldsForWildcard).toBeDefined();
expect(abstractSearchStrategy.checkForViability).toBeDefined();
});
@@ -68,17 +52,46 @@ describe('AbstractSearchStrategy', () => {
});
});
- test('should invoke callWithRequestFactory with req param passed', () => {
- abstractSearchStrategy.getCallWithRequestInstance(req);
+ test('should return response', async () => {
+ const searches = [{ body: 'body', index: 'index' }];
+ const searchFn = jest.fn().mockReturnValue(Promise.resolve({}));
- expect(callWithRequestFactory).toHaveBeenCalledWith(server, req);
- });
-
- test('should return a search request', () => {
- const searchRequest = abstractSearchStrategy.getSearchRequest(req);
+ const responses = await abstractSearchStrategy.search(
+ {
+ requestContext: {},
+ framework: {
+ core: {
+ getStartServices: jest.fn().mockReturnValue(
+ Promise.resolve([
+ {},
+ {
+ data: {
+ search: {
+ search: searchFn,
+ },
+ },
+ },
+ ])
+ ),
+ },
+ },
+ },
+ searches
+ );
- expect(searchRequest instanceof SearchRequest).toBe(true);
- expect(searchRequest.callWithRequest).toBe('callWithRequest');
- expect(searchRequest.req).toBe(req);
+ expect(responses).toEqual([{}]);
+ expect(searchFn).toHaveBeenCalledWith(
+ {},
+ {
+ params: {
+ body: 'body',
+ index: 'index',
+ },
+ indexType: undefined,
+ },
+ {
+ strategy: 'es',
+ }
+ );
});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
index 0b1c6e6e20414..92b7e6976962e 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
@@ -18,7 +18,7 @@
*/
import {
- LegacyAPICaller,
+ RequestHandlerContext,
FakeRequest,
IUiSettingsClient,
SavedObjectsClientContract,
@@ -33,6 +33,7 @@ import { IndexPatternsFetcher } from '../../../../../data/server';
* This will be replaced by standard KibanaRequest and RequestContext objects in a later version.
*/
export type ReqFacade = FakeRequest & {
+ requestContext: RequestHandlerContext;
framework: Framework;
payload: unknown;
pre: {
@@ -40,34 +41,42 @@ export type ReqFacade = FakeRequest & {
};
getUiSettingsService: () => IUiSettingsClient;
getSavedObjectsClient: () => SavedObjectsClientContract;
- server: {
- plugins: {
- elasticsearch: {
- getCluster: () => {
- callWithRequest: (req: ReqFacade, endpoint: string, params: any) => Promise;
- };
- };
- };
- };
getEsShardTimeout: () => Promise;
};
export class AbstractSearchStrategy {
- public getCallWithRequestInstance: (req: ReqFacade) => LegacyAPICaller;
- public getSearchRequest: (req: ReqFacade) => any;
-
- constructor(
- server: any,
- callWithRequestFactory: (server: any, req: ReqFacade) => LegacyAPICaller,
- SearchRequest: any
- ) {
- this.getCallWithRequestInstance = (req) => callWithRequestFactory(server, req);
+ public searchStrategyName!: string;
+ public indexType?: string;
+ public additionalParams: any;
- this.getSearchRequest = (req) => {
- const callWithRequest = this.getCallWithRequestInstance(req);
+ constructor(name: string, type?: string, additionalParams: any = {}) {
+ this.searchStrategyName = name;
+ this.indexType = type;
+ this.additionalParams = additionalParams;
+ }
- return new SearchRequest(req, callWithRequest);
- };
+ async search(req: ReqFacade, bodies: any[], options = {}) {
+ const [, deps] = await req.framework.core.getStartServices();
+ const requests: any[] = [];
+ bodies.forEach((body) => {
+ requests.push(
+ deps.data.search.search(
+ req.requestContext,
+ {
+ params: {
+ ...body,
+ ...this.additionalParams,
+ },
+ indexType: this.indexType,
+ },
+ {
+ ...options,
+ strategy: this.searchStrategyName,
+ }
+ )
+ );
+ });
+ return Promise.all(requests);
}
async getFieldsForWildcard(req: ReqFacade, indexPattern: string, capabilities: any) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js
index 63f2911ce1118..7c3609ae3c405 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js
@@ -16,21 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
+
+import { ES_SEARCH_STRATEGY } from '../../../../../data/server';
import { AbstractSearchStrategy } from './abstract_search_strategy';
-import { SearchRequest } from '../search_requests/search_request';
import { DefaultSearchCapabilities } from '../default_search_capabilities';
-const callWithRequestFactory = (server, request) => {
- const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data');
-
- return callWithRequest;
-};
-
export class DefaultSearchStrategy extends AbstractSearchStrategy {
name = 'default';
- constructor(server) {
- super(server, callWithRequestFactory, SearchRequest);
+ constructor() {
+ super(ES_SEARCH_STRATEGY);
}
checkForViability(req) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js
index 2e3a459bf06fd..a9994ba3e1f75 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js
@@ -20,42 +20,20 @@ import { DefaultSearchStrategy } from './default_search_strategy';
describe('DefaultSearchStrategy', () => {
let defaultSearchStrategy;
- let server;
- let callWithRequest;
let req;
beforeEach(() => {
- server = {};
- callWithRequest = jest.fn();
- req = {
- server: {
- plugins: {
- elasticsearch: {
- getCluster: jest.fn().mockReturnValue({
- callWithRequest,
- }),
- },
- },
- },
- };
- defaultSearchStrategy = new DefaultSearchStrategy(server);
+ req = {};
+ defaultSearchStrategy = new DefaultSearchStrategy();
});
test('should init an DefaultSearchStrategy instance', () => {
expect(defaultSearchStrategy.name).toBe('default');
expect(defaultSearchStrategy.checkForViability).toBeDefined();
- expect(defaultSearchStrategy.getCallWithRequestInstance).toBeDefined();
- expect(defaultSearchStrategy.getSearchRequest).toBeDefined();
+ expect(defaultSearchStrategy.search).toBeDefined();
expect(defaultSearchStrategy.getFieldsForWildcard).toBeDefined();
});
- test('should invoke callWithRequestFactory with passed params', () => {
- const value = defaultSearchStrategy.getCallWithRequestInstance(req);
-
- expect(value).toBe(callWithRequest);
- expect(req.server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith('data');
- });
-
test('should check a strategy for viability', () => {
const value = defaultSearchStrategy.checkForViability(req);
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js
index b015aaf0ef8db..d8a230dfeef4e 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js
@@ -39,7 +39,6 @@ export async function getAnnotations({
capabilities,
series,
}) {
- const searchRequest = searchStrategy.getSearchRequest(req);
const annotations = panel.annotations.filter(validAnnotation);
const lastSeriesTimestamp = getLastSeriesTimestamp(series);
const handleAnnotationResponseBy = handleAnnotationResponse(lastSeriesTimestamp);
@@ -47,6 +46,7 @@ export async function getAnnotations({
const bodiesPromises = annotations.map((annotation) =>
getAnnotationRequestParams(req, panel, annotation, esQueryConfig, capabilities)
);
+
const searches = (await Promise.all(bodiesPromises)).reduce(
(acc, items) => acc.concat(items),
[]
@@ -55,10 +55,10 @@ export async function getAnnotations({
if (!searches.length) return { responses: [] };
try {
- const data = await searchRequest.search(searches);
+ const data = await searchStrategy.search(req.framework.core, req.requestContext, searches);
return annotations.reduce((acc, annotation, index) => {
- acc[annotation.id] = handleAnnotationResponseBy(data[index], annotation);
+ acc[annotation.id] = handleAnnotationResponseBy(data[index].rawResponse, annotation);
return acc;
}, {});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js
index ee48816c6a8af..1eace13c2e336 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js
@@ -28,7 +28,6 @@ export async function getSeriesData(req, panel) {
searchStrategy,
capabilities,
} = await req.framework.searchStrategyRegistry.getViableStrategyForPanel(req, panel);
- const searchRequest = searchStrategy.getSearchRequest(req);
const esQueryConfig = await getEsQueryConfig(req);
const meta = {
type: panel.type,
@@ -45,8 +44,13 @@ export async function getSeriesData(req, panel) {
[]
);
- const data = await searchRequest.search(searches);
- const series = data.map(handleResponseBody(panel));
+ const data = await searchStrategy.search(req, searches);
+
+ const handleResponseBodyFn = handleResponseBody(panel);
+
+ const series = data.map((resp) =>
+ handleResponseBodyFn(resp.rawResponse ? resp.rawResponse : resp)
+ );
let annotations = null;
if (panel.annotations && panel.annotations.length) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js
index 1d1c245907959..3791eb229db5b 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js
@@ -30,7 +30,6 @@ export async function getTableData(req, panel) {
searchStrategy,
capabilities,
} = await req.framework.searchStrategyRegistry.getViableStrategy(req, panelIndexPattern);
- const searchRequest = searchStrategy.getSearchRequest(req);
const esQueryConfig = await getEsQueryConfig(req);
const { indexPatternObject } = await getIndexPatternObject(req, panelIndexPattern);
@@ -41,13 +40,18 @@ export async function getTableData(req, panel) {
try {
const body = buildRequestBody(req, panel, esQueryConfig, indexPatternObject, capabilities);
- const [resp] = await searchRequest.search([
+ const [resp] = await searchStrategy.search(req, [
{
body,
index: panelIndexPattern,
},
]);
- const buckets = get(resp, 'aggregations.pivot.buckets', []);
+
+ const buckets = get(
+ resp.rawResponse ? resp.rawResponse : resp,
+ 'aggregations.pivot.buckets',
+ []
+ );
return {
...meta,
diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts
index d863937a4e3dc..678ba2b371978 100644
--- a/src/plugins/vis_type_timeseries/server/plugin.ts
+++ b/src/plugins/vis_type_timeseries/server/plugin.ts
@@ -33,6 +33,7 @@ import { VisTypeTimeseriesConfig } from './config';
import { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data';
import { ValidationTelemetryService } from './validation_telemetry';
import { UsageCollectionSetup } from '../../usage_collection/server';
+import { PluginStart } from '../../data/server';
import { visDataRoutes } from './routes/vis';
// @ts-ignore
import { fieldsRoutes } from './routes/fields';
@@ -47,6 +48,10 @@ interface VisTypeTimeseriesPluginSetupDependencies {
usageCollection?: UsageCollectionSetup;
}
+interface VisTypeTimeseriesPluginStartDependencies {
+ data: PluginStart;
+}
+
export interface VisTypeTimeseriesSetup {
getVisData: (
requestContext: RequestHandlerContext,
@@ -57,7 +62,7 @@ export interface VisTypeTimeseriesSetup {
}
export interface Framework {
- core: CoreSetup;
+ core: CoreSetup;
plugins: any;
config$: Observable;
globalConfig$: PluginInitializerContext['config']['legacy']['globalConfig$'];
@@ -74,7 +79,10 @@ export class VisTypeTimeseriesPlugin implements Plugin {
this.validationTelementryService = new ValidationTelemetryService();
}
- public setup(core: CoreSetup, plugins: VisTypeTimeseriesPluginSetupDependencies) {
+ public setup(
+ core: CoreSetup,
+ plugins: VisTypeTimeseriesPluginSetupDependencies
+ ) {
const logger = this.initializerContext.logger.get('visTypeTimeseries');
core.uiSettings.register(uiSettings);
const config$ = this.initializerContext.config.create();
diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts
index 48efd4398e4d4..1ca8b57ab230f 100644
--- a/src/plugins/vis_type_timeseries/server/routes/vis.ts
+++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts
@@ -21,7 +21,8 @@ import { IRouter, KibanaRequest } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { getVisData, GetVisDataOptions } from '../lib/get_vis_data';
import { visPayloadSchema } from '../../common/vis_schema';
-import { Framework, ValidationTelemetryServiceSetup } from '../index';
+import { ValidationTelemetryServiceSetup } from '../index';
+import { Framework } from '../plugin';
const escapeHatch = schema.object({}, { unknowns: 'allow' });
diff --git a/x-pack/plugins/data_enhanced/server/index.ts b/x-pack/plugins/data_enhanced/server/index.ts
index fbe1ecc10d632..3c5d5d1e99d13 100644
--- a/x-pack/plugins/data_enhanced/server/index.ts
+++ b/x-pack/plugins/data_enhanced/server/index.ts
@@ -11,4 +11,6 @@ export function plugin(initializerContext: PluginInitializerContext) {
return new EnhancedDataServerPlugin(initializerContext);
}
+export { ENHANCED_ES_SEARCH_STRATEGY } from '../common';
+
export { EnhancedDataServerPlugin as Plugin };
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js
index d466ebd69737e..8672a8b8f6849 100644
--- a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js
+++ b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js
@@ -6,21 +6,16 @@
import { registerRollupSearchStrategy } from './register_rollup_search_strategy';
describe('Register Rollup Search Strategy', () => {
- let routeDependencies;
let addSearchStrategy;
+ let getRollupService;
beforeEach(() => {
- routeDependencies = {
- router: jest.fn().mockName('router'),
- elasticsearchService: jest.fn().mockName('elasticsearchService'),
- elasticsearch: jest.fn().mockName('elasticsearch'),
- };
-
addSearchStrategy = jest.fn().mockName('addSearchStrategy');
+ getRollupService = jest.fn().mockName('getRollupService');
});
test('should run initialization', () => {
- registerRollupSearchStrategy(routeDependencies, addSearchStrategy);
+ registerRollupSearchStrategy(addSearchStrategy, getRollupService);
expect(addSearchStrategy).toHaveBeenCalled();
});
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts
index 333863979ba95..22dafbb71d802 100644
--- a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts
+++ b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts
@@ -4,27 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { ILegacyScopedClusterClient } from 'src/core/server';
import {
- AbstractSearchRequest,
DefaultSearchCapabilities,
AbstractSearchStrategy,
+ ReqFacade,
} from '../../../../../../src/plugins/vis_type_timeseries/server';
-import { CallWithRequestFactoryShim } from '../../types';
import { getRollupSearchStrategy } from './rollup_search_strategy';
-import { getRollupSearchRequest } from './rollup_search_request';
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
export const registerRollupSearchStrategy = (
- callWithRequestFactory: CallWithRequestFactoryShim,
- addSearchStrategy: (searchStrategy: any) => void
+ addSearchStrategy: (searchStrategy: any) => void,
+ getRollupService: (reg: ReqFacade) => Promise
) => {
- const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest);
const RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities);
const RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
- RollupSearchRequest,
RollupSearchCapabilities,
- callWithRequestFactory
+ getRollupService
);
addSearchStrategy(new RollupSearchStrategy());
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js
deleted file mode 100644
index 2ea0612140946..0000000000000
--- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { getRollupSearchRequest } from './rollup_search_request';
-
-class AbstractSearchRequest {
- indexPattern = 'indexPattern';
- callWithRequest = jest.fn(({ body }) => Promise.resolve(body));
-}
-
-describe('Rollup search request', () => {
- let RollupSearchRequest;
-
- beforeEach(() => {
- RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest);
- });
-
- test('should create instance of RollupSearchRequest', () => {
- const rollupSearchRequest = new RollupSearchRequest();
-
- expect(rollupSearchRequest).toBeInstanceOf(AbstractSearchRequest);
- expect(rollupSearchRequest.search).toBeDefined();
- expect(rollupSearchRequest.callWithRequest).toBeDefined();
- });
-
- test('should send one request for single search', async () => {
- const rollupSearchRequest = new RollupSearchRequest();
- const searches = [{ body: 'body', index: 'index' }];
-
- await rollupSearchRequest.search(searches);
-
- expect(rollupSearchRequest.callWithRequest).toHaveBeenCalledTimes(1);
- expect(rollupSearchRequest.callWithRequest).toHaveBeenCalledWith('rollup.search', {
- body: 'body',
- index: 'index',
- rest_total_hits_as_int: true,
- });
- });
-
- test('should send multiple request for multi search', async () => {
- const rollupSearchRequest = new RollupSearchRequest();
- const searches = [
- { body: 'body', index: 'index' },
- { body: 'body1', index: 'index' },
- ];
-
- await rollupSearchRequest.search(searches);
-
- expect(rollupSearchRequest.callWithRequest).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts
deleted file mode 100644
index 7e12d5286f34c..0000000000000
--- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-const SEARCH_METHOD = 'rollup.search';
-
-interface Search {
- index: string;
- body: {
- [key: string]: any;
- };
-}
-
-export const getRollupSearchRequest = (AbstractSearchRequest: any) =>
- class RollupSearchRequest extends AbstractSearchRequest {
- async search(searches: Search[]) {
- const requests = searches.map(({ body, index }) =>
- this.callWithRequest(SEARCH_METHOD, {
- body,
- index,
- rest_total_hits_as_int: true,
- })
- );
-
- return await Promise.all(requests);
- }
- };
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js
index 63f4628e36bfe..f3da7ed3fdd17 100644
--- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js
+++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js
@@ -7,13 +7,32 @@ import { getRollupSearchStrategy } from './rollup_search_strategy';
describe('Rollup Search Strategy', () => {
let RollupSearchStrategy;
- let RollupSearchRequest;
let RollupSearchCapabilities;
let callWithRequest;
let rollupResolvedData;
- const server = 'server';
- const request = 'request';
+ const request = {
+ requestContext: {
+ core: {
+ elasticsearch: {
+ client: {
+ asCurrentUser: {
+ rollup: {
+ getRollupIndexCaps: jest.fn().mockImplementation(() => rollupResolvedData),
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+ const getRollupService = jest.fn().mockImplementation(() => {
+ return {
+ callAsCurrentUser: async () => {
+ return rollupResolvedData;
+ },
+ };
+ });
const indexPattern = 'indexPattern';
beforeEach(() => {
@@ -33,19 +52,17 @@ describe('Rollup Search Strategy', () => {
}
}
- RollupSearchRequest = jest.fn();
RollupSearchCapabilities = jest.fn(() => 'capabilities');
- callWithRequest = jest.fn().mockImplementation(() => rollupResolvedData);
RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
- RollupSearchRequest,
- RollupSearchCapabilities
+ RollupSearchCapabilities,
+ getRollupService
);
});
test('should create instance of RollupSearchRequest', () => {
- const rollupSearchStrategy = new RollupSearchStrategy(server);
+ const rollupSearchStrategy = new RollupSearchStrategy();
expect(rollupSearchStrategy.name).toBe('rollup');
});
@@ -55,7 +72,7 @@ describe('Rollup Search Strategy', () => {
const rollupIndex = 'rollupIndex';
beforeEach(() => {
- rollupSearchStrategy = new RollupSearchStrategy(server);
+ rollupSearchStrategy = new RollupSearchStrategy();
rollupSearchStrategy.getRollupData = jest.fn(() => ({
[rollupIndex]: {
rollup_jobs: [
@@ -104,7 +121,7 @@ describe('Rollup Search Strategy', () => {
let rollupSearchStrategy;
beforeEach(() => {
- rollupSearchStrategy = new RollupSearchStrategy(server);
+ rollupSearchStrategy = new RollupSearchStrategy();
});
test('should return rollup data', async () => {
@@ -112,10 +129,7 @@ describe('Rollup Search Strategy', () => {
const rollupData = await rollupSearchStrategy.getRollupData(request, indexPattern);
- expect(callWithRequest).toHaveBeenCalledWith('rollup.rollupIndexCapabilities', {
- indexPattern,
- });
- expect(rollupSearchStrategy.getCallWithRequestInstance).toHaveBeenCalledWith(request);
+ expect(getRollupService).toHaveBeenCalled();
expect(rollupData).toBe('data');
});
@@ -135,7 +149,7 @@ describe('Rollup Search Strategy', () => {
const rollupIndex = 'rollupIndex';
beforeEach(() => {
- rollupSearchStrategy = new RollupSearchStrategy(server);
+ rollupSearchStrategy = new RollupSearchStrategy();
fieldsCapabilities = {
[rollupIndex]: {
aggs: {
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts
index 885836780f1a9..e7794caf8697b 100644
--- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts
+++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts
@@ -4,15 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { keyBy, isString } from 'lodash';
-import { KibanaRequest } from 'src/core/server';
-
-import { CallWithRequestFactoryShim } from '../../types';
+import { ILegacyScopedClusterClient } from 'src/core/server';
+import { ReqFacade } from '../../../../../../src/plugins/vis_type_timeseries/server';
+import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../data_enhanced/server';
import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields';
import { getCapabilitiesForRollupIndices } from '../map_capabilities';
-const ROLLUP_INDEX_CAPABILITIES_METHOD = 'rollup.rollupIndexCapabilities';
-
-const getRollupIndices = (rollupData: { [key: string]: any[] }) => Object.keys(rollupData);
+const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData);
const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*');
const isIndexPatternValid = (indexPattern: string) =>
@@ -20,28 +18,40 @@ const isIndexPatternValid = (indexPattern: string) =>
export const getRollupSearchStrategy = (
AbstractSearchStrategy: any,
- RollupSearchRequest: any,
RollupSearchCapabilities: any,
- callWithRequestFactory: CallWithRequestFactoryShim
+ getRollupService: (reg: ReqFacade) => Promise
) =>
class RollupSearchStrategy extends AbstractSearchStrategy {
name = 'rollup';
constructor() {
- // TODO: When vis_type_timeseries and AbstractSearchStrategy are migrated to the NP, it
- // shouldn't require elasticsearchService to be injected, and we can remove this null argument.
- super(null, callWithRequestFactory, RollupSearchRequest);
+ super(ENHANCED_ES_SEARCH_STRATEGY, 'rollup', { rest_total_hits_as_int: true });
}
- getRollupData(req: KibanaRequest, indexPattern: string) {
- const callWithRequest = this.getCallWithRequestInstance(req);
+ async search(req: ReqFacade, bodies: any[], options = {}) {
+ const rollupService = await getRollupService(req);
+ const requests: any[] = [];
+ bodies.forEach((body) => {
+ requests.push(
+ rollupService.callAsCurrentUser('rollup.search', {
+ ...body,
+ rest_total_hits_as_int: true,
+ })
+ );
+ });
+ return Promise.all(requests);
+ }
- return callWithRequest(ROLLUP_INDEX_CAPABILITIES_METHOD, {
- indexPattern,
- }).catch(() => Promise.resolve({}));
+ async getRollupData(req: ReqFacade, indexPattern: string) {
+ const rollupService = await getRollupService(req);
+ return rollupService
+ .callAsCurrentUser('rollup.rollupIndexCapabilities', {
+ indexPattern,
+ })
+ .catch(() => Promise.resolve({}));
}
- async checkForViability(req: KibanaRequest, indexPattern: string) {
+ async checkForViability(req: ReqFacade, indexPattern: string) {
let isViable = false;
let capabilities = null;
@@ -66,7 +76,7 @@ export const getRollupSearchStrategy = (
}
async getFieldsForWildcard(
- req: KibanaRequest,
+ req: ReqFacade,
indexPattern: string,
{
fieldsCapabilities,
diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts
index 8b3a6355f950d..fe193150fc1ca 100644
--- a/x-pack/plugins/rollup/server/plugin.ts
+++ b/x-pack/plugins/rollup/server/plugin.ts
@@ -17,17 +17,16 @@ import {
ILegacyCustomClusterClient,
Plugin,
Logger,
- KibanaRequest,
PluginInitializerContext,
ILegacyScopedClusterClient,
- LegacyAPICaller,
SharedGlobalConfig,
} from 'src/core/server';
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
+import { ReqFacade } from '../../../../src/plugins/vis_type_timeseries/server';
import { PLUGIN, CONFIG_ROLLUPS } from '../common';
-import { Dependencies, CallWithRequestFactoryShim } from './types';
+import { Dependencies } from './types';
import { registerApiRoutes } from './routes';
import { License } from './services';
import { registerRollupUsageCollector } from './collectors';
@@ -132,19 +131,12 @@ export class RollupPlugin implements Plugin {
});
if (visTypeTimeseries) {
- // TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim.
- const callWithRequestFactoryShim = (
- elasticsearchServiceShim: CallWithRequestFactoryShim,
- request: KibanaRequest
- ): LegacyAPICaller => {
- return async (...args: Parameters) => {
- this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices));
- return await this.rollupEsClient.asScoped(request).callAsCurrentUser(...args);
- };
+ const getRollupService = async (request: ReqFacade) => {
+ this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices));
+ return this.rollupEsClient.asScoped(request);
};
-
const { addSearchStrategy } = visTypeTimeseries;
- registerRollupSearchStrategy(callWithRequestFactoryShim, addSearchStrategy);
+ registerRollupSearchStrategy(addSearchStrategy, getRollupService);
}
if (usageCollection) {
diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts
index 290d2df050099..b167806cf8d5d 100644
--- a/x-pack/plugins/rollup/server/types.ts
+++ b/x-pack/plugins/rollup/server/types.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IRouter, LegacyAPICaller, KibanaRequest } from 'src/core/server';
+import { IRouter } from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server';
@@ -39,9 +39,3 @@ export interface RouteDependencies {
IndexPatternsFetcher: typeof IndexPatternsFetcher;
};
}
-
-// TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim.
-export type CallWithRequestFactoryShim = (
- elasticsearchServiceShim: CallWithRequestFactoryShim,
- request: KibanaRequest
-) => LegacyAPICaller;