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/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,
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/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()
}
}
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/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,
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}`}
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",
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;
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) => {
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/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);
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'));
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({