= ({ basePath }) => (
),
From 8acdcff840ac1d7e0fe00319b2e84600d177e107 Mon Sep 17 00:00:00 2001
From: Candace Park <56409205+parkiino@users.noreply.github.com>
Date: Mon, 12 Jul 2021 15:37:16 -0400
Subject: [PATCH 12/52] [Security Solution][Endpoint][Host Isolation] Host
isolation cases view unit test (#104579)
---
.../user_action_tree/index.test.tsx | 87 ++++++++++++++++++-
...tion_host_isolation_comment_event.test.tsx | 42 +++++++++
...er_action_host_isolation_comment_event.tsx | 2 +-
.../plugins/cases/public/containers/mock.ts | 61 +++++++++++++
4 files changed, 190 insertions(+), 2 deletions(-)
create mode 100644 x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx
diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx
index 610399c31928b..be1516843184d 100644
--- a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx
+++ b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx
@@ -13,7 +13,14 @@ import routeData from 'react-router';
import { getFormMock, useFormMock, useFormDataMock } from '../__mock__/form';
import { useUpdateComment } from '../../containers/use_update_comment';
-import { basicCase, basicPush, getUserAction } from '../../containers/mock';
+import {
+ basicCase,
+ basicPush,
+ getUserAction,
+ getHostIsolationUserAction,
+ hostIsolationComment,
+ hostReleaseComment,
+} from '../../containers/mock';
import { UserActionTree } from '.';
import { TestProviders } from '../../common/mock';
import { Ecs } from '../../../common';
@@ -368,4 +375,82 @@ describe(`UserActionTree`, () => {
).toEqual(true);
});
});
+ describe('Host isolation action', () => {
+ it('renders in the cases details view', async () => {
+ const isolateAction = [getHostIsolationUserAction()];
+ const props = {
+ ...defaultProps,
+ caseUserActions: isolateAction,
+ data: { ...defaultProps.data, comments: [...basicCase.comments, hostIsolationComment()] },
+ };
+
+ const wrapper = mount(
+
+
+
+ );
+ await waitFor(() => {
+ expect(wrapper.find(`[data-test-subj="endpoint-action"]`).exists()).toBe(true);
+ });
+ });
+
+ it('shows the correct username', async () => {
+ const isolateAction = [getHostIsolationUserAction()];
+ const props = {
+ ...defaultProps,
+ caseUserActions: isolateAction,
+ data: { ...defaultProps.data, comments: [hostIsolationComment()] },
+ };
+
+ const wrapper = mount(
+
+
+
+ );
+ await waitFor(() => {
+ expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().prop('name')).toEqual(
+ defaultProps.data.createdBy.fullName
+ );
+ });
+ });
+
+ it('shows a lock icon if the action is isolate', async () => {
+ const isolateAction = [getHostIsolationUserAction()];
+ const props = {
+ ...defaultProps,
+ caseUserActions: isolateAction,
+ data: { ...defaultProps.data, comments: [hostIsolationComment()] },
+ };
+
+ const wrapper = mount(
+
+
+
+ );
+ await waitFor(() => {
+ expect(
+ wrapper.find(`[data-test-subj="endpoint-action"]`).first().prop('timelineIcon')
+ ).toBe('lock');
+ });
+ });
+ it('shows a lockOpen icon if the action is unisolate/release', async () => {
+ const isolateAction = [getHostIsolationUserAction()];
+ const props = {
+ ...defaultProps,
+ caseUserActions: isolateAction,
+ data: { ...defaultProps.data, comments: [hostReleaseComment()] },
+ };
+
+ const wrapper = mount(
+
+
+
+ );
+ await waitFor(() => {
+ expect(
+ wrapper.find(`[data-test-subj="endpoint-action"]`).first().prop('timelineIcon')
+ ).toBe('lockOpen');
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.test.tsx
new file mode 100644
index 0000000000000..636cd7e40aac1
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import { HostIsolationCommentEvent } from './user_action_host_isolation_comment_event';
+
+const defaultProps = () => {
+ return {
+ type: 'isolate',
+ endpoints: [{ endpointId: 'e1', hostname: 'host1' }],
+ href: jest.fn(),
+ onClick: jest.fn(),
+ };
+};
+
+describe('UserActionHostIsolationCommentEvent', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders with the correct action and hostname', async () => {
+ const wrapper = mount();
+ expect(wrapper.find(`[data-test-subj="actions-link-e1"]`).first().exists()).toBeTruthy();
+ expect(wrapper.text()).toBe('isolated host host1');
+ });
+
+ it('navigates to app on link click', async () => {
+ const onActionsLinkClick = jest.fn();
+
+ const wrapper = mount(
+
+ );
+
+ wrapper.find(`[data-test-subj="actions-link-e1"]`).first().simulate('click');
+ expect(onActionsLinkClick).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx
index d363e874a4e0d..2381d31b3ada8 100644
--- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx
+++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_host_isolation_comment_event.tsx
@@ -44,7 +44,7 @@ const HostIsolationCommentEventComponent: React.FC = ({
{endpoints[0].hostname}
diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts
index a900010235c9f..c955bb34240e2 100644
--- a/x-pack/plugins/cases/public/containers/mock.ts
+++ b/x-pack/plugins/cases/public/containers/mock.ts
@@ -76,6 +76,58 @@ export const alertComment: Comment = {
version: 'WzQ3LDFc',
};
+export const hostIsolationComment: () => Comment = () => {
+ return {
+ type: CommentType.actions,
+ comment: 'I just isolated the host!',
+ id: 'isolate-comment-id',
+ actions: {
+ targets: [
+ {
+ hostname: 'host1',
+ endpointId: '001',
+ },
+ ],
+ type: 'isolate',
+ },
+ associationType: AssociationType.case,
+ createdAt: basicCreatedAt,
+ createdBy: elasticUser,
+ owner: SECURITY_SOLUTION_OWNER,
+ pushedAt: null,
+ pushedBy: null,
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzQ3LDFc',
+ };
+};
+
+export const hostReleaseComment: () => Comment = () => {
+ return {
+ type: CommentType.actions,
+ comment: 'I just released the host!',
+ id: 'isolate-comment-id',
+ actions: {
+ targets: [
+ {
+ hostname: 'host1',
+ endpointId: '001',
+ },
+ ],
+ type: 'unisolate',
+ },
+ associationType: AssociationType.case,
+ createdAt: basicCreatedAt,
+ createdBy: elasticUser,
+ owner: SECURITY_SOLUTION_OWNER,
+ pushedAt: null,
+ pushedBy: null,
+ updatedAt: null,
+ updatedBy: null,
+ version: 'WzQ3LDFc',
+ };
+};
+
export const basicCase: Case = {
type: CaseType.individual,
owner: SECURITY_SOLUTION_OWNER,
@@ -374,6 +426,15 @@ export const getAlertUserAction = () => ({
newValue: '{"type":"alert","alertId":"alert-id-1","index":"index-id-1"}',
});
+export const getHostIsolationUserAction = () => ({
+ ...basicAction,
+ actionId: 'isolate-action-id',
+ actionField: ['comment'] as UserActionField,
+ action: 'create' as UserAction,
+ commentId: 'isolate-comment-id',
+ newValue: 'some value',
+});
+
export const caseUserActions: CaseUserActions[] = [
getUserAction(['description'], 'create'),
getUserAction(['comment'], 'create'),
From 3d724ee706b00cf7326d7f2f68af85f5016c7328 Mon Sep 17 00:00:00 2001
From: Joe Portner <5295965+jportner@users.noreply.github.com>
Date: Mon, 12 Jul 2021 16:03:32 -0400
Subject: [PATCH 13/52] [project-assigner] Add entry for Platform Security Team
(#105307)
---
.github/workflows/project-assigner.yml | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml
index 4966a0b506317..7a09c2b35cc17 100644
--- a/.github/workflows/project-assigner.yml
+++ b/.github/workflows/project-assigner.yml
@@ -11,5 +11,13 @@ jobs:
uses: elastic/github-actions/project-assigner@v2.0.0
id: project_assigner
with:
- issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]'
+ issue-mappings: |
+ [
+ {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"},
+ {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"},
+ {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"},
+ {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"},
+ {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"},
+ {"label": "Team:Security", "projectNumber": 320, "columnName": "Awaiting triage", "projectScope": "org"}
+ ]
ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
From e279042c56a8199b18eca4e66b056613afa8d866 Mon Sep 17 00:00:00 2001
From: Tyler Smalley
Date: Mon, 12 Jul 2021 13:06:11 -0700
Subject: [PATCH 14/52] Revert "[project-assigner] Add entry for Platform
Security Team (#105307)" (#105311)
This reverts commit 3d724ee706b00cf7326d7f2f68af85f5016c7328.
---
.github/workflows/project-assigner.yml | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml
index 7a09c2b35cc17..4966a0b506317 100644
--- a/.github/workflows/project-assigner.yml
+++ b/.github/workflows/project-assigner.yml
@@ -11,13 +11,5 @@ jobs:
uses: elastic/github-actions/project-assigner@v2.0.0
id: project_assigner
with:
- issue-mappings: |
- [
- {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"},
- {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"},
- {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"},
- {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"},
- {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"},
- {"label": "Team:Security", "projectNumber": 320, "columnName": "Awaiting triage", "projectScope": "org"}
- ]
+ issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]'
ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
From fdc99681a74b810da08e84b989620c2804e363c5 Mon Sep 17 00:00:00 2001
From: Vadim Yakhin
Date: Mon, 12 Jul 2021 17:20:48 -0300
Subject: [PATCH 15/52] [Workplace Search] Port PR 4033 from ent-search to
Kibana and update typings (#105054)
* Improve typings
Custom API Source allow indexing several types of data. We didn't
account for all of them.
For example, geolocation can be array of arrays of numbers.
This commit improves typings.
The following commits mostly fix TS errors
that appear after this commit.
* Remove type castings to account for all possible variable types
* Update helper functions to accept new CustomAPIFieldValue
* Fix TS error: convert url to string before using it in EuiLink
* Update mock and tests to match updated typings
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../__mocks__/content_sources.mock.ts | 9 ++++++--
.../applications/workplace_search/types.ts | 21 +++++++++++++++++--
.../get_as_local_datetime_string.test.ts | 6 ++++++
.../utils/get_as_local_datetime_string.ts | 8 +++++--
.../workplace_search/utils/mime_types.ts | 5 ++++-
.../example_result_detail_card.tsx | 2 +-
.../example_search_result_group.tsx | 5 ++---
.../example_standout_result.tsx | 4 ++--
.../display_settings/subtitle_field.test.tsx | 8 ++++++-
.../display_settings/title_field.test.tsx | 12 +++++++++--
.../components/source_content.tsx | 2 +-
11 files changed, 65 insertions(+), 17 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts
index 748dc6a7cbcf8..c599a13cc3119 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts
@@ -303,6 +303,7 @@ export const exampleResult = {
titleField: 'otherTitle',
subtitleField: 'otherSubtitle',
urlField: 'myLink',
+ urlFieldIsLinkable: true,
color: '#e3e3e3',
descriptionField: 'about',
typeField: 'otherType',
@@ -314,14 +315,18 @@ export const exampleResult = {
{ fieldName: 'dogs', label: 'Canines' },
],
},
- titleFieldHover: false,
- urlFieldHover: false,
exampleDocuments: [
{
myLink: 'http://foo',
otherTitle: 'foo',
+ content_source_id: '60e85e7ea2564c265a88a4f0',
+ external_id: 'doc-60e85eb7a2564c937a88a4f3',
+ last_updated: '2021-07-09T14:35:35+00:00',
+ updated_at: '2021-07-09T14:35:35+00:00',
+ source: 'custom',
},
],
+ schemaFields: {},
};
export const mostRecentIndexJob = {
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts
index edc772b369558..e50b12f781947 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts
@@ -96,7 +96,7 @@ export interface ContentSource {
export interface SourceContentItem {
id: string;
last_updated: string;
- [key: string]: string;
+ [key: string]: string | CustomAPIFieldValue;
}
export interface ContentSourceDetails extends ContentSource {
@@ -186,8 +186,25 @@ export interface CustomSource {
id: string;
}
+// https://www.elastic.co/guide/en/workplace-search/current/workplace-search-custom-sources-api.html#_schema_data_types
+type CustomAPIString = string | string[];
+type CustomAPINumber = number | number[];
+type CustomAPIDate = string | string[];
+type CustomAPIGeolocation = string | string[] | number[] | number[][];
+
+export type CustomAPIFieldValue =
+ | CustomAPIString
+ | CustomAPINumber
+ | CustomAPIDate
+ | CustomAPIGeolocation;
+
export interface Result {
- [key: string]: string | string[];
+ content_source_id: string;
+ last_updated: string;
+ external_id: string;
+ updated_at: string;
+ source: string;
+ [key: string]: CustomAPIFieldValue;
}
export interface OptionValue {
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.test.ts
index 6475df7f4c399..36df182b99b85 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.test.ts
@@ -14,6 +14,12 @@ describe('getAsLocalDateTimeString', () => {
expect(getAsLocalDateTimeString(date)).toEqual(new Date(Date.parse(date)).toLocaleString());
});
+ it('returns null if passed value is not a string', () => {
+ const date = ['1', '2'];
+
+ expect(getAsLocalDateTimeString(date)).toEqual(null);
+ });
+
it('returns null if string cannot be parsed as date', () => {
const date = 'foo';
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts
index d5ceb50d4c9af..6350c4e4a4099 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/get_as_local_datetime_string.ts
@@ -5,7 +5,11 @@
* 2.0.
*/
-export const getAsLocalDateTimeString = (str: string) => {
- const dateValue = Date.parse(str);
+import { CustomAPIFieldValue } from '../types';
+
+export const getAsLocalDateTimeString = (maybeDate: CustomAPIFieldValue) => {
+ if (typeof maybeDate !== 'string') return null;
+
+ const dateValue = Date.parse(maybeDate);
return dateValue ? new Date(dateValue).toLocaleString() : null;
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts
index f7664c90d461c..7a5020be5986e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/mime_types.ts
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { CustomAPIFieldValue } from '../types';
+
const mimeTypes = {
'application/iwork-keynote-sffkey': 'Keynote',
'application/x-iwork-keynote-sffkey': 'Keynote',
@@ -51,4 +53,5 @@ const mimeTypes = {
'video/quicktime': 'MOV',
} as { [key: string]: string };
-export const mimeType = (type: string) => mimeTypes[type.toLowerCase()] || type;
+export const mimeType = (type: CustomAPIFieldValue) =>
+ mimeTypes[type.toString().toLowerCase()] || type;
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx
index eef508b2e618f..8b0a72ac23e39 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx
@@ -62,7 +62,7 @@ export const ExampleResultDetailCard: React.FC = () => {
{detailFields.length > 0 ? (
detailFields.map(({ fieldName, label }, index) => {
- const value = result[fieldName] as string;
+ const value = result[fieldName];
const dateValue = getAsLocalDateTimeString(value);
return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx
index 549faf1676a54..3ca5b619c0366 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx
@@ -117,7 +117,7 @@ export const ExampleSearchResultGroup: React.FC = () => {
data-test-subj="MediaTypeField"
>
- {mimeType(result[mediaTypeField] as string)}
+ {mimeType(result[mediaTypeField])}
)}
@@ -135,8 +135,7 @@ export const ExampleSearchResultGroup: React.FC = () => {
by {result[updatedByField]}
)}
- {getAsLocalDateTimeString(result.last_updated as string) ||
- result.last_updated}
+ {getAsLocalDateTimeString(result.last_updated) || result.last_updated}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx
index 46b8de6789467..b3ba4c6d50973 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx
@@ -109,7 +109,7 @@ export const ExampleStandoutResult: React.FC = () => {
data-test-subj="MediaTypeField"
>
- {mimeType(result[mediaTypeField] as string)}
+ {mimeType(result[mediaTypeField])}
)}
@@ -127,7 +127,7 @@ export const ExampleStandoutResult: React.FC = () => {
by {result[updatedByField]}
)}
- {getAsLocalDateTimeString(result.last_updated as string) || result.last_updated}
+ {getAsLocalDateTimeString(result.last_updated) || result.last_updated}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx
index 76c28ae3d4060..7506c499dff31 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { exampleResult } from '../../../../__mocks__/content_sources.mock';
+
import React from 'react';
import { shallow } from 'enzyme';
@@ -12,7 +14,11 @@ import { shallow } from 'enzyme';
import { SubtitleField } from './subtitle_field';
describe('SubtitleField', () => {
- const result = { foo: 'bar' };
+ const result = {
+ ...exampleResult.exampleDocuments[0],
+ foo: 'bar',
+ };
+
it('renders', () => {
const props = {
result,
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx
index 2ed4aa0b0fad1..e5681bc7e8619 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx
@@ -5,6 +5,8 @@
* 2.0.
*/
+import { exampleResult } from '../../../../__mocks__/content_sources.mock';
+
import React from 'react';
import { shallow } from 'enzyme';
@@ -12,7 +14,10 @@ import { shallow } from 'enzyme';
import { TitleField } from './title_field';
describe('TitleField', () => {
- const result = { foo: 'bar' };
+ const result = {
+ ...exampleResult.exampleDocuments[0],
+ foo: 'bar',
+ };
it('renders', () => {
const props = {
result,
@@ -26,7 +31,10 @@ describe('TitleField', () => {
it('handles title when array', () => {
const props = {
- result: { foo: ['baz', 'bar'] },
+ result: {
+ ...exampleResult.exampleDocuments[0],
+ foo: ['baz', 'bar'],
+ },
titleField: 'foo',
titleFieldHover: false,
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
index a0e3c28f20eb0..a97cc85cb822a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx
@@ -137,7 +137,7 @@ export const SourceContent: React.FC = () => {
)}
{urlFieldIsLinkable && (
-
+
)}
From 41a93079b21b7516ce2aac40e09341056005f718 Mon Sep 17 00:00:00 2001
From: Joe Portner <5295965+jportner@users.noreply.github.com>
Date: Mon, 12 Jul 2021 16:50:00 -0400
Subject: [PATCH 16/52] [project-assigner] Add entry for Platform Security Team
again (#105327)
---
.github/workflows/project-assigner.yml | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml
index 4966a0b506317..f4e62648a9741 100644
--- a/.github/workflows/project-assigner.yml
+++ b/.github/workflows/project-assigner.yml
@@ -8,8 +8,16 @@ jobs:
name: Assign issue or PR to project based on label
steps:
- name: Assign to project
- uses: elastic/github-actions/project-assigner@v2.0.0
+ uses: elastic/github-actions/project-assigner@v2.1.0
id: project_assigner
with:
- issue-mappings: '[{"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"}]'
+ issue-mappings: |
+ [
+ {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"},
+ {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"},
+ {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"},
+ {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"},
+ {"label": "Feature:Input Controls", "projectNumber": 72, "columnName": "Inbox"},
+ {"label": "Team:Security", "projectNumber": 320, "columnName": "Awaiting triage", "projectScope": "org"}
+ ]
ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }}
From c9b1a3cdef2c40190cbcfe6a06a0b495f8140956 Mon Sep 17 00:00:00 2001
From: Kevin Logan <56395104+kevinlog@users.noreply.github.com>
Date: Mon, 12 Jul 2021 17:32:37 -0400
Subject: [PATCH 17/52] [Security Solution] Push user action comments for Host
Isolation to connectors (#105265)
---
.../plugins/cases/server/client/cases/mock.ts | 100 ++++++++++++++++++
.../cases/server/client/cases/utils.test.ts | 85 +++++++++++++++
.../cases/server/client/cases/utils.ts | 27 ++++-
3 files changed, 210 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts
index 23db57c6d3097..313d6cd12a6db 100644
--- a/x-pack/plugins/cases/server/client/cases/mock.ts
+++ b/x-pack/plugins/cases/server/client/cases/mock.ts
@@ -52,6 +52,106 @@ export const comment: CommentResponse = {
version: 'WzEsMV0=',
};
+export const isolateCommentActions: CommentResponse = {
+ associationType: AssociationType.case,
+ id: 'mock-action-comment-1',
+ comment: 'Isolating this for investigation',
+ type: CommentType.actions as const,
+ created_at: '2019-11-25T21:55:00.177Z',
+ actions: {
+ targets: [
+ {
+ endpointId: '123',
+ hostname: 'windows-host-1',
+ },
+ ],
+ type: 'isolate',
+ },
+ created_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ owner: SECURITY_SOLUTION_OWNER,
+ pushed_at: null,
+ pushed_by: null,
+ updated_at: '2019-11-25T21:55:00.177Z',
+ updated_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ version: 'WzEsMV0=',
+};
+
+export const releaseCommentActions: CommentResponse = {
+ associationType: AssociationType.case,
+ id: 'mock-action-comment-1',
+ comment: 'Releasing this for investigation',
+ type: CommentType.actions as const,
+ created_at: '2019-11-25T21:55:00.177Z',
+ actions: {
+ targets: [
+ {
+ endpointId: '123',
+ hostname: 'windows-host-1',
+ },
+ ],
+ type: 'unisolate',
+ },
+ created_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ owner: SECURITY_SOLUTION_OWNER,
+ pushed_at: null,
+ pushed_by: null,
+ updated_at: '2019-11-25T21:55:00.177Z',
+ updated_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ version: 'WzEsMV0=',
+};
+
+export const isolateCommentActionsMultipleTargets: CommentResponse = {
+ associationType: AssociationType.case,
+ id: 'mock-action-comment-1',
+ comment: 'Isolating this for investigation',
+ type: CommentType.actions as const,
+ created_at: '2019-11-25T21:55:00.177Z',
+ actions: {
+ targets: [
+ {
+ endpointId: '123',
+ hostname: 'windows-host-1',
+ },
+ {
+ endpointId: '456',
+ hostname: 'windows-host-2',
+ },
+ ],
+ type: 'isolate',
+ },
+ created_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ owner: SECURITY_SOLUTION_OWNER,
+ pushed_at: null,
+ pushed_by: null,
+ updated_at: '2019-11-25T21:55:00.177Z',
+ updated_by: {
+ full_name: 'elastic',
+ email: 'testemail@elastic.co',
+ username: 'elastic',
+ },
+ version: 'WzEsMV0=',
+};
+
export const commentAlert: CommentResponse = {
associationType: AssociationType.case,
id: 'mock-comment-1',
diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts
index bfd5d1279420b..dc8af8785056d 100644
--- a/x-pack/plugins/cases/server/client/cases/utils.test.ts
+++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts
@@ -18,6 +18,9 @@ import {
commentAlert,
commentAlertMultipleIds,
commentGeneratedAlert,
+ isolateCommentActions,
+ releaseCommentActions,
+ isolateCommentActionsMultipleTargets,
} from './mock';
import {
@@ -37,6 +40,52 @@ const formatComment = {
comment: 'Wow, good luck catching that bad meanie!',
};
+const formatIsolateActionComment = {
+ commentId: isolateCommentActions.id,
+ comment: 'Isolating this for investigation',
+ actions: {
+ targets: [
+ {
+ hostname: 'windows-host-1',
+ endpointId: '123',
+ },
+ ],
+ type: 'isolate',
+ },
+};
+
+const formatReleaseActionComment = {
+ commentId: releaseCommentActions.id,
+ comment: 'Releasing this for investigation',
+ actions: {
+ targets: [
+ {
+ hostname: 'windows-host-1',
+ endpointId: '123',
+ },
+ ],
+ type: 'unisolate',
+ },
+};
+
+const formatIsolateCommentActionsMultipleTargets = {
+ commentId: isolateCommentActionsMultipleTargets.id,
+ comment: 'Isolating this for investigation',
+ actions: {
+ targets: [
+ {
+ hostname: 'windows-host-1',
+ endpointId: '123',
+ },
+ {
+ hostname: 'windows-host-2',
+ endpointId: '456',
+ },
+ ],
+ type: 'isolate',
+ },
+};
+
const params = { ...basicParams };
describe('utils', () => {
@@ -289,6 +338,42 @@ describe('utils', () => {
},
]);
});
+
+ test('transform isolate action comment', () => {
+ const comments = [isolateCommentActions];
+ const res = transformComments(comments, ['informationCreated']);
+ const actionText = `Isolated host ${formatIsolateActionComment.actions.targets[0].hostname} with comment: ${formatIsolateActionComment.comment}`;
+ expect(res).toEqual([
+ {
+ commentId: formatIsolateActionComment.commentId,
+ comment: `${actionText} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`,
+ },
+ ]);
+ });
+
+ test('transform release action comment', () => {
+ const comments = [releaseCommentActions];
+ const res = transformComments(comments, ['informationCreated']);
+ const actionText = `Released host ${formatReleaseActionComment.actions.targets[0].hostname} with comment: ${formatReleaseActionComment.comment}`;
+ expect(res).toEqual([
+ {
+ commentId: formatReleaseActionComment.commentId,
+ comment: `${actionText} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`,
+ },
+ ]);
+ });
+
+ test('transform isolate action comment with multiple hosts', () => {
+ const comments = [isolateCommentActionsMultipleTargets];
+ const res = transformComments(comments, ['informationCreated']);
+ const actionText = `Isolated host ${formatIsolateCommentActionsMultipleTargets.actions.targets[0].hostname} and 1 more with comment: ${formatIsolateCommentActionsMultipleTargets.comment}`;
+ expect(res).toEqual([
+ {
+ commentId: formatIsolateCommentActionsMultipleTargets.commentId,
+ comment: `${actionText} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`,
+ },
+ ]);
+ });
});
describe('transformers', () => {
diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts
index f5a10d705e095..f44adbea2c8b2 100644
--- a/x-pack/plugins/cases/server/client/cases/utils.ts
+++ b/x-pack/plugins/cases/server/client/cases/utils.ts
@@ -19,6 +19,7 @@ import {
CommentAttributes,
CommentRequestUserType,
CommentRequestAlertType,
+ CommentRequestActionsType,
} from '../../../common';
import { ActionsClient } from '../../../../actions/server';
import { CasesClientGetAlertsResponse } from '../../client/alerts/types';
@@ -76,6 +77,17 @@ const getCommentContent = (comment: CommentResponse): string => {
} else if (comment.type === CommentType.alert || comment.type === CommentType.generatedAlert) {
const ids = getAlertIds(comment);
return `Alert with ids ${ids.join(', ')} added to case`;
+ } else if (
+ comment.type === CommentType.actions &&
+ (comment.actions.type === 'isolate' || comment.actions.type === 'unisolate')
+ ) {
+ const firstHostname =
+ comment.actions.targets?.length > 0 ? comment.actions.targets[0].hostname : 'unknown';
+ const totalHosts = comment.actions.targets.length;
+ const actionText = comment.actions.type === 'isolate' ? 'Isolated' : 'Released';
+ const additionalHostsText = totalHosts - 1 > 0 ? `and ${totalHosts - 1} more ` : ``;
+
+ return `${actionText} host ${firstHostname} ${additionalHostsText}with comment: ${comment.comment}`;
}
return '';
@@ -161,7 +173,8 @@ export const createIncident = async ({
const commentsToBeUpdated = caseComments?.filter(
(comment) =>
// We push only user's comments
- comment.type === CommentType.user && commentsIdsToBeUpdated.has(comment.id)
+ (comment.type === CommentType.user || comment.type === CommentType.actions) &&
+ commentsIdsToBeUpdated.has(comment.id)
);
const totalAlerts = countAlerts(caseComments);
@@ -322,7 +335,7 @@ export const isCommentAlertType = (
export const getCommentContextFromAttributes = (
attributes: CommentAttributes
-): CommentRequestUserType | CommentRequestAlertType => {
+): CommentRequestUserType | CommentRequestAlertType | CommentRequestActionsType => {
const owner = attributes.owner;
switch (attributes.type) {
case CommentType.user:
@@ -340,6 +353,16 @@ export const getCommentContextFromAttributes = (
rule: attributes.rule,
owner,
};
+ case CommentType.actions:
+ return {
+ type: attributes.type,
+ comment: attributes.comment,
+ actions: {
+ targets: attributes.actions.targets,
+ type: attributes.actions.type,
+ },
+ owner,
+ };
default:
return {
type: CommentType.user,
From 0e83e992f0b049c4513fe6925bd0493eaae8e14b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ece=20=C3=96zalp?=
Date: Mon, 12 Jul 2021 17:32:54 -0400
Subject: [PATCH 18/52] [CTI] adds Recorded Future link (#105301)
---
x-pack/plugins/security_solution/common/cti/constants.ts | 2 +-
.../overview_cti_links/threat_intel_panel_view.tsx | 4 ++--
.../overview/containers/overview_cti_links/index.tsx | 9 ++++-----
3 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts
index 631a13df1ecb1..7b22e9036f566 100644
--- a/x-pack/plugins/security_solution/common/cti/constants.ts
+++ b/x-pack/plugins/security_solution/common/cti/constants.ts
@@ -65,9 +65,9 @@ export const CTI_DEFAULT_SOURCES = [
'Abuse Malware',
'AlienVault OTX',
'Anomali',
- 'Anomali ThreatStream',
'Malware Bazaar',
'MISP',
+ 'Recorded Future',
];
export const DEFAULT_CTI_SOURCE_INDEX = ['filebeat-*'];
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx
index b34f6e657d39a..51ce06762ddf9 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx
@@ -159,10 +159,10 @@ export const ThreatIntelPanelView: React.FC = ({
alignItems="center"
justifyContent="flexEnd"
>
-
+
{count}
-
+
{path ? (
{linkCopy}
diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx
index b7f919dc97013..8839aff7dc33d 100644
--- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx
@@ -74,8 +74,8 @@ export const useCtiDashboardLinks = (
})
)
);
- const items = DashboardsSO.savedObjects?.reduce(
- (acc: CtiListItem[], dashboardSO, i) => {
+ const items = DashboardsSO.savedObjects
+ ?.reduce((acc: CtiListItem[], dashboardSO, i) => {
const item = createLinkFromDashboardSO(
dashboardSO,
eventCountsByDataset,
@@ -87,9 +87,8 @@ export const useCtiDashboardLinks = (
acc.push(item);
}
return acc;
- },
- []
- );
+ }, [])
+ .sort((a, b) => (a.title > b.title ? 1 : -1));
setListItems(items);
} else {
handleDisabledPlugin();
From a11e9ed235df1a7d6eac4d461fd0c27f341d316b Mon Sep 17 00:00:00 2001
From: Lee Drengenberg
Date: Mon, 12 Jul 2021 17:05:31 -0500
Subject: [PATCH 19/52] new dialog for built-in alerts needs to be clicked
(#105284)
---
.../apps/monitoring/_monitoring_metricbeat.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js b/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js
index d8341c56aa25c..9dcc18b3c3f20 100644
--- a/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js
+++ b/x-pack/test/stack_functional_integration/apps/monitoring/_monitoring_metricbeat.js
@@ -12,6 +12,7 @@ export default ({ getService, getPageObjects }) => {
const log = getService('log');
const testSubjects = getService('testSubjects');
const isSaml = !!process.env.VM.includes('saml') || !!process.env.VM.includes('oidc');
+ const clusterOverview = getService('monitoringClusterOverview');
before(async () => {
await browser.setWindowSize(1200, 800);
@@ -25,6 +26,7 @@ export default ({ getService, getPageObjects }) => {
}
// navigateToApp without a username and password will default to the superuser
await PageObjects.common.navigateToApp('monitoring', { insertTimestamp: false });
+ await clusterOverview.closeAlertsModal();
});
it('should have Monitoring already enabled', async () => {
From dcc468c2f4b2663e1c7d59857ce849c55500f023 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?=
Date: Tue, 13 Jul 2021 01:08:47 +0300
Subject: [PATCH 20/52] [Osquery] Fix live query form saved queries picker bug
(#105308)
---
.../public/live_queries/form/index.tsx | 2 +-
.../form/live_query_query_field.tsx | 17 +++++--
.../saved_queries/saved_queries_dropdown.tsx | 51 +++++++++++++++----
.../queries/query_flyout.tsx | 8 ++-
4 files changed, 60 insertions(+), 18 deletions(-)
diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx
index 658280042696e..8654a74fecfb4 100644
--- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx
+++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx
@@ -110,7 +110,7 @@ const LiveQueryFormComponent: React.FC = ({
{
agentSelection: {
agents: [],
- allAgentsSelected: true,
+ allAgentsSelected: false,
platformsSelected: [],
policiesSelected: [],
},
diff --git a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx
index 9f0b5acd8994a..070339bb58af2 100644
--- a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx
+++ b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx
@@ -6,12 +6,15 @@
*/
import { EuiFormRow, EuiSpacer } from '@elastic/eui';
-import React, { useCallback } from 'react';
+import React, { useCallback, useRef } from 'react';
import { OsquerySchemaLink } from '../../components/osquery_schema_link';
import { FieldHook } from '../../shared_imports';
import { OsqueryEditor } from '../../editor';
-import { SavedQueriesDropdown } from '../../saved_queries/saved_queries_dropdown';
+import {
+ SavedQueriesDropdown,
+ SavedQueriesDropdownRef,
+} from '../../saved_queries/saved_queries_dropdown';
interface LiveQueryQueryFieldProps {
disabled?: boolean;
@@ -21,16 +24,18 @@ interface LiveQueryQueryFieldProps {
const LiveQueryQueryFieldComponent: React.FC = ({ disabled, field }) => {
const { value, setValue, errors } = field;
const error = errors[0]?.message;
+ const savedQueriesDropdownRef = useRef(null);
const handleSavedQueryChange = useCallback(
(savedQuery) => {
- setValue(savedQuery.query);
+ setValue(savedQuery?.query ?? '');
},
[setValue]
);
const handleEditorChange = useCallback(
(newValue) => {
+ savedQueriesDropdownRef.current?.clearSelection();
setValue(newValue);
},
[setValue]
@@ -39,7 +44,11 @@ const LiveQueryQueryFieldComponent: React.FC = ({ disa
return (
<>
-
+
}>
diff --git a/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx b/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx
index 073b56bfd1d4c..30df2267fbfa1 100644
--- a/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx
+++ b/x-pack/plugins/osquery/public/saved_queries/saved_queries_dropdown.tsx
@@ -7,7 +7,14 @@
import { find } from 'lodash/fp';
import { EuiCodeBlock, EuiFormRow, EuiComboBox, EuiText } from '@elastic/eui';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, {
+ forwardRef,
+ useCallback,
+ useEffect,
+ useImperativeHandle,
+ useMemo,
+ useState,
+} from 'react';
import { SimpleSavedObject } from 'kibana/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@@ -15,21 +22,27 @@ import { useHistory, useLocation } from 'react-router-dom';
import { useSavedQueries } from './use_saved_queries';
+export interface SavedQueriesDropdownRef {
+ clearSelection: () => void;
+}
+
interface SavedQueriesDropdownProps {
disabled?: boolean;
onChange: (
- value: SimpleSavedObject<{
- id: string;
- description?: string | undefined;
- query: string;
- }>['attributes']
+ value:
+ | SimpleSavedObject<{
+ id: string;
+ description?: string | undefined;
+ query: string;
+ }>['attributes']
+ | null
) => void;
}
-const SavedQueriesDropdownComponent: React.FC = ({
- disabled,
- onChange,
-}) => {
+const SavedQueriesDropdownComponent = forwardRef<
+ SavedQueriesDropdownRef,
+ SavedQueriesDropdownProps
+>(({ disabled, onChange }, ref) => {
const { replace } = useHistory();
const location = useLocation();
const [selectedOptions, setSelectedOptions] = useState([]);
@@ -52,6 +65,12 @@ const SavedQueriesDropdownComponent: React.FC = ({
const handleSavedQueryChange = useCallback(
(newSelectedOptions) => {
+ if (!newSelectedOptions.length) {
+ onChange(null);
+ setSelectedOptions(newSelectedOptions);
+ return;
+ }
+
const selectedSavedQuery = find(
['attributes.id', newSelectedOptions[0].value.id],
data?.savedObjects
@@ -80,6 +99,8 @@ const SavedQueriesDropdownComponent: React.FC = ({
[]
);
+ const clearSelection = useCallback(() => setSelectedOptions([]), []);
+
useEffect(() => {
const savedQueryId = location.state?.form?.savedQueryId;
@@ -94,6 +115,14 @@ const SavedQueriesDropdownComponent: React.FC = ({
}
}, [handleSavedQueryChange, replace, location.state, queryOptions]);
+ useImperativeHandle(
+ ref,
+ () => ({
+ clearSelection,
+ }),
+ [clearSelection]
+ );
+
return (
= ({
/>
);
-};
+});
export const SavedQueriesDropdown = React.memo(SavedQueriesDropdownComponent);
diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx
index 32547bc5dd2d0..95a31efeaf135 100644
--- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx
+++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/query_flyout.tsx
@@ -71,10 +71,14 @@ const QueryFlyoutComponent: React.FC = ({
[integrationPackageVersion]
);
- const { submit, setFieldValue } = form;
+ const { submit, setFieldValue, reset } = form;
const handleSetQueryValue = useCallback(
(savedQuery) => {
+ if (!savedQuery) {
+ return reset();
+ }
+
setFieldValue('id', savedQuery.id);
setFieldValue('query', savedQuery.query);
@@ -94,7 +98,7 @@ const QueryFlyoutComponent: React.FC = ({
setFieldValue('version', [savedQuery.version]);
}
},
- [isFieldSupported, setFieldValue]
+ [isFieldSupported, setFieldValue, reset]
);
return (
From e88910a1c67c3eb3d6301fe90bf8f0e38a2af221 Mon Sep 17 00:00:00 2001
From: Spencer
Date: Mon, 12 Jul 2021 15:11:49 -0700
Subject: [PATCH 21/52] [kbn/optimizer] rewrite bazel-out paths to source paths
(#105154)
Co-authored-by: spalger
---
.../src/public_path_module_creator.ts | 9 +++
.../__snapshots__/parse_path.test.ts.snap | 38 +++++------
.../kbn-optimizer/src/common/parse_path.ts | 64 ++++++++++++++++---
.../basic_optimization.test.ts | 10 ++-
.../worker/populate_bundle_cache_plugin.ts | 59 ++++++++++++++---
src/dev/precommit_hook/casing_check_config.js | 6 +-
6 files changed, 141 insertions(+), 45 deletions(-)
create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts
new file mode 100644
index 0000000000000..b03ee16d2f746
--- /dev/null
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+// stub
diff --git a/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap b/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap
index f537674c3fff7..2a30694afb826 100644
--- a/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap
+++ b/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`parseDirPath() parses / 1`] = `
-Object {
+ParsedPath {
"dirs": Array [],
"filename": undefined,
"query": undefined,
@@ -10,7 +10,7 @@ Object {
`;
exports[`parseDirPath() parses /foo 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
],
@@ -21,7 +21,7 @@ Object {
`;
exports[`parseDirPath() parses /foo/bar/baz 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -34,7 +34,7 @@ Object {
`;
exports[`parseDirPath() parses /foo/bar/baz/ 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -47,7 +47,7 @@ Object {
`;
exports[`parseDirPath() parses c:\\ 1`] = `
-Object {
+ParsedPath {
"dirs": Array [],
"filename": undefined,
"query": undefined,
@@ -56,7 +56,7 @@ Object {
`;
exports[`parseDirPath() parses c:\\foo 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
],
@@ -67,7 +67,7 @@ Object {
`;
exports[`parseDirPath() parses c:\\foo\\bar\\baz 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -80,7 +80,7 @@ Object {
`;
exports[`parseDirPath() parses c:\\foo\\bar\\baz\\ 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -93,7 +93,7 @@ Object {
`;
exports[`parseFilePath() parses /foo 1`] = `
-Object {
+ParsedPath {
"dirs": Array [],
"filename": "foo",
"query": undefined,
@@ -102,7 +102,7 @@ Object {
`;
exports[`parseFilePath() parses /foo/bar/baz 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -114,7 +114,7 @@ Object {
`;
exports[`parseFilePath() parses /foo/bar/baz.json 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -126,7 +126,7 @@ Object {
`;
exports[`parseFilePath() parses /foo/bar/baz.json?light 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -140,7 +140,7 @@ Object {
`;
exports[`parseFilePath() parses /foo/bar/baz.json?light=true&dark=false 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -155,7 +155,7 @@ Object {
`;
exports[`parseFilePath() parses c:/foo/bar/baz.json 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -167,7 +167,7 @@ Object {
`;
exports[`parseFilePath() parses c:\\foo 1`] = `
-Object {
+ParsedPath {
"dirs": Array [],
"filename": "foo",
"query": undefined,
@@ -176,7 +176,7 @@ Object {
`;
exports[`parseFilePath() parses c:\\foo\\bar\\baz 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -188,7 +188,7 @@ Object {
`;
exports[`parseFilePath() parses c:\\foo\\bar\\baz.json 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -200,7 +200,7 @@ Object {
`;
exports[`parseFilePath() parses c:\\foo\\bar\\baz.json?dark 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
@@ -214,7 +214,7 @@ Object {
`;
exports[`parseFilePath() parses c:\\foo\\bar\\baz.json?dark=true&light=false 1`] = `
-Object {
+ParsedPath {
"dirs": Array [
"foo",
"bar",
diff --git a/packages/kbn-optimizer/src/common/parse_path.ts b/packages/kbn-optimizer/src/common/parse_path.ts
index 7ea0042db25c9..da3744ba477bd 100644
--- a/packages/kbn-optimizer/src/common/parse_path.ts
+++ b/packages/kbn-optimizer/src/common/parse_path.ts
@@ -9,17 +9,61 @@
import normalizePath from 'normalize-path';
import Qs from 'querystring';
+class ParsedPath {
+ constructor(
+ public readonly root: string,
+ public readonly dirs: string[],
+ public readonly query?: Record,
+ public readonly filename?: string
+ ) {}
+
+ private indexOfDir(match: string | RegExp, fromIndex: number = 0) {
+ for (let i = fromIndex; i < this.dirs.length; i++) {
+ if (this.matchDir(i, match)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private matchDir(i: number, match: string | RegExp) {
+ return typeof match === 'string' ? this.dirs[i] === match : match.test(this.dirs[i]);
+ }
+
+ matchDirs(...segments: Array) {
+ const [first, ...rest] = segments;
+ let fromIndex = 0;
+ while (true) {
+ // do the dirs include the first segment to match?
+ const startIndex = this.indexOfDir(first, fromIndex);
+ if (startIndex === -1) {
+ return;
+ }
+
+ // are all of the ...rest segments also matched at this point?
+ if (!rest.length || rest.every((seg, i) => this.matchDir(startIndex + 1 + i, seg))) {
+ return { startIndex, endIndex: startIndex + rest.length };
+ }
+
+ // no match, search again, this time looking at instances after the matched instance
+ fromIndex = startIndex + 1;
+ }
+ }
+}
+
/**
* Parse an absolute path, supporting normalized paths from webpack,
* into a list of directories and root
*/
export function parseDirPath(path: string) {
const filePath = parseFilePath(path);
- return {
- ...filePath,
- dirs: [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])],
- filename: undefined,
- };
+ return new ParsedPath(
+ filePath.root,
+ [...filePath.dirs, ...(filePath.filename ? [filePath.filename] : [])],
+ filePath.query,
+ undefined
+ );
}
export function parseFilePath(path: string) {
@@ -32,10 +76,10 @@ export function parseFilePath(path: string) {
}
const [root, ...others] = normalized.split('/');
- return {
- root: root === '' ? '/' : root,
- dirs: others.slice(0, -1),
+ return new ParsedPath(
+ root === '' ? '/' : root,
+ others.slice(0, -1),
query,
- filename: others[others.length - 1] || undefined,
- };
+ others[others.length - 1] || undefined
+ );
}
diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts
index 97a7f33be673d..48d36b706b831 100644
--- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts
+++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts
@@ -15,7 +15,7 @@ import cpy from 'cpy';
import del from 'del';
import { tap, filter } from 'rxjs/operators';
import { REPO_ROOT } from '@kbn/utils';
-import { ToolingLog, createReplaceSerializer } from '@kbn/dev-utils';
+import { ToolingLog } from '@kbn/dev-utils';
import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '../index';
import { allValuesFrom } from '../common';
@@ -29,8 +29,6 @@ expect.addSnapshotSerializer({
test: (value: any) => typeof value === 'string' && value.includes(REPO_ROOT),
});
-expect.addSnapshotSerializer(createReplaceSerializer(/\w+-fastbuild/, '-fastbuild'));
-
const log = new ToolingLog({
level: 'error',
writeTo: {
@@ -132,7 +130,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => {
expect(foo.cache.getModuleCount()).toBe(6);
expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(`
Array [
- /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts,
@@ -155,7 +153,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => {
/node_modules/@kbn/optimizer/postcss.config.js,
/node_modules/css-loader/package.json,
/node_modules/style-loader/package.json,
- /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts,
@@ -175,7 +173,7 @@ it('builds expected bundles, saves bundle counts to metadata', async () => {
expect(baz.cache.getReferencedFiles()).toMatchInlineSnapshot(`
Array [
- /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/bazel-out/-fastbuild/bin/packages/kbn-ui-shared-deps/target/public_path_module_creator.js,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/packages/kbn-ui-shared-deps/src/public_path_module_creator.ts,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/kibana.json,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/x-pack/baz/public/index.ts,
/packages/kbn-optimizer/src/worker/entry_point_creator.ts,
diff --git a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts
index 8d890b31b639d..a3455d7ddf2b9 100644
--- a/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts
+++ b/packages/kbn-optimizer/src/worker/populate_bundle_cache_plugin.ts
@@ -6,11 +6,11 @@
* Side Public License, v 1.
*/
-import webpack from 'webpack';
-
import Path from 'path';
import { inspect } from 'util';
+import webpack from 'webpack';
+
import { Bundle, WorkerConfig, ascending, parseFilePath } from '../common';
import { BundleRefModule } from './bundle_ref_module';
import {
@@ -21,6 +21,20 @@ import {
getModulePath,
} from './webpack_helpers';
+function tryToResolveRewrittenPath(from: string, toResolve: string) {
+ try {
+ return require.resolve(toResolve);
+ } catch (error) {
+ if (error.code === 'MODULE_NOT_FOUND') {
+ throw new Error(
+ `attempted to rewrite bazel-out path [${from}] to [${toResolve}] but couldn't find the rewrite target`
+ );
+ }
+
+ throw error;
+ }
+}
+
/**
* sass-loader creates about a 40% overhead on the overall optimizer runtime, and
* so this constant is used to indicate to assignBundlesToWorkers() that there is
@@ -57,17 +71,44 @@ export class PopulateBundleCachePlugin {
let path = getModulePath(module);
let parsedPath = parseFilePath(path);
- if (parsedPath.dirs.includes('bazel-out')) {
- const index = parsedPath.dirs.indexOf('bazel-out');
- path = Path.join(
- workerConfig.repoRoot,
- 'bazel-out',
- ...parsedPath.dirs.slice(index + 1),
- parsedPath.filename ?? ''
+ const bazelOut = parsedPath.matchDirs(
+ 'bazel-out',
+ /-fastbuild$/,
+ 'bin',
+ 'packages',
+ /.*/,
+ 'target'
+ );
+
+ // if the module is referenced from one of our packages and resolved to the `bazel-out` dir
+ // we should rewrite our reference to point to the source file so that we can track the
+ // modified time of that file rather than the built output which is rebuilt all the time
+ // without actually changing
+ if (bazelOut) {
+ const packageDir = parsedPath.dirs[bazelOut.endIndex - 1];
+ const subDirs = parsedPath.dirs.slice(bazelOut.endIndex + 1);
+ path = tryToResolveRewrittenPath(
+ path,
+ Path.join(
+ workerConfig.repoRoot,
+ 'packages',
+ packageDir,
+ 'src',
+ ...subDirs,
+ parsedPath.filename
+ ? Path.basename(parsedPath.filename, Path.extname(parsedPath.filename))
+ : ''
+ )
);
parsedPath = parseFilePath(path);
}
+ if (parsedPath.matchDirs('bazel-out')) {
+ throw new Error(
+ `a bazel-out dir is being referenced by module [${path}] and not getting rewritten to its source location`
+ );
+ }
+
if (!parsedPath.dirs.includes('node_modules')) {
referencedFiles.add(path);
diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js
index ba18c085b649d..57ae640da3c84 100644
--- a/src/dev/precommit_hook/casing_check_config.js
+++ b/src/dev/precommit_hook/casing_check_config.js
@@ -77,7 +77,11 @@ export const IGNORE_FILE_GLOBS = [
*
* @type {Array}
*/
-export const KEBAB_CASE_DIRECTORY_GLOBS = ['packages/*', 'x-pack'];
+export const KEBAB_CASE_DIRECTORY_GLOBS = [
+ 'packages/*',
+ 'x-pack',
+ 'packages/kbn-optimizer/src/__fixtures__/mock_repo/packages/kbn-ui-shared-deps',
+];
/**
* These patterns are matched against directories and indicate
From 9ab26cf089cebd66fe1a5a33f49b487b9acf7d79 Mon Sep 17 00:00:00 2001
From: Brandon Morelli
Date: Mon, 12 Jul 2021 15:12:56 -0700
Subject: [PATCH 22/52] docs: APM updates for 7.14 (#104232)
Co-authored-by: Nathan L Smith
---
docs/apm/agent-configuration.asciidoc | 1 +
docs/apm/apm-alerts.asciidoc | 115 ++++++++----------
docs/apm/filters.asciidoc | 1 +
docs/apm/images/apm-agent-configuration.png | Bin 274436 -> 263010 bytes
docs/apm/images/apm-alert.png | Bin 589227 -> 422758 bytes
docs/apm/images/apm-error-group.png | Bin 308559 -> 334561 bytes
docs/apm/images/apm-logs-tab.png | Bin 439344 -> 558203 bytes
docs/apm/images/apm-services-overview.png | Bin 410666 -> 287982 bytes
docs/apm/images/apm-settings.png | Bin 206908 -> 227438 bytes
docs/apm/images/apm-span-detail.png | Bin 171713 -> 195719 bytes
docs/apm/images/apm-traces.png | Bin 191564 -> 259441 bytes
.../images/apm-transaction-duration-dist.png | Bin 60209 -> 61932 bytes
.../images/apm-transaction-response-dist.png | Bin 742410 -> 471222 bytes
docs/apm/images/apm-transaction-sample.png | Bin 256043 -> 314695 bytes
docs/apm/images/apm-transactions-overview.png | Bin 577820 -> 543872 bytes
docs/apm/images/service-maps-java.png | Bin 571180 -> 416593 bytes
docs/apm/images/service-maps.png | Bin 486694 -> 314343 bytes
docs/apm/service-maps.asciidoc | 1 +
docs/apm/transactions.asciidoc | 12 +-
docs/apm/troubleshooting.asciidoc | 1 +
docs/settings/apm-settings.asciidoc | 2 +-
21 files changed, 61 insertions(+), 72 deletions(-)
diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc
index 2574d254ac14c..f2e07412c4a38 100644
--- a/docs/apm/agent-configuration.asciidoc
+++ b/docs/apm/agent-configuration.asciidoc
@@ -43,6 +43,7 @@ Supported configurations are also tagged with the image:./images/dynamic-config.
[horizontal]
Go Agent:: {apm-go-ref}/configuration.html[Configuration reference]
+iOS agent:: _Not yet supported_
Java Agent:: {apm-java-ref}/configuration.html[Configuration reference]
.NET Agent:: {apm-dotnet-ref}/configuration.html[Configuration reference]
Node.js Agent:: {apm-node-ref}/configuration.html[Configuration reference]
diff --git a/docs/apm/apm-alerts.asciidoc b/docs/apm/apm-alerts.asciidoc
index 3e3e2b178ff10..42016ac08bfc7 100644
--- a/docs/apm/apm-alerts.asciidoc
+++ b/docs/apm/apm-alerts.asciidoc
@@ -1,69 +1,57 @@
[role="xpack"]
[[apm-alerts]]
-=== Alerts
+=== Alerts and rules
++++
Create an alert
++++
+The APM app allows you to define **rules** to detect complex conditions within your APM data
+and trigger built-in **actions** when those conditions are met.
-The APM app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions] feature.
-It provides a set of built-in **actions** and APM specific threshold **alerts** for you to use
-and enables central management of all alerts from <>.
+The following **rules** are supported:
+
+* Latency anomaly rule:
+Alert when latency of a service is abnormal
+* Transaction error rate threshold rule:
+Alert when the service's transaction error rate is above the defined threshold
+* Error count threshold rule:
+Alert when the number of errors in a service exceeds a defined threshold
[role="screenshot"]
image::apm/images/apm-alert.png[Create an alert in the APM app]
-For a walkthrough of the alert flyout panel, including detailed information on each configurable property,
-see Kibana's <>.
-
-The APM app supports four different types of alerts:
-
-* Transaction duration anomaly:
-alerts when the service's transaction duration reaches a certain anomaly score
-* Transaction duration threshold:
-alerts when the service's transaction duration exceeds a given time limit over a given time frame
-* Transaction error rate threshold:
-alerts when the service's transaction error rate is above the selected rate over a given time frame
-* Error count threshold:
-alerts when service exceeds a selected number of errors over a given time frame
+For a complete walkthrough of the **Create rule** flyout panel, including detailed information on each configurable property,
+see Kibana's <>.
-Below, we'll walk through the creation of two of these alerts.
+Below, we'll walk through the creation of two APM rules.
[float]
[[apm-create-transaction-alert]]
-=== Example: create a transaction duration alert
+=== Example: create a latency anomaly rule
-Transaction duration alerts trigger when the duration of a specific transaction type in a service exceeds a defined threshold.
-This guide will create an alert for the `opbeans-java` service based on the following criteria:
+Latency anomaly rules trigger when the latency of a service is abnormal.
+This guide will create an alert for all services based on the following criteria:
-* Environment: Production
-* Transaction type: `transaction.type:request`
-* Average request is above `1500ms` for the last 5 minutes
-* Check every 10 minutes, and repeat the alert every 30 minutes
-* Send the alert via Slack
+* Environment: production
+* Severity level: critical
+* Run every five minutes
+* Send an alert to a Slack channel only when the rule status changes
-From the APM app, navigate to the `opbeans-java` service and select
-**Alerts** > **Create threshold alert** > **Transaction duration**.
+From any page in the APM app, select **Alerts and rules** > **Latency** > **Create anomaly rule**.
+Change the name of the alert, but do not edit the tags.
-`Transaction duration | opbeans-java` is automatically set as the name of the alert,
-and `apm` and `service.name:opbeans-java` are added as tags.
-It's fine to change the name of the alert, but do not edit the tags.
+Based on the criteria above, define the following rule details:
-Based on the alert criteria, define the following alert details:
+* **Check every** - `5 minutes`
+* **Notify** - "Only on status change"
+* **Environment** - `all`
+* **Has anomaly with severity** - `critical`
-* **Check every** - `10 minutes`
-* **Notify every** - `30 minutes`
-* **TYPE** - `request`
-* **WHEN** - `avg`
-* **IS ABOVE** - `1500ms`
-* **FOR THE LAST** - `5 minutes`
-
-Select an action type.
-Multiple action types can be selected, but in this example, we want to post to a Slack channel.
+Next, add a connector. Multiple connectors can be selected, but in this example we're interested in Slack.
Select **Slack** > **Create a connector**.
Enter a name for the connector,
-and paste the webhook URL.
+and paste your Slack webhook URL.
See Slack's webhook documentation if you need to create one.
A default message is provided as a starting point for your alert.
@@ -72,35 +60,32 @@ to pass additional alert values at the time a condition is detected to an action
A list of available variables can be accessed by selecting the
**add variable** button image:apm/images/add-variable.png[add variable button].
-Select **Save**. The alert has been created and is now active!
+Click **Save**. The rule has been created and is now active!
[float]
[[apm-create-error-alert]]
-=== Example: create an error rate alert
+=== Example: create an error count threshold alert
-Error rate alerts trigger when the number of errors in a service exceeds a defined threshold.
-This guide creates an alert for the `opbeans-python` service based on the following criteria:
+The error count threshold alert triggers when the number of errors in a service exceeds a defined threshold.
+This guide will create an alert for all services based on the following criteria:
-* Environment: Production
+* All environments
* Error rate is above 25 for the last minute
-* Check every 1 minute, and repeat the alert every 10 minutes
-* Send the alert via email to the `opbeans-python` team
-
-From the APM app, navigate to the `opbeans-python` service and select
-**Alerts** > **Create threshold alert** > **Error rate**.
+* Check every 1 minute, and alert every time the rule is active
+* Send the alert via email to the site reliability team
-`Error rate | opbeans-python` is automatically set as the name of the alert,
-and `apm` and `service.name:opbeans-python` are added as tags.
-It's fine to change the name of the alert, but do not edit the tags.
+From any page in the APM app, select **Alerts and rules** > **Error count** > **Create threshold rule**.
+Change the name of the alert, but do not edit the tags.
-Based on the alert criteria, define the following alert details:
+Based on the criteria above, define the following rule details:
* **Check every** - `1 minute`
-* **Notify every** - `10 minutes`
-* **IS ABOVE** - `25 errors`
-* **FOR THE LAST** - `1 minute`
+* **Notify** - "Every time alert is active"
+* **Environment** - `all`
+* **Is above** - `25 errors`
+* **For the last** - `1 minute`
-Select the **Email** action type and click **Create a connector**.
+Select the **Email** connector and click **Create a connector**.
Fill out the required details: sender, host, port, etc., and click **save**.
A default message is provided as a starting point for your alert.
@@ -109,14 +94,14 @@ to pass additional alert values at the time a condition is detected to an action
A list of available variables can be accessed by selecting the
**add variable** button image:apm/images/add-variable.png[add variable button].
-Select **Save**. The alert has been created and is now active!
+Click **Save**. The alert has been created and is now active!
[float]
[[apm-alert-manage]]
-=== Manage alerts and actions
+=== Manage alerts and rules
-From the APM app, select **Alerts** > **View active alerts** to be taken to the Kibana alerts and actions management page.
-From this page, you can create, edit, disable, mute, and delete alerts, and create, edit, and disable connectors.
+From the APM app, select **Alerts and rules** > **Manage rules** to be taken to the Kibana **Rules and Connectors** page.
+From this page, you can disable, mute, and delete APM alerts.
[float]
[[apm-alert-more-info]]
@@ -126,4 +111,4 @@ See {kibana-ref}/alerting-getting-started.html[alerting and actions] for more in
NOTE: If you are using an **on-premise** Elastic Stack deployment with security,
communication between Elasticsearch and Kibana must have TLS configured.
-More information is in the alerting {kibana-ref}/alerting-setup.html#alerting-prerequisites[prerequisites].
\ No newline at end of file
+More information is in the alerting {kibana-ref}/alerting-setup.html#alerting-prerequisites[prerequisites].
diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc
index 56602ab7c05c9..c0ea81c87378b 100644
--- a/docs/apm/filters.asciidoc
+++ b/docs/apm/filters.asciidoc
@@ -36,6 +36,7 @@ It's vital to be consistent when naming environments in your agents.
To learn how to configure service environments, see the specific agent documentation:
* *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`]
+* *iOS agent:* _Not yet supported_
* *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`]
* *.NET:* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`]
* *Node.js:* {apm-node-ref}/configuration.html#environment[`environment`]
diff --git a/docs/apm/images/apm-agent-configuration.png b/docs/apm/images/apm-agent-configuration.png
index 07398f0609187d24616bef3e82e21e1eb57ba40e..22fd9d75c3d730c847839865215ac8cd2a2fdeea 100644
GIT binary patch
literal 263010
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