From 4845bef18194bf90778f6c156b7548dfc98f86c8 Mon Sep 17 00:00:00 2001
From: John Dorlus <silne.dorlus@elastic.co>
Date: Fri, 26 Jun 2020 11:59:50 -0400
Subject: [PATCH 01/21] Fixed issue where promise chain was broken. (#70004)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 x-pack/test/functional/apps/rollup_job/rollup_jobs.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js
index 28f5e2ae00f09..5b6484d7184f3 100644
--- a/x-pack/test/functional/apps/rollup_job/rollup_jobs.js
+++ b/x-pack/test/functional/apps/rollup_job/rollup_jobs.js
@@ -31,9 +31,9 @@ export default function ({ getService, getPageObjects }) {
     it('create new rollup job', async () => {
       const interval = '1000ms';
 
-      pastDates.map(async (day) => {
+      for (const day of pastDates) {
         await es.index(mockIndices(day, rollupSourceDataPrepend));
-      });
+      }
 
       await PageObjects.common.navigateToApp('rollupJob');
       await PageObjects.rollup.createNewRollUpJob(

From 100a5fd18b7c500a99932a8e8c0cf47c12bfe7e5 Mon Sep 17 00:00:00 2001
From: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Date: Fri, 26 Jun 2020 17:12:21 +0100
Subject: [PATCH 02/21] [SIEM] Update readme for timeline apis (#67038)

* update doc

* update unit test

* remove redundant params

* fix types

* update readme

* update readme

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 .../public/timelines/containers/api.ts        |  12 +-
 .../server/lib/timeline/routes/README.md      | 299 +++++++++++++++++-
 .../routes/__mocks__/request_responses.ts     |   1 -
 .../routes/export_timelines_route.test.ts     |   2 +-
 .../routes/schemas/export_timelines_schema.ts |   1 -
 5 files changed, 301 insertions(+), 14 deletions(-)

diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts
index 10893feccfed4..a2277897e99bf 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts
@@ -103,8 +103,6 @@ export const persistTimeline = async ({
 
 export const importTimelines = async ({
   fileToImport,
-  overwrite = false,
-  signal,
 }: ImportDataProps): Promise<ImportDataResponse> => {
   const formData = new FormData();
   formData.append('file', fileToImport);
@@ -112,31 +110,25 @@ export const importTimelines = async ({
   return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, {
     method: 'POST',
     headers: { 'Content-Type': undefined },
-    query: { overwrite },
     body: formData,
-    signal,
   });
 };
 
 export const exportSelectedTimeline: ExportSelectedData = async ({
-  excludeExportDetails = false,
   filename = `timelines_export.ndjson`,
   ids = [],
   signal,
 }): Promise<Blob> => {
   const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined;
-  const response = await KibanaServices.get().http.fetch<Blob>(`${TIMELINE_EXPORT_URL}`, {
+  const response = await KibanaServices.get().http.fetch<{ body: Blob }>(`${TIMELINE_EXPORT_URL}`, {
     method: 'POST',
     body,
     query: {
-      exclude_export_details: excludeExportDetails,
       file_name: filename,
     },
-    signal,
-    asResponse: true,
   });
 
-  return response.body!;
+  return response.body;
 };
 
 export const getDraftTimeline = async ({
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/README.md b/x-pack/plugins/security_solution/server/lib/timeline/routes/README.md
index 2c5547e39fc4e..ee57d5bb3d031 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/README.md
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/README.md
@@ -323,4 +323,301 @@ kbn-version: 8.0.0
      "timelineId":"f5a4bd10-83cd-11ea-bf78-0547a65f1281", // This is a must as well
      "version":"Wzg2LDFd" // Please provide the existing timeline version
 }
-```
\ No newline at end of file
+```
+
+## Export timeline api
+
+#### POST /api/timeline/_export
+
+##### Authorization
+
+Type: Basic Auth
+
+username: Your Kibana username
+
+password: Your Kibana password
+
+
+
+
+##### Request header
+
+```
+
+Content-Type: application/json
+
+kbn-version: 8.0.0
+
+```
+
+##### Request param
+
+```
+file_name:	${filename}.ndjson
+```
+
+##### Request body
+```json
+{
+	ids: [
+		${timelineId}
+	]
+}
+```
+
+## Import timeline api
+
+#### POST /api/timeline/_import
+
+##### Authorization
+
+Type: Basic Auth
+
+username: Your Kibana username
+
+password: Your Kibana password
+
+
+
+
+##### Request header
+
+```
+
+Content-Type: application/json
+
+kbn-version: 8.0.0
+
+```
+
+##### Request body
+
+```
+{
+  file: sample.ndjson
+}
+```
+
+
+(each json in the file should match this format)
+example:
+```
+{"savedObjectId":"a3002fd0-781b-11ea-85e4-df9002f1452c","version":"WzIzLDFd","columns":[{"columnHeaderType":"not-filtered","id":"@timestamp"},{"columnHeaderType":"not-filtered","id":"message"},{"columnHeaderType":"not-filtered","id":"event.category"},{"columnHeaderType":"not-filtered","id":"event.action"},{"columnHeaderType":"not-filtered","id":"host.name"},{"columnHeaderType":"not-filtered","id":"source.ip"},{"columnHeaderType":"not-filtered","id":"destination.ip"},{"columnHeaderType":"not-filtered","id":"user.name"}],"dataProviders":[],"description":"tes description","eventType":"all","filters":[{"meta":{"field":null,"negate":false,"alias":null,"disabled":false,"params":"{\"query\":\"MacBook-Pro-de-Gloria.local\"}","type":"phrase","key":"host.name"},"query":"{\"match_phrase\":{\"host.name\":\"MacBook-Pro-de-Gloria.local\"}}","missing":null,"exists":null,"match_all":null,"range":null,"script":null}],"kqlMode":"filter","kqlQuery":{"filterQuery":{"serializedQuery":"{\"bool\":{\"should\":[{\"exists\":{\"field\":\"host.name\"}}],\"minimum_should_match\":1}}","kuery":{"expression":"host.name: *","kind":"kuery"}}},"title":"Test","dateRange":{"start":1585227005527,"end":1585313405527},"savedQueryId":null,"sort":{"columnId":"@timestamp","sortDirection":"desc"},"created":1586187068132,"createdBy":"angela","updated":1586187068132,"updatedBy":"angela","eventNotes":[],"globalNotes":[{"noteId":"a3b4d9d0-781b-11ea-85e4-df9002f1452c","version":"WzI1LDFd","note":"this is a note","timelineId":"a3002fd0-781b-11ea-85e4-df9002f1452c","created":1586187069313,"createdBy":"angela","updated":1586187069313,"updatedBy":"angela"}],"pinnedEventIds":[]}
+```
+
+##### Response
+```
+{"success":true,"success_count":1,"errors":[]}
+```
+
+## Get draft timeline api
+
+#### GET /api/timeline/_draft
+
+##### Authorization
+
+Type: Basic Auth
+
+username: Your Kibana username
+
+password: Your Kibana password
+
+
+##### Request header
+
+```
+
+Content-Type: application/json
+
+kbn-version: 8.0.0
+
+```
+
+##### Request param
+```
+timelineType: `default` or `template`
+```
+
+##### Response
+```json
+{
+    "data": {
+        "persistTimeline": {
+            "timeline": {
+                "savedObjectId": "ababbd90-99de-11ea-8446-1d7fd9f03ebf",
+                "version": "WzM2MiwzXQ==",
+                "columns": [
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "@timestamp"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "message"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "event.category"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "event.action"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "host.name"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "source.ip"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "destination.ip"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "user.name"
+                    }
+                ],
+                "dataProviders": [],
+                "description": "",
+                "eventType": "all",
+                "filters": [],
+                "kqlMode": "filter",
+                "timelineType": "default",
+                "kqlQuery": {
+                    "filterQuery": null
+                },
+                "title": "",
+                "sort": {
+                    "columnId": "@timestamp",
+                    "sortDirection": "desc"
+                },
+                "status": "draft",
+                "created": 1589899222908,
+                "createdBy": "casetester",
+                "updated": 1589899222908,
+                "updatedBy": "casetester",
+                "templateTimelineId": null,
+                "templateTimelineVersion": null,
+                "favorite": [],
+                "eventIdToNoteIds": [],
+                "noteIds": [],
+                "notes": [],
+                "pinnedEventIds": [],
+                "pinnedEventsSaveObject": []
+            }
+        }
+    }
+}
+```
+
+## Create draft timeline api
+
+#### POST /api/timeline/_draft
+
+##### Authorization
+
+Type: Basic Auth
+
+username: Your Kibana username
+
+password: Your Kibana password
+
+
+##### Request header
+
+```
+
+Content-Type: application/json
+
+kbn-version: 8.0.0
+
+```
+
+##### Request body
+
+```json
+{
+	"timelineType": "default" or "template"
+}
+```
+
+##### Response
+```json
+{
+    "data": {
+        "persistTimeline": {
+            "timeline": {
+                "savedObjectId": "ababbd90-99de-11ea-8446-1d7fd9f03ebf",
+                "version": "WzQyMywzXQ==",
+                "columns": [
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "@timestamp"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "message"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "event.category"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "event.action"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "host.name"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "source.ip"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "destination.ip"
+                    },
+                    {
+                        "columnHeaderType": "not-filtered",
+                        "id": "user.name"
+                    }
+                ],
+                "dataProviders": [],
+                "description": "",
+                "eventType": "all",
+                "filters": [],
+                "kqlMode": "filter",
+                "timelineType": "default",
+                "kqlQuery": {
+                    "filterQuery": null
+                },
+                "title": "",
+                "sort": {
+                    "columnId": "@timestamp",
+                    "sortDirection": "desc"
+                },
+                "status": "draft",
+                "created": 1589903306582,
+                "createdBy": "casetester",
+                "updated": 1589903306582,
+                "updatedBy": "casetester",
+                "templateTimelineId": null,
+                "templateTimelineVersion": null,
+                "favorite": [],
+                "eventIdToNoteIds": [],
+                "noteIds": [],
+                "notes": [],
+                "pinnedEventIds": [],
+                "pinnedEventsSaveObject": []
+            }
+        }
+    }
+}
+```
+
+
+
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts
index 470ba1a853b58..0b320459c76a8 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts
@@ -23,7 +23,6 @@ export const getExportTimelinesRequest = () =>
     path: TIMELINE_EXPORT_URL,
     query: {
       file_name: 'mock_export_timeline.ndjson',
-      exclude_export_details: 'false',
     },
     body: {
       ids: ['f0e58720-57b6-11ea-b88d-3f1a31716be8', '890b8ae0-57df-11ea-a7c9-3976b7f1cb37'],
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts
index 2bccb7c393837..c66bf7b192c62 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/export_timelines_route.test.ts
@@ -98,7 +98,7 @@ describe('export timelines', () => {
       const result = server.validate(request);
 
       expect(result.badRequest.mock.calls[1][0]).toEqual(
-        'Invalid value "undefined" supplied to "file_name",Invalid value "undefined" supplied to "exclude_export_details",Invalid value "undefined" supplied to "exclude_export_details"'
+        'Invalid value "undefined" supplied to "file_name"'
       );
     });
   });
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts
index 6f8265903b2a7..9264f1e3e5047 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/schemas/export_timelines_schema.ts
@@ -8,7 +8,6 @@ import * as rt from 'io-ts';
 
 export const exportTimelinesQuerySchema = rt.type({
   file_name: rt.string,
-  exclude_export_details: rt.union([rt.literal('true'), rt.literal('false')]),
 });
 
 export const exportTimelinesRequestBodySchema = rt.type({

From 3ac5bc53236608fe598bdc7f45c226a91544b2ca Mon Sep 17 00:00:00 2001
From: Anton Dosov <anton.dosov@elastic.co>
Date: Fri, 26 Jun 2020 18:33:32 +0200
Subject: [PATCH 03/21] Dynamic uiActions & license support (#68507)

This pr adds convenient license support to dynamic uiActions in x-pack.
Works for actions created with action factories & drilldowns.

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 .../public/actions/action_internal.ts         |  3 +
 .../public/service/ui_actions_service.test.ts |  2 +-
 .../public/service/ui_actions_service.ts      |  1 -
 .../dashboard_to_url_drilldown/index.tsx      |  2 +
 x-pack/plugins/licensing/public/mocks.ts      | 14 +++-
 .../plugins/ui_actions_enhanced/kibana.json   |  3 +-
 .../action_wizard/action_wizard.test.tsx      | 26 +++++-
 .../action_wizard/action_wizard.tsx           | 44 +++++++++--
 .../components/action_wizard/test_data.tsx    | 10 ++-
 .../connected_flyout_manage_drilldowns.tsx    |  7 ++
 .../i18n.ts                                   | 20 +++++
 .../flyout_list_manage_drilldowns.story.tsx   |  2 +-
 .../form_drilldown_wizard.tsx                 | 27 ++++++-
 .../list_manage_drilldowns.test.tsx           |  7 +-
 .../list_manage_drilldowns.tsx                | 20 ++++-
 .../public/drilldowns/drilldown_definition.ts |  7 ++
 .../dynamic_actions/action_factory.test.ts    | 46 +++++++++++
 .../public/dynamic_actions/action_factory.ts  | 30 +++++--
 .../action_factory_definition.ts              | 11 ++-
 .../dynamic_action_manager.test.ts            | 79 +++++++++++++++----
 .../dynamic_actions/dynamic_action_manager.ts | 16 ++--
 .../ui_actions_enhanced/public/mocks.ts       |  2 +
 .../ui_actions_enhanced/public/plugin.ts      | 25 +++++-
 .../ui_actions_service_enhancements.test.ts   | 11 ++-
 .../ui_actions_service_enhancements.ts        | 13 ++-
 25 files changed, 370 insertions(+), 58 deletions(-)
 create mode 100644 x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts

diff --git a/src/plugins/ui_actions/public/actions/action_internal.ts b/src/plugins/ui_actions/public/actions/action_internal.ts
index aba1e22fe09ee..10eb760b13089 100644
--- a/src/plugins/ui_actions/public/actions/action_internal.ts
+++ b/src/plugins/ui_actions/public/actions/action_internal.ts
@@ -24,6 +24,9 @@ import { Presentable } from '../util/presentable';
 import { uiToReactComponent } from '../../../kibana_react/public';
 import { ActionType } from '../types';
 
+/**
+ * @internal
+ */
 export class ActionInternal<A extends ActionDefinition = ActionDefinition>
   implements Action<Context<A>>, Presentable<Context<A>> {
   constructor(public readonly definition: A) {}
diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts
index 45a1bdffa52ad..39502c3dd17fc 100644
--- a/src/plugins/ui_actions/public/service/ui_actions_service.test.ts
+++ b/src/plugins/ui_actions/public/service/ui_actions_service.test.ts
@@ -20,7 +20,7 @@
 import { UiActionsService } from './ui_actions_service';
 import { Action, ActionInternal, createAction } from '../actions';
 import { createHelloWorldAction } from '../tests/test_samples';
-import { ActionRegistry, TriggerRegistry, TriggerId, ActionType } from '../types';
+import { TriggerRegistry, TriggerId, ActionType, ActionRegistry } from '../types';
 import { Trigger } from '../triggers';
 
 // Casting to ActionType or TriggerId is a hack - in a real situation use
diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts
index 760897f0287d8..11f5769a94648 100644
--- a/src/plugins/ui_actions/public/service/ui_actions_service.ts
+++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts
@@ -220,7 +220,6 @@ export class UiActionsService {
     for (const [key, value] of this.actions.entries()) actions.set(key, value);
     for (const [key, value] of this.triggerToActions.entries())
       triggerToActions.set(key, [...value]);
-
     return new UiActionsService({ triggers, actions, triggerToActions });
   };
 }
diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx
index 4810fb2d6ad8d..5e4ba54864461 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx
+++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx
@@ -39,6 +39,8 @@ export class DashboardToUrlDrilldown implements Drilldown<Config, ActionContext>
 
   public readonly order = 8;
 
+  readonly minimalLicense = 'gold'; // example of minimal license support
+
   public readonly getDisplayName = () => 'Go to URL (example)';
 
   public readonly euiIcon = 'link';
diff --git a/x-pack/plugins/licensing/public/mocks.ts b/x-pack/plugins/licensing/public/mocks.ts
index 68b280c5341f2..8421a343d91ca 100644
--- a/x-pack/plugins/licensing/public/mocks.ts
+++ b/x-pack/plugins/licensing/public/mocks.ts
@@ -4,7 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 import { BehaviorSubject } from 'rxjs';
-import { LicensingPluginSetup } from './types';
+import { LicensingPluginSetup, LicensingPluginStart } from './types';
 import { licenseMock } from '../common/licensing.mock';
 
 const createSetupMock = () => {
@@ -18,7 +18,19 @@ const createSetupMock = () => {
   return mock;
 };
 
+const createStartMock = () => {
+  const license = licenseMock.createLicense();
+  const mock: jest.Mocked<LicensingPluginStart> = {
+    license$: new BehaviorSubject(license),
+    refresh: jest.fn(),
+  };
+  mock.refresh.mockResolvedValue(license);
+
+  return mock;
+};
+
 export const licensingMock = {
   createSetup: createSetupMock,
+  createStart: createStartMock,
   ...licenseMock,
 };
diff --git a/x-pack/plugins/ui_actions_enhanced/kibana.json b/x-pack/plugins/ui_actions_enhanced/kibana.json
index 027004f165c3b..a813903d8b212 100644
--- a/x-pack/plugins/ui_actions_enhanced/kibana.json
+++ b/x-pack/plugins/ui_actions_enhanced/kibana.json
@@ -4,7 +4,8 @@
   "configPath": ["xpack", "ui_actions_enhanced"],
   "requiredPlugins": [
     "embeddable",
-    "uiActions"
+    "uiActions",
+    "licensing"
   ],
   "server": false,
   "ui": true
diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx
index 745b3c403afc6..78252dccd20d2 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx
@@ -7,7 +7,15 @@
 import React from 'react';
 import { cleanup, fireEvent, render } from '@testing-library/react/pure';
 import { TEST_SUBJ_ACTION_FACTORY_ITEM, TEST_SUBJ_SELECTED_ACTION_FACTORY } from './action_wizard';
-import { dashboardFactory, dashboards, Demo, urlFactory } from './test_data';
+import {
+  dashboardFactory,
+  dashboards,
+  Demo,
+  urlFactory,
+  urlDrilldownActionFactory,
+} from './test_data';
+import { ActionFactory } from '../../dynamic_actions';
+import { licenseMock } from '../../../../licensing/common/licensing.mock';
 
 // TODO: afterEach is not available for it globally during setup
 // https://github.com/elastic/kibana/issues/59469
@@ -54,3 +62,19 @@ test('If only one actions factory is available then actionFactory selection is e
   // check that can't change to action factory type
   expect(screen.queryByTestId(/change/i)).not.toBeInTheDocument();
 });
+
+test('If not enough license, button is disabled', () => {
+  const urlWithGoldLicense = new ActionFactory(
+    {
+      ...urlDrilldownActionFactory,
+      minimalLicense: 'gold',
+    },
+    () => licenseMock.createLicense()
+  );
+  const screen = render(<Demo actionFactories={[dashboardFactory, urlWithGoldLicense]} />);
+
+  // check that all factories are displayed to pick
+  expect(screen.getAllByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).toHaveLength(2);
+
+  expect(screen.getByText(/Go to URL/i)).toBeDisabled();
+});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
index ccadf60426edf..6769c8bab0732 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx
@@ -10,10 +10,12 @@ import {
   EuiFlexGroup,
   EuiFlexItem,
   EuiIcon,
+  EuiKeyPadMenuItem,
   EuiSpacer,
   EuiText,
-  EuiKeyPadMenuItem,
+  EuiToolTip,
 } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
 import { txtChangeButton } from './i18n';
 import './action_wizard.scss';
 import { ActionFactory } from '../../dynamic_actions';
@@ -61,7 +63,11 @@ export const ActionWizard: React.FC<ActionWizardProps> = ({
   context,
 }) => {
   // auto pick action factory if there is only 1 available
-  if (!currentActionFactory && actionFactories.length === 1) {
+  if (
+    !currentActionFactory &&
+    actionFactories.length === 1 &&
+    actionFactories[0].isCompatibleLicence()
+  ) {
     onActionFactoryChange(actionFactories[0]);
   }
 
@@ -175,24 +181,46 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({
     willChange: 'opacity',
   };
 
+  /**
+   * make sure not compatible factories are in the end
+   */
+  const ensureOrder = (factories: ActionFactory[]) => {
+    const compatibleLicense = factories.filter((f) => f.isCompatibleLicence());
+    const notCompatibleLicense = factories.filter((f) => !f.isCompatibleLicence());
+    return [
+      ...compatibleLicense.sort((f1, f2) => f2.order - f1.order),
+      ...notCompatibleLicense.sort((f1, f2) => f2.order - f1.order),
+    ];
+  };
+
   return (
     <EuiFlexGroup gutterSize="m" wrap={true} style={firefoxBugFix}>
-      {[...actionFactories]
-        .sort((f1, f2) => f2.order - f1.order)
-        .map((actionFactory) => (
-          <EuiFlexItem grow={false} key={actionFactory.id}>
+      {ensureOrder(actionFactories).map((actionFactory) => (
+        <EuiFlexItem grow={false} key={actionFactory.id}>
+          <EuiToolTip
+            content={
+              !actionFactory.isCompatibleLicence() && (
+                <FormattedMessage
+                  defaultMessage="Insufficient license level"
+                  id="xpack.uiActionsEnhanced.components.actionWizard.insufficientLicenseLevelTooltip"
+                />
+              )
+            }
+          >
             <EuiKeyPadMenuItem
               className="auaActionWizard__actionFactoryItem"
               label={actionFactory.getDisplayName(context)}
               data-test-subj={`${TEST_SUBJ_ACTION_FACTORY_ITEM}-${actionFactory.id}`}
               onClick={() => onActionFactorySelected(actionFactory)}
+              disabled={!actionFactory.isCompatibleLicence()}
             >
               {actionFactory.getIconType(context) && (
                 <EuiIcon type={actionFactory.getIconType(context)!} size="m" />
               )}
             </EuiKeyPadMenuItem>
-          </EuiFlexItem>
-        ))}
+          </EuiToolTip>
+        </EuiFlexItem>
+      ))}
     </EuiFlexGroup>
   );
 };
diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx
index 0a135e60126ca..2672a086dca73 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx
@@ -10,6 +10,7 @@ import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/p
 import { ActionWizard } from './action_wizard';
 import { ActionFactoryDefinition, ActionFactory } from '../../dynamic_actions';
 import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public';
+import { licenseMock } from '../../../../licensing/common/licensing.mock';
 
 type ActionBaseConfig = object;
 
@@ -101,10 +102,13 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition<
   create: () => ({
     id: 'test',
     execute: async () => alert('Navigate to dashboard!'),
+    enhancements: {},
   }),
 };
 
-export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory);
+export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, () =>
+  licenseMock.createLicense()
+);
 
 interface UrlDrilldownConfig {
   url: string;
@@ -159,7 +163,9 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition<UrlDrilldownConf
   create: () => null as any,
 };
 
-export const urlFactory = new ActionFactory(urlDrilldownActionFactory);
+export const urlFactory = new ActionFactory(urlDrilldownActionFactory, () =>
+  licenseMock.createLicense()
+);
 
 export function Demo({ actionFactories }: { actionFactories: Array<ActionFactory<any>> }) {
   const [state, setState] = useState<{
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
index fbc72d0470635..20d15b4f4d2bd 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx
@@ -18,6 +18,8 @@ import {
 import { useContainerState } from '../../../../../../../src/plugins/kibana_utils/public';
 import { DrilldownListItem } from '../list_manage_drilldowns';
 import {
+  insufficientLicenseLevel,
+  invalidDrilldownType,
   toastDrilldownCreated,
   toastDrilldownDeleted,
   toastDrilldownEdited,
@@ -133,6 +135,11 @@ export function createFlyoutManageDrilldowns({
         drilldownName: drilldown.action.name,
         actionName: actionFactory?.getDisplayName(factoryContext) ?? drilldown.action.factoryId,
         icon: actionFactory?.getIconType(factoryContext),
+        error: !actionFactory
+          ? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development
+          : !actionFactory.isCompatibleLicence()
+          ? insufficientLicenseLevel
+          : undefined,
       };
     }
 
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/i18n.ts
index e75ee2634aa43..4b2be5db0c558 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/i18n.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/i18n.ts
@@ -86,3 +86,23 @@ export const toastDrilldownsCRUDError = i18n.translate(
     description: 'Title for generic error toast when persisting drilldown updates failed',
   }
 );
+
+export const insufficientLicenseLevel = i18n.translate(
+  'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.insufficientLicenseLevelError',
+  {
+    defaultMessage: 'Insufficient license level',
+    description:
+      'User created drilldown with higher license type, but then downgraded the license. This error is shown in the list near created drilldown',
+  }
+);
+
+export const invalidDrilldownType = (type: string) =>
+  i18n.translate(
+    'xpack.uiActionsEnhanced.drilldowns.components.flyoutDrilldownWizard.invalidDrilldownType',
+    {
+      defaultMessage: "Drilldown type {type} doesn't exist",
+      values: {
+        type,
+      },
+    }
+  );
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx
index 0529f0451b16a..603de39bc8908 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.story.tsx
@@ -15,7 +15,7 @@ storiesOf('components/FlyoutListManageDrilldowns', module).add('default', () =>
       drilldowns={[
         { id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' },
         { id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' },
-        { id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3' },
+        { id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3', error: 'Some error...' },
       ]}
     />
   </EuiFlyout>
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx
index 622ed58e3625d..e7e7f72dbf58f 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx
@@ -5,11 +5,14 @@
  */
 
 import React from 'react';
-import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer } from '@elastic/eui';
+import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
 import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n';
 import { ActionFactory } from '../../../dynamic_actions';
 import { ActionWizard } from '../../../components/action_wizard';
 
+const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions';
+
 const noopFn = () => {};
 
 export interface FormDrilldownWizardProps {
@@ -49,10 +52,32 @@ export const FormDrilldownWizard: React.FC<FormDrilldownWizardProps> = ({
     </EuiFormRow>
   );
 
+  const hasNotCompatibleLicenseFactory = () =>
+    actionFactories?.some((f) => !f.isCompatibleLicence());
+
+  const renderGetMoreActionsLink = () => (
+    <EuiText size="s">
+      <EuiLink
+        href={GET_MORE_ACTIONS_LINK}
+        target="_blank"
+        external
+        data-test-subj={'getMoreActionsLink'}
+      >
+        <FormattedMessage
+          id="xpack.uiActionsEnhanced.drilldowns.components.FormDrilldownWizard.getMoreActionsLinkLabel"
+          defaultMessage="Get more actions"
+        />
+      </EuiLink>
+    </EuiText>
+  );
+
   const actionWizard = (
     <EuiFormRow
       label={actionFactories?.length > 1 ? txtDrilldownAction : undefined}
       fullWidth={true}
+      labelAppend={
+        !currentActionFactory && hasNotCompatibleLicenseFactory() && renderGetMoreActionsLink()
+      }
     >
       <ActionWizard
         actionFactories={actionFactories}
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.test.tsx
index 715bba74cf8ec..889f8983254d5 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.test.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.test.tsx
@@ -19,7 +19,7 @@ afterEach(cleanup);
 const drilldowns: DrilldownListItem[] = [
   { id: '1', actionName: 'Dashboard', drilldownName: 'Drilldown 1' },
   { id: '2', actionName: 'Dashboard', drilldownName: 'Drilldown 2' },
-  { id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3' },
+  { id: '3', actionName: 'Dashboard', drilldownName: 'Drilldown 3', error: 'an error' },
 ];
 
 test('Render list of drilldowns', () => {
@@ -67,3 +67,8 @@ test('Can delete drilldowns', () => {
 
   expect(fn).toBeCalledWith([drilldowns[1].id, drilldowns[2].id]);
 });
+
+test('Error is displayed', () => {
+  const screen = render(<ListManageDrilldowns drilldowns={drilldowns} />);
+  expect(screen.getByLabelText('an error')).toBeInTheDocument();
+});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx
index cd41a3d6ec23a..b828c4d7d076d 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx
@@ -14,6 +14,7 @@ import {
   EuiIcon,
   EuiSpacer,
   EuiTextColor,
+  EuiToolTip,
 } from '@elastic/eui';
 import React, { useState } from 'react';
 import {
@@ -28,6 +29,7 @@ export interface DrilldownListItem {
   actionName: string;
   drilldownName: string;
   icon?: string;
+  error?: string;
 }
 
 export interface ListManageDrilldownsProps {
@@ -52,11 +54,27 @@ export function ListManageDrilldowns({
 
   const columns: Array<EuiBasicTableColumn<DrilldownListItem>> = [
     {
-      field: 'drilldownName',
       name: 'Name',
       truncateText: true,
       width: '50%',
       'data-test-subj': 'drilldownListItemName',
+      render: (drilldown: DrilldownListItem) => (
+        <div>
+          {drilldown.drilldownName}{' '}
+          {drilldown.error && (
+            <EuiToolTip id={`drilldownError-${drilldown.id}`} content={drilldown.error}>
+              <EuiIcon
+                type="alert"
+                color="danger"
+                title={drilldown.error}
+                aria-label={drilldown.error}
+                data-test-subj={`drilldownError-${drilldown.id}`}
+                style={{ marginLeft: '4px' }} /* a bit of spacing from text */
+              />
+            </EuiToolTip>
+          )}
+        </div>
+      ),
     },
     {
       name: 'Action',
diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts
index f01dd22c06bc5..a41ae851e185b 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts
@@ -5,6 +5,7 @@
  */
 
 import { ActionFactoryDefinition } from '../dynamic_actions';
+import { LicenseType } from '../../../licensing/public';
 
 /**
  * This is a convenience interface to register a drilldown. Drilldown has
@@ -28,6 +29,12 @@ export interface DrilldownDefinition<
    */
   id: string;
 
+  /**
+   * Minimal licence level
+   * Empty means no restrictions
+   */
+  minimalLicense?: LicenseType;
+
   /**
    * Determines the display order of the drilldowns in the flyout picker.
    * Higher numbers are displayed first.
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts
new file mode 100644
index 0000000000000..918c6422546f4
--- /dev/null
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts
@@ -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 { ActionFactory } from './action_factory';
+import { ActionFactoryDefinition } from './action_factory_definition';
+import { licensingMock } from '../../../licensing/public/mocks';
+
+const def: ActionFactoryDefinition = {
+  id: 'ACTION_FACTORY_1',
+  CollectConfig: {} as any,
+  createConfig: () => ({}),
+  isConfigValid: (() => true) as any,
+  create: ({ name }) => ({
+    id: '',
+    execute: async () => {},
+    getDisplayName: () => name,
+    enhancements: {},
+  }),
+};
+
+describe('License & ActionFactory', () => {
+  test('no license requirements', async () => {
+    const factory = new ActionFactory(def, () => licensingMock.createLicense());
+    expect(await factory.isCompatible({})).toBe(true);
+    expect(factory.isCompatibleLicence()).toBe(true);
+  });
+
+  test('not enough license level', async () => {
+    const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () =>
+      licensingMock.createLicense()
+    );
+    expect(await factory.isCompatible({})).toBe(true);
+    expect(factory.isCompatibleLicence()).toBe(false);
+  });
+
+  test('enough license level', async () => {
+    const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () =>
+      licensingMock.createLicense({ license: { type: 'gold' } })
+    );
+    expect(await factory.isCompatible({})).toBe(true);
+    expect(factory.isCompatibleLicence()).toBe(true);
+  });
+});
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts
index 262a5ef7d4561..95b7941b48ed3 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts
@@ -5,13 +5,12 @@
  */
 
 import { uiToReactComponent } from '../../../../../src/plugins/kibana_react/public';
-import {
-  UiActionsActionDefinition as ActionDefinition,
-  UiActionsPresentable as Presentable,
-} from '../../../../../src/plugins/ui_actions/public';
+import { UiActionsPresentable as Presentable } from '../../../../../src/plugins/ui_actions/public';
 import { ActionFactoryDefinition } from './action_factory_definition';
 import { Configurable } from '../../../../../src/plugins/kibana_utils/public';
 import { SerializedAction } from './types';
+import { ILicense } from '../../../licensing/public';
+import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public';
 
 export class ActionFactory<
   Config extends object = object,
@@ -19,10 +18,12 @@ export class ActionFactory<
   ActionContext extends object = object
 > implements Omit<Presentable<FactoryContext>, 'getHref'>, Configurable<Config, FactoryContext> {
   constructor(
-    protected readonly def: ActionFactoryDefinition<Config, FactoryContext, ActionContext>
+    protected readonly def: ActionFactoryDefinition<Config, FactoryContext, ActionContext>,
+    protected readonly getLicence: () => ILicense
   ) {}
 
   public readonly id = this.def.id;
+  public readonly minimalLicense = this.def.minimalLicense;
   public readonly order = this.def.order || 0;
   public readonly MenuItem? = this.def.MenuItem;
   public readonly ReactMenuItem? = this.MenuItem ? uiToReactComponent(this.MenuItem) : undefined;
@@ -51,9 +52,26 @@ export class ActionFactory<
     return await this.def.isCompatible(context);
   }
 
+  /**
+   * Does this action factory licence requirements
+   * compatible with current license?
+   */
+  public isCompatibleLicence() {
+    if (!this.minimalLicense) return true;
+    return this.getLicence().hasAtLeast(this.minimalLicense);
+  }
+
   public create(
     serializedAction: Omit<SerializedAction<Config>, 'factoryId'>
   ): ActionDefinition<ActionContext> {
-    return this.def.create(serializedAction);
+    const action = this.def.create(serializedAction);
+    return {
+      ...action,
+      isCompatible: async (context: ActionContext): Promise<boolean> => {
+        if (!this.isCompatibleLicence()) return false;
+        if (!action.isCompatible) return true;
+        return action.isCompatible(context);
+      },
+    };
   }
 }
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts
index d3751fe811665..d63f69ba5ab72 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts
@@ -4,12 +4,13 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { Configurable } from '../../../../../src/plugins/kibana_utils/public';
+import { SerializedAction } from './types';
+import { LicenseType } from '../../../licensing/public';
 import {
   UiActionsActionDefinition as ActionDefinition,
   UiActionsPresentable as Presentable,
 } from '../../../../../src/plugins/ui_actions/public';
-import { Configurable } from '../../../../../src/plugins/kibana_utils/public';
-import { SerializedAction } from './types';
 
 /**
  * This is a convenience interface for registering new action factories.
@@ -28,6 +29,12 @@ export interface ActionFactoryDefinition<
    */
   id: string;
 
+  /**
+   * Minimal licence level
+   * Empty means no licence restrictions
+   */
+  readonly minimalLicense?: LicenseType;
+
   /**
    * This method should return a definition of a new action, normally used to
    * register it in `ui_actions` registry.
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts
index 516b1f3cd2773..930f88ff08775 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts
@@ -7,11 +7,12 @@
 import { DynamicActionManager } from './dynamic_action_manager';
 import { ActionStorage, MemoryActionStorage } from './dynamic_action_storage';
 import { UiActionsService } from '../../../../../src/plugins/ui_actions/public';
-import { ActionInternal } from '../../../../../src/plugins/ui_actions/public/actions';
+import { ActionRegistry } from '../../../../../src/plugins/ui_actions/public/types';
 import { of } from '../../../../../src/plugins/kibana_utils';
 import { UiActionsServiceEnhancements } from '../services';
 import { ActionFactoryDefinition } from './action_factory_definition';
 import { SerializedAction, SerializedEvent } from './types';
+import { licensingMock } from '../../../licensing/public/mocks';
 
 const actionFactoryDefinition1: ActionFactoryDefinition = {
   id: 'ACTION_FACTORY_1',
@@ -67,14 +68,21 @@ const event3: SerializedEvent = {
   },
 };
 
-const setup = (events: readonly SerializedEvent[] = []) => {
+const setup = (
+  events: readonly SerializedEvent[] = [],
+  { getLicenseInfo = () => licensingMock.createLicense() } = {
+    getLicenseInfo: () => licensingMock.createLicense(),
+  }
+) => {
   const isCompatible = async () => true;
   const storage: ActionStorage = new MemoryActionStorage(events);
-  const actions = new Map<string, ActionInternal>();
+  const actions: ActionRegistry = new Map();
   const uiActions = new UiActionsService({
     actions,
   });
-  const uiActionsEnhancements = new UiActionsServiceEnhancements();
+  const uiActionsEnhancements = new UiActionsServiceEnhancements({
+    getLicenseInfo,
+  });
   const manager = new DynamicActionManager({
     isCompatible,
     storage,
@@ -95,6 +103,9 @@ const setup = (events: readonly SerializedEvent[] = []) => {
 };
 
 describe('DynamicActionManager', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
   test('can instantiate', () => {
     const { manager } = setup([event1]);
     expect(manager).toBeInstanceOf(DynamicActionManager);
@@ -103,11 +114,11 @@ describe('DynamicActionManager', () => {
   describe('.start()', () => {
     test('instantiates stored events', async () => {
       const { manager, actions, uiActions } = setup([event1]);
-      const create1 = jest.fn();
-      const create2 = jest.fn();
+      const create1 = jest.spyOn(actionFactoryDefinition1, 'create');
+      const create2 = jest.spyOn(actionFactoryDefinition2, 'create');
 
-      uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 });
-      uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 });
+      uiActions.registerActionFactory(actionFactoryDefinition1);
+      uiActions.registerActionFactory(actionFactoryDefinition2);
 
       expect(create1).toHaveBeenCalledTimes(0);
       expect(create2).toHaveBeenCalledTimes(0);
@@ -122,11 +133,11 @@ describe('DynamicActionManager', () => {
 
     test('does nothing when no events stored', async () => {
       const { manager, actions, uiActions } = setup();
-      const create1 = jest.fn();
-      const create2 = jest.fn();
+      const create1 = jest.spyOn(actionFactoryDefinition1, 'create');
+      const create2 = jest.spyOn(actionFactoryDefinition2, 'create');
 
-      uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 });
-      uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 });
+      uiActions.registerActionFactory(actionFactoryDefinition1);
+      uiActions.registerActionFactory(actionFactoryDefinition2);
 
       expect(create1).toHaveBeenCalledTimes(0);
       expect(create2).toHaveBeenCalledTimes(0);
@@ -207,11 +218,9 @@ describe('DynamicActionManager', () => {
   describe('.stop()', () => {
     test('removes events from UI actions registry', async () => {
       const { manager, actions, uiActions } = setup([event1, event2]);
-      const create1 = jest.fn();
-      const create2 = jest.fn();
 
-      uiActions.registerActionFactory({ ...actionFactoryDefinition1, create: create1 });
-      uiActions.registerActionFactory({ ...actionFactoryDefinition2, create: create2 });
+      uiActions.registerActionFactory(actionFactoryDefinition1);
+      uiActions.registerActionFactory(actionFactoryDefinition2);
 
       expect(actions.size).toBe(0);
 
@@ -632,4 +641,42 @@ describe('DynamicActionManager', () => {
       });
     });
   });
+
+  test('revived actions incompatible when license is not enough', async () => {
+    const getLicenseInfo = jest.fn(() =>
+      licensingMock.createLicense({ license: { type: 'basic' } })
+    );
+    const { manager, uiActions } = setup([event1, event3], { getLicenseInfo });
+    const basicActionFactory: ActionFactoryDefinition = {
+      ...actionFactoryDefinition1,
+      minimalLicense: 'basic',
+    };
+
+    const goldActionFactory: ActionFactoryDefinition = {
+      ...actionFactoryDefinition2,
+      minimalLicense: 'gold',
+    };
+
+    uiActions.registerActionFactory(basicActionFactory);
+    uiActions.registerActionFactory(goldActionFactory);
+
+    await manager.start();
+
+    const basicActions = await uiActions.getTriggerCompatibleActions(
+      'VALUE_CLICK_TRIGGER',
+      {} as any
+    );
+    expect(basicActions).toHaveLength(1);
+
+    getLicenseInfo.mockImplementation(() =>
+      licensingMock.createLicense({ license: { type: 'gold' } })
+    );
+
+    const basicAndGoldActions = await uiActions.getTriggerCompatibleActions(
+      'VALUE_CLICK_TRIGGER',
+      {} as any
+    );
+
+    expect(basicAndGoldActions).toHaveLength(2);
+  });
 });
diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts
index 58344026079e7..4afefe3006a43 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts
@@ -72,14 +72,18 @@ export class DynamicActionManager {
     const { uiActions, isCompatible } = this.params;
 
     const actionId = this.generateActionId(eventId);
+
     const factory = uiActions.getActionFactory(event.action.factoryId);
-    const actionDefinition: ActionDefinition = {
-      ...factory.create(action as SerializedAction<object>),
+    const actionDefinition: ActionDefinition = factory.create(action as SerializedAction<object>);
+    uiActions.registerAction({
+      ...actionDefinition,
       id: actionId,
-      isCompatible,
-    };
-
-    uiActions.registerAction(actionDefinition);
+      isCompatible: async (context) => {
+        if (!(await isCompatible(context))) return false;
+        if (!actionDefinition.isCompatible) return true;
+        return actionDefinition.isCompatible(context);
+      },
+    });
     for (const trigger of triggers) uiActions.attachAction(trigger as any, actionId);
   }
 
diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts
index 196b8f2c1d5c7..ff07d6e74a9c0 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts
@@ -10,6 +10,7 @@ import { uiActionsPluginMock } from '../../../../src/plugins/ui_actions/public/m
 import { embeddablePluginMock } from '../../../../src/plugins/embeddable/public/mocks';
 import { AdvancedUiActionsSetup, AdvancedUiActionsStart } from '.';
 import { plugin as pluginInitializer } from '.';
+import { licensingMock } from '../../licensing/public/mocks';
 
 export type Setup = jest.Mocked<AdvancedUiActionsSetup>;
 export type Start = jest.Mocked<AdvancedUiActionsStart>;
@@ -62,6 +63,7 @@ const createPlugin = (
       return plugin.start(anotherCoreStart, {
         uiActions: uiActionsStart,
         embeddable: embeddableStart,
+        licensing: licensingMock.createStart(),
       });
     },
   };
diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts
index 04caef92f15a2..a625ea2e2118b 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts
@@ -4,6 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { BehaviorSubject, Subscription } from 'rxjs';
 import {
   PluginInitializerContext,
   CoreSetup,
@@ -31,6 +32,7 @@ import {
 } from './custom_time_range_badge';
 import { CommonlyUsedRange } from './types';
 import { UiActionsServiceEnhancements } from './services';
+import { ILicense, LicensingPluginStart } from '../../licensing/public';
 import { createFlyoutManageDrilldowns } from './drilldowns';
 import { Storage } from '../../../../src/plugins/kibana_utils/public';
 
@@ -42,6 +44,7 @@ interface SetupDependencies {
 interface StartDependencies {
   embeddable: EmbeddableStart;
   uiActions: UiActionsStart;
+  licensing: LicensingPluginStart;
 }
 
 export interface SetupContract
@@ -63,7 +66,19 @@ declare module '../../../../src/plugins/ui_actions/public' {
 
 export class AdvancedUiActionsPublicPlugin
   implements Plugin<SetupContract, StartContract, SetupDependencies, StartDependencies> {
-  private readonly enhancements = new UiActionsServiceEnhancements();
+  readonly licenceInfo = new BehaviorSubject<ILicense | undefined>(undefined);
+  private getLicenseInfo(): ILicense {
+    if (!this.licenceInfo.getValue()) {
+      throw new Error(
+        'AdvancedUiActionsPublicPlugin: Licence is not ready! Licence becomes available only after setup.'
+      );
+    }
+    return this.licenceInfo.getValue()!;
+  }
+  private readonly enhancements = new UiActionsServiceEnhancements({
+    getLicenseInfo: () => this.getLicenseInfo(),
+  });
+  private subs: Subscription[] = [];
 
   constructor(initializerContext: PluginInitializerContext) {}
 
@@ -74,7 +89,9 @@ export class AdvancedUiActionsPublicPlugin
     };
   }
 
-  public start(core: CoreStart, { uiActions }: StartDependencies): StartContract {
+  public start(core: CoreStart, { uiActions, licensing }: StartDependencies): StartContract {
+    this.subs.push(licensing.license$.subscribe(this.licenceInfo));
+
     const dateFormat = core.uiSettings.get('dateFormat') as string;
     const commonlyUsedRanges = core.uiSettings.get(
       UI_SETTINGS.TIMEPICKER_QUICK_RANGES
@@ -106,5 +123,7 @@ export class AdvancedUiActionsPublicPlugin
     };
   }
 
-  public stop() {}
+  public stop() {
+    this.subs.forEach((s) => s.unsubscribe());
+  }
 }
diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts
index 3137e35a2fe47..4f2ddcf7e0491 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts
@@ -6,6 +6,9 @@
 
 import { UiActionsServiceEnhancements } from './ui_actions_service_enhancements';
 import { ActionFactoryDefinition, ActionFactory } from '../dynamic_actions';
+import { licensingMock } from '../../../licensing/public/mocks';
+
+const getLicenseInfo = () => licensingMock.createLicense();
 
 describe('UiActionsService', () => {
   describe('action factories', () => {
@@ -25,7 +28,7 @@ describe('UiActionsService', () => {
     };
 
     test('.getActionFactories() returns empty array if no action factories registered', () => {
-      const service = new UiActionsServiceEnhancements();
+      const service = new UiActionsServiceEnhancements({ getLicenseInfo });
 
       const factories = service.getActionFactories();
 
@@ -33,7 +36,7 @@ describe('UiActionsService', () => {
     });
 
     test('can register and retrieve an action factory', () => {
-      const service = new UiActionsServiceEnhancements();
+      const service = new UiActionsServiceEnhancements({ getLicenseInfo });
 
       service.registerActionFactory(factoryDefinition1);
 
@@ -44,7 +47,7 @@ describe('UiActionsService', () => {
     });
 
     test('can retrieve all action factories', () => {
-      const service = new UiActionsServiceEnhancements();
+      const service = new UiActionsServiceEnhancements({ getLicenseInfo });
 
       service.registerActionFactory(factoryDefinition1);
       service.registerActionFactory(factoryDefinition2);
@@ -58,7 +61,7 @@ describe('UiActionsService', () => {
     });
 
     test('throws when retrieving action factory that does not exist', () => {
-      const service = new UiActionsServiceEnhancements();
+      const service = new UiActionsServiceEnhancements({ getLicenseInfo });
 
       service.registerActionFactory(factoryDefinition1);
 
diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts
index b7bdced228584..bd05659d59e9d 100644
--- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts
+++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts
@@ -7,16 +7,20 @@
 import { ActionFactoryRegistry } from '../types';
 import { ActionFactory, ActionFactoryDefinition } from '../dynamic_actions';
 import { DrilldownDefinition } from '../drilldowns';
+import { ILicense } from '../../../licensing/common/types';
 
 export interface UiActionsServiceEnhancementsParams {
   readonly actionFactories?: ActionFactoryRegistry;
+  readonly getLicenseInfo: () => ILicense;
 }
 
 export class UiActionsServiceEnhancements {
   protected readonly actionFactories: ActionFactoryRegistry;
+  protected readonly getLicenseInfo: () => ILicense;
 
-  constructor({ actionFactories = new Map() }: UiActionsServiceEnhancementsParams = {}) {
+  constructor({ actionFactories = new Map(), getLicenseInfo }: UiActionsServiceEnhancementsParams) {
     this.actionFactories = actionFactories;
+    this.getLicenseInfo = getLicenseInfo;
   }
 
   /**
@@ -34,7 +38,10 @@ export class UiActionsServiceEnhancements {
       throw new Error(`ActionFactory [actionFactory.id = ${definition.id}] already registered.`);
     }
 
-    const actionFactory = new ActionFactory<Config, FactoryContext, ActionContext>(definition);
+    const actionFactory = new ActionFactory<Config, FactoryContext, ActionContext>(
+      definition,
+      this.getLicenseInfo
+    );
 
     this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory<any, any, any>);
   };
@@ -72,9 +79,11 @@ export class UiActionsServiceEnhancements {
     euiIcon,
     execute,
     getHref,
+    minimalLicense,
   }: DrilldownDefinition<Config, ExecutionContext>): void => {
     const actionFactory: ActionFactoryDefinition<Config, object, ExecutionContext> = {
       id: factoryId,
+      minimalLicense,
       order,
       CollectConfig,
       createConfig,

From 7440eea3dc4d0d614e39f4728def24f8bc5cd16d Mon Sep 17 00:00:00 2001
From: Marta Bondyra <marta.bondyra@elastic.co>
Date: Fri, 26 Jun 2020 18:43:35 +0200
Subject: [PATCH 04/21] [Lens] Use accordion menus in field list for available
 and empty fields (#68871)

---
 .../__mocks__/loader.ts                       |   1 -
 .../no_fields_callout.test.tsx.snap           |  49 ++
 .../indexpattern_datasource/_index.scss       |   1 -
 .../{_datapanel.scss => datapanel.scss}       |  14 +-
 .../datapanel.test.tsx                        | 203 ++++---
 .../indexpattern_datasource/datapanel.tsx     | 508 ++++++++++--------
 .../dimension_panel/dimension_panel.test.tsx  |   2 -
 .../dimension_panel/field_select.tsx          |  47 +-
 .../dimension_panel/popover_editor.tsx        |   1 -
 .../field_item.test.tsx                       |   6 +-
 .../indexpattern_datasource/field_item.tsx    |   8 +-
 .../fields_accordion.test.tsx                 |  97 ++++
 .../fields_accordion.tsx                      | 101 ++++
 .../indexpattern.test.ts                      |   5 -
 .../indexpattern_suggestions.test.tsx         |   8 -
 .../layerpanel.test.tsx                       |   1 -
 .../indexpattern_datasource/loader.test.ts    |   7 -
 .../public/indexpattern_datasource/loader.ts  |   2 -
 .../no_fields_callout.test.tsx                |  36 ++
 .../no_fields_callout.tsx                     |  75 +++
 .../definitions/date_histogram.test.tsx       |   1 -
 .../operations/definitions/terms.test.tsx     |   1 -
 .../operations/operations.test.ts             |   1 -
 .../state_helpers.test.ts                     |   8 -
 .../public/indexpattern_datasource/types.ts   |   1 -
 .../translations/translations/ja-JP.json      |   6 -
 .../translations/translations/zh-CN.json      |   6 -
 .../test/functional/page_objects/lens_page.ts |   9 -
 28 files changed, 784 insertions(+), 421 deletions(-)
 create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/no_fields_callout.test.tsx.snap
 rename x-pack/plugins/lens/public/indexpattern_datasource/{_datapanel.scss => datapanel.scss} (81%)
 create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx
 create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
 create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx
 create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx

diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts
index fe865edd62986..f2fedda1fa353 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/__mocks__/loader.ts
@@ -19,7 +19,6 @@ export function loadInitialState() {
       [restricted.id]: restricted,
     },
     layers: {},
-    showEmptyFields: false,
   };
   return result;
 }
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/no_fields_callout.test.tsx.snap b/x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/no_fields_callout.test.tsx.snap
new file mode 100644
index 0000000000000..607f968d86faa
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/__snapshots__/no_fields_callout.test.tsx.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NoFieldCallout renders properly for index with no fields 1`] = `
+<EuiCallOut
+  color="warning"
+  size="s"
+  title="No fields exist in this index pattern."
+/>
+`;
+
+exports[`NoFieldCallout renders properly when affected by field filter 1`] = `
+<EuiCallOut
+  color="warning"
+  size="s"
+  title="No fields match the selected filters."
+>
+  <strong>
+    Try:
+  </strong>
+  <ul>
+    <li>
+      Using different field filters
+    </li>
+  </ul>
+</EuiCallOut>
+`;
+
+exports[`NoFieldCallout renders properly when affected by field filters, global filter and timerange 1`] = `
+<EuiCallOut
+  color="warning"
+  size="s"
+  title="No fields match the selected filters."
+>
+  <strong>
+    Try:
+  </strong>
+  <ul>
+    <li>
+      Extending the time range
+    </li>
+    <li>
+      Using different field filters
+    </li>
+    <li>
+      Changing the global filters
+    </li>
+  </ul>
+</EuiCallOut>
+`;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss b/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss
index a0f3e53d7ac2c..a10dde4881691 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/_index.scss
@@ -1,2 +1 @@
-@import 'datapanel';
 @import 'field_item';
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
similarity index 81%
rename from x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss
rename to x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
index 77d4b41a0413c..3e767502fae3b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/_datapanel.scss
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss
@@ -16,10 +16,6 @@
   line-height: $euiSizeXXL;
 }
 
-.lnsInnerIndexPatternDataPanel__filterWrapper {
-  flex-grow: 0;
-}
-
 /**
  * 1. Don't cut off the shadow of the field items
  */
@@ -41,11 +37,9 @@
   right: $euiSizeXS; /* 1 */
 }
 
-.lnsInnerIndexPatternDataPanel__filterButton {
-  width: 100%;
-  color: $euiColorPrimary;
-  padding-left: $euiSizeS;
-  padding-right: $euiSizeS;
+.lnsInnerIndexPatternDataPanel__fieldItems {
+  // Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds
+  padding: $euiSizeXS $euiSizeXS 0;
 }
 
 .lnsInnerIndexPatternDataPanel__textField {
@@ -54,7 +48,9 @@
 }
 
 .lnsInnerIndexPatternDataPanel__filterType {
+  font-size: $euiFontSizeS;
   padding: $euiSizeS;
+  border-bottom: 1px solid $euiColorLightestShade;
 }
 
 .lnsInnerIndexPatternDataPanel__filterTypeInner {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
index 187ccb8c47563..7653dab2c9b84 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
@@ -9,19 +9,19 @@ import { createMockedDragDropContext } from './mocks';
 import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
 import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel';
 import { FieldItem } from './field_item';
+import { NoFieldsCallout } from './no_fields_callout';
 import { act } from 'react-dom/test-utils';
 import { coreMock } from 'src/core/public/mocks';
 import { IndexPatternPrivateState } from './types';
 import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
 import { ChangeIndexPattern } from './change_indexpattern';
-import { EuiProgress } from '@elastic/eui';
+import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui';
 import { documentField } from './document_field';
 
 const initialState: IndexPatternPrivateState = {
   indexPatternRefs: [],
   existingFields: {},
   currentIndexPatternId: '1',
-  showEmptyFields: false,
   layers: {
     first: {
       indexPatternId: '1',
@@ -229,8 +229,6 @@ describe('IndexPattern Data Panel', () => {
       },
       query: { query: '', language: 'lucene' },
       filters: [],
-      showEmptyFields: false,
-      onToggleEmptyFields: jest.fn(),
     };
   });
 
@@ -303,7 +301,6 @@ describe('IndexPattern Data Panel', () => {
         state: {
           indexPatternRefs: [],
           existingFields: {},
-          showEmptyFields: false,
           currentIndexPatternId: 'a',
           indexPatterns: {
             a: { id: 'a', title: 'aaa', timeFieldName: 'atime', fields: [] },
@@ -534,42 +531,97 @@ describe('IndexPattern Data Panel', () => {
     });
   });
 
-  describe('while showing empty fields', () => {
-    it('should list all supported fields in the pattern sorted alphabetically', async () => {
-      const wrapper = shallowWithIntl(
-        <InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
+  describe('displaying field list', () => {
+    let props: Parameters<typeof InnerIndexPatternDataPanel>[0];
+    beforeEach(() => {
+      props = {
+        ...defaultProps,
+        existingFields: {
+          idx1: {
+            bytes: true,
+            memory: true,
+          },
+        },
+      };
+    });
+    it('should list all supported fields in the pattern sorted alphabetically in groups', async () => {
+      const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
+      expect(wrapper.find(FieldItem).first().prop('field').name).toEqual('Records');
+      expect(
+        wrapper
+          .find('[data-test-subj="lnsIndexPatternAvailableFields"]')
+          .find(FieldItem)
+          .map((fieldItem) => fieldItem.prop('field').name)
+      ).toEqual(['bytes', 'memory']);
+      wrapper
+        .find('[data-test-subj="lnsIndexPatternEmptyFields"]')
+        .find('button')
+        .first()
+        .simulate('click');
+      expect(
+        wrapper
+          .find('[data-test-subj="lnsIndexPatternEmptyFields"]')
+          .find(FieldItem)
+          .map((fieldItem) => fieldItem.prop('field').name)
+      ).toEqual(['client', 'source', 'timestamp']);
+    });
+
+    it('should display NoFieldsCallout when all fields are empty', async () => {
+      const wrapper = mountWithIntl(
+        <InnerIndexPatternDataPanel {...defaultProps} existingFields={{ idx1: {} }} />
       );
+      expect(wrapper.find(NoFieldsCallout).length).toEqual(1);
+      expect(
+        wrapper
+          .find('[data-test-subj="lnsIndexPatternAvailableFields"]')
+          .find(FieldItem)
+          .map((fieldItem) => fieldItem.prop('field').name)
+      ).toEqual([]);
+      wrapper
+        .find('[data-test-subj="lnsIndexPatternEmptyFields"]')
+        .find('button')
+        .first()
+        .simulate('click');
+      expect(
+        wrapper
+          .find('[data-test-subj="lnsIndexPatternEmptyFields"]')
+          .find(FieldItem)
+          .map((fieldItem) => fieldItem.prop('field').name)
+      ).toEqual(['bytes', 'client', 'memory', 'source', 'timestamp']);
+    });
 
-      expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
-        'Records',
-        'bytes',
-        'client',
-        'memory',
-        'source',
-        'timestamp',
-      ]);
+    it('should display spinner for available fields accordion if existing fields are not loaded yet', async () => {
+      const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...defaultProps} />);
+      expect(
+        wrapper.find('[data-test-subj="lnsIndexPatternAvailableFields"]').find(EuiLoadingSpinner)
+          .length
+      ).toEqual(1);
+      wrapper.setProps({ existingFields: { idx1: {} } });
+      expect(wrapper.find(NoFieldsCallout).length).toEqual(1);
     });
 
     it('should filter down by name', () => {
-      const wrapper = shallowWithIntl(
-        <InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
-      );
-
+      const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
       act(() => {
         wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({
-          target: { value: 'mem' },
+          target: { value: 'me' },
         } as ChangeEvent<HTMLInputElement>);
       });
 
+      wrapper
+        .find('[data-test-subj="lnsIndexPatternEmptyFields"]')
+        .find('button')
+        .first()
+        .simulate('click');
+
       expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
         'memory',
+        'timestamp',
       ]);
     });
 
     it('should filter down by type', () => {
-      const wrapper = mountWithIntl(
-        <InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
-      );
+      const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
 
       wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
 
@@ -581,112 +633,55 @@ describe('IndexPattern Data Panel', () => {
       ]);
     });
 
-    it('should toggle type if clicked again', () => {
-      const wrapper = mountWithIntl(
-        <InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
-      );
+    it('should display no fields in groups when filtered by type Record', () => {
+      const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
 
       wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
 
-      wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
-      wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
+      wrapper.find('[data-test-subj="typeFilter-document"]').first().simulate('click');
 
       expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
         'Records',
-        'bytes',
-        'client',
-        'memory',
-        'source',
-        'timestamp',
       ]);
+      expect(wrapper.find(NoFieldsCallout).length).toEqual(2);
     });
 
-    it('should filter down by type and by name', () => {
-      const wrapper = mountWithIntl(
-        <InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
-      );
-
-      act(() => {
-        wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({
-          target: { value: 'mem' },
-        } as ChangeEvent<HTMLInputElement>);
-      });
-
+    it('should toggle type if clicked again', () => {
+      const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
       wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
 
       wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
-
-      expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
-        'memory',
-      ]);
-    });
-  });
-
-  describe('filtering out empty fields', () => {
-    let emptyFieldsTestProps: typeof defaultProps;
-
-    beforeEach(() => {
-      emptyFieldsTestProps = {
-        ...defaultProps,
-        indexPatterns: {
-          ...defaultProps.indexPatterns,
-          '1': {
-            ...defaultProps.indexPatterns['1'],
-            fields: defaultProps.indexPatterns['1'].fields.map((field) => ({
-              ...field,
-              exists: field.type === 'number',
-            })),
-          },
-        },
-        onToggleEmptyFields: jest.fn(),
-      };
-    });
-
-    it('should list all supported fields in the pattern sorted alphabetically', async () => {
-      const props = {
-        ...emptyFieldsTestProps,
-        existingFields: {
-          idx1: {
-            bytes: true,
-            memory: true,
-          },
-        },
-      };
-      const wrapper = shallowWithIntl(<InnerIndexPatternDataPanel {...props} />);
-
+      wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
+      wrapper
+        .find('[data-test-subj="lnsIndexPatternEmptyFields"]')
+        .find('button')
+        .first()
+        .simulate('click');
       expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
         'Records',
         'bytes',
         'memory',
+        'client',
+        'source',
+        'timestamp',
       ]);
     });
 
-    it('should filter down by name', () => {
-      const wrapper = shallowWithIntl(
-        <InnerIndexPatternDataPanel {...emptyFieldsTestProps} showEmptyFields={true} />
-      );
-
+    it('should filter down by type and by name', () => {
+      const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
       act(() => {
         wrapper.find('[data-test-subj="lnsIndexPatternFieldSearch"]').prop('onChange')!({
-          target: { value: 'mem' },
+          target: { value: 'me' },
         } as ChangeEvent<HTMLInputElement>);
       });
 
-      expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
-        'memory',
-      ]);
-    });
-
-    it('should allow removing the filter for data', () => {
-      const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...emptyFieldsTestProps} />);
-
       wrapper.find('[data-test-subj="lnsIndexPatternFiltersToggle"]').first().simulate('click');
 
-      wrapper.find('[data-test-subj="lnsEmptyFilter"]').first().prop('onChange')!(
-        {} as ChangeEvent
-      );
+      wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
 
-      expect(emptyFieldsTestProps.onToggleEmptyFields).toHaveBeenCalled();
+      expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([
+        'memory',
+      ]);
     });
   });
 });
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
index ae5632ddae84e..b72f87e243dcd 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
@@ -4,26 +4,21 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { uniq, indexBy } from 'lodash';
-import React, { useState, useEffect, memo, useCallback } from 'react';
+import './datapanel.scss';
+import { uniq, indexBy, groupBy, throttle } from 'lodash';
+import React, { useState, useEffect, memo, useCallback, useMemo } from 'react';
 import {
-  // @ts-ignore
-  EuiHighlight,
   EuiFlexGroup,
   EuiFlexItem,
   EuiContextMenuPanel,
   EuiContextMenuItem,
   EuiContextMenuPanelProps,
   EuiPopover,
-  EuiPopoverTitle,
-  EuiPopoverFooter,
   EuiCallOut,
   EuiFormControlLayout,
-  EuiSwitch,
-  EuiFacetButton,
-  EuiIcon,
   EuiSpacer,
-  EuiFormLabel,
+  EuiFilterGroup,
+  EuiFilterButton,
 } from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
 import { FormattedMessage } from '@kbn/i18n/react';
@@ -31,6 +26,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public';
 import { DatasourceDataPanelProps, DataType, StateSetter } from '../types';
 import { ChildDragDropProvider, DragContextState } from '../drag_drop';
 import { FieldItem } from './field_item';
+import { NoFieldsCallout } from './no_fields_callout';
 import {
   IndexPattern,
   IndexPatternPrivateState,
@@ -41,6 +37,7 @@ import { trackUiEvent } from '../lens_ui_telemetry';
 import { syncExistingFields } from './loader';
 import { fieldExists } from './pure_helpers';
 import { Loader } from '../loader';
+import { FieldsAccordion } from './fields_accordion';
 import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public';
 
 export type Props = DatasourceDataPanelProps<IndexPatternPrivateState> & {
@@ -87,21 +84,9 @@ export function IndexPatternDataPanel({
   changeIndexPattern,
 }: Props) {
   const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state;
-
   const onChangeIndexPattern = useCallback(
     (id: string) => changeIndexPattern(id, state, setState),
-    [state, setState]
-  );
-
-  const onToggleEmptyFields = useCallback(
-    (showEmptyFields?: boolean) => {
-      setState((prevState) => ({
-        ...prevState,
-        showEmptyFields:
-          showEmptyFields === undefined ? !prevState.showEmptyFields : showEmptyFields,
-      }));
-    },
-    [setState]
+    [state, setState, changeIndexPattern]
   );
 
   const indexPatternList = uniq(
@@ -179,8 +164,6 @@ export function IndexPatternDataPanel({
           dateRange={dateRange}
           filters={filters}
           dragDropContext={dragDropContext}
-          showEmptyFields={state.showEmptyFields}
-          onToggleEmptyFields={onToggleEmptyFields}
           core={core}
           data={data}
           onChangeIndexPattern={onChangeIndexPattern}
@@ -195,8 +178,26 @@ interface DataPanelState {
   nameFilter: string;
   typeFilter: DataType[];
   isTypeFilterOpen: boolean;
+  isAvailableAccordionOpen: boolean;
+  isEmptyAccordionOpen: boolean;
+}
+
+export interface FieldsGroup {
+  specialFields: IndexPatternField[];
+  availableFields: IndexPatternField[];
+  emptyFields: IndexPatternField[];
 }
 
+const defaultFieldGroups = {
+  specialFields: [],
+  availableFields: [],
+  emptyFields: [],
+};
+
+const fieldFiltersLabel = i18n.translate('xpack.lens.indexPatterns.fieldFiltersLabel', {
+  defaultMessage: 'Field filters',
+});
+
 export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
   currentIndexPatternId,
   indexPatternRefs,
@@ -206,8 +207,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
   filters,
   dragDropContext,
   onChangeIndexPattern,
-  showEmptyFields,
-  onToggleEmptyFields,
   core,
   data,
   existingFields,
@@ -217,8 +216,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
   indexPatternRefs: IndexPatternRef[];
   indexPatterns: Record<string, IndexPattern>;
   dragDropContext: DragContextState;
-  showEmptyFields: boolean;
-  onToggleEmptyFields: (showEmptyFields?: boolean) => void;
   onChangeIndexPattern: (newId: string) => void;
   existingFields: IndexPatternPrivateState['existingFields'];
 }) {
@@ -226,79 +223,158 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
     nameFilter: '',
     typeFilter: [],
     isTypeFilterOpen: false,
+    isAvailableAccordionOpen: true,
+    isEmptyAccordionOpen: false,
   });
   const [pageSize, setPageSize] = useState(PAGINATION_SIZE);
   const [scrollContainer, setScrollContainer] = useState<Element | undefined>(undefined);
   const currentIndexPattern = indexPatterns[currentIndexPatternId];
   const allFields = currentIndexPattern.fields;
-  const fieldByName = indexBy(allFields, 'name');
   const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] }));
-
-  const lazyScroll = () => {
-    if (scrollContainer) {
-      const nearBottom =
-        scrollContainer.scrollTop + scrollContainer.clientHeight >
-        scrollContainer.scrollHeight * 0.9;
-      if (nearBottom) {
-        setPageSize(Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, allFields.length)));
-      }
-    }
-  };
+  const hasSyncedExistingFields = existingFields[currentIndexPattern.title];
+  const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter(
+    (type) => type in fieldTypeNames
+  );
 
   useEffect(() => {
     // Reset the scroll if we have made material changes to the field list
     if (scrollContainer) {
       scrollContainer.scrollTop = 0;
       setPageSize(PAGINATION_SIZE);
-      lazyScroll();
     }
-  }, [localState.nameFilter, localState.typeFilter, currentIndexPatternId, showEmptyFields]);
+  }, [localState.nameFilter, localState.typeFilter, currentIndexPatternId, scrollContainer]);
 
-  const availableFieldTypes = uniq(allFields.map(({ type }) => type)).filter(
-    (type) => type in fieldTypeNames
-  );
+  const fieldGroups: FieldsGroup = useMemo(() => {
+    const containsData = (field: IndexPatternField) => {
+      const fieldByName = indexBy(allFields, 'name');
+      const overallField = fieldByName[field.name];
 
-  const displayedFields = allFields.filter((field) => {
-    if (!supportedFieldTypes.has(field.type)) {
-      return false;
-    }
+      return (
+        overallField && fieldExists(existingFields, currentIndexPattern.title, overallField.name)
+      );
+    };
 
-    if (
-      localState.nameFilter.length &&
-      !field.name.toLowerCase().includes(localState.nameFilter.toLowerCase())
-    ) {
-      return false;
+    const allSupportedTypesFields = allFields.filter((field) =>
+      supportedFieldTypes.has(field.type)
+    );
+    const sorted = allSupportedTypesFields.sort(sortFields);
+    // optimization before existingFields are synced
+    if (!hasSyncedExistingFields) {
+      return {
+        ...defaultFieldGroups,
+        ...groupBy(sorted, (field) => {
+          if (field.type === 'document') {
+            return 'specialFields';
+          } else {
+            return 'emptyFields';
+          }
+        }),
+      };
     }
+    return {
+      ...defaultFieldGroups,
+      ...groupBy(sorted, (field) => {
+        if (field.type === 'document') {
+          return 'specialFields';
+        } else if (containsData(field)) {
+          return 'availableFields';
+        } else return 'emptyFields';
+      }),
+    };
+  }, [allFields, existingFields, currentIndexPattern, hasSyncedExistingFields]);
 
-    if (!showEmptyFields) {
-      const indexField = currentIndexPattern && fieldByName[field.name];
-      const exists =
-        field.type === 'document' ||
-        (indexField && fieldExists(existingFields, currentIndexPattern.title, indexField.name));
-      if (localState.typeFilter.length > 0) {
-        return exists && localState.typeFilter.includes(field.type as DataType);
-      }
+  const filteredFieldGroups: FieldsGroup = useMemo(() => {
+    const filterFieldGroup = (fieldGroup: IndexPatternField[]) =>
+      fieldGroup.filter((field) => {
+        if (
+          localState.nameFilter.length &&
+          !field.name.toLowerCase().includes(localState.nameFilter.toLowerCase())
+        ) {
+          return false;
+        }
 
-      return exists;
-    }
+        if (localState.typeFilter.length > 0) {
+          return localState.typeFilter.includes(field.type as DataType);
+        }
+        return true;
+      });
 
-    if (localState.typeFilter.length > 0) {
-      return localState.typeFilter.includes(field.type as DataType);
+    return Object.entries(fieldGroups).reduce((acc, [name, fields]) => {
+      return {
+        ...acc,
+        [name]: filterFieldGroup(fields),
+      };
+    }, defaultFieldGroups);
+  }, [fieldGroups, localState.nameFilter, localState.typeFilter]);
+
+  const lazyScroll = useCallback(() => {
+    if (scrollContainer) {
+      const nearBottom =
+        scrollContainer.scrollTop + scrollContainer.clientHeight >
+        scrollContainer.scrollHeight * 0.9;
+      if (nearBottom) {
+        const displayedFieldsLength =
+          (localState.isAvailableAccordionOpen ? filteredFieldGroups.availableFields.length : 0) +
+          (localState.isEmptyAccordionOpen ? filteredFieldGroups.emptyFields.length : 0);
+        setPageSize(
+          Math.max(
+            PAGINATION_SIZE,
+            Math.min(pageSize + PAGINATION_SIZE * 0.5, displayedFieldsLength)
+          )
+        );
+      }
     }
+  }, [
+    scrollContainer,
+    localState.isAvailableAccordionOpen,
+    localState.isEmptyAccordionOpen,
+    filteredFieldGroups,
+    pageSize,
+    setPageSize,
+  ]);
 
-    return true;
-  });
+  const [paginatedAvailableFields, paginatedEmptyFields]: [
+    IndexPatternField[],
+    IndexPatternField[]
+  ] = useMemo(() => {
+    const { availableFields, emptyFields } = filteredFieldGroups;
+    const isAvailableAccordionOpen = localState.isAvailableAccordionOpen;
+    const isEmptyAccordionOpen = localState.isEmptyAccordionOpen;
+
+    if (isAvailableAccordionOpen && isEmptyAccordionOpen) {
+      if (availableFields.length > pageSize) {
+        return [availableFields.slice(0, pageSize), []];
+      } else {
+        return [availableFields, emptyFields.slice(0, pageSize - availableFields.length)];
+      }
+    }
+    if (isAvailableAccordionOpen && !isEmptyAccordionOpen) {
+      return [availableFields.slice(0, pageSize), []];
+    }
 
-  const specialFields = displayedFields.filter((f) => f.type === 'document');
-  const paginatedFields = displayedFields
-    .filter((f) => f.type !== 'document')
-    .sort(sortFields)
-    .slice(0, pageSize);
-  const hilight = localState.nameFilter.toLowerCase();
+    if (!isAvailableAccordionOpen && isEmptyAccordionOpen) {
+      return [[], emptyFields.slice(0, pageSize)];
+    }
+    return [[], []];
+  }, [
+    localState.isAvailableAccordionOpen,
+    localState.isEmptyAccordionOpen,
+    filteredFieldGroups,
+    pageSize,
+  ]);
 
-  const filterByTypeLabel = i18n.translate('xpack.lens.indexPatterns.filterByTypeLabel', {
-    defaultMessage: 'Filter by type',
-  });
+  const fieldProps = useMemo(
+    () => ({
+      core,
+      data,
+      indexPattern: currentIndexPattern,
+      highlight: localState.nameFilter.toLowerCase(),
+      dateRange,
+      query,
+      filters,
+    }),
+    [core, data, currentIndexPattern, dateRange, query, filters, localState.nameFilter]
+  );
 
   return (
     <ChildDragDropProvider {...dragDropContext}>
@@ -308,7 +384,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
         direction="column"
         responsive={false}
       >
-        <EuiFlexItem grow={null}>
+        <EuiFlexItem grow={false}>
           <div className="lnsInnerIndexPatternDataPanel__header">
             <ChangeIndexPattern
               data-test-subj="indexPattern-switcher"
@@ -327,58 +403,59 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
             />
           </div>
         </EuiFlexItem>
-        <EuiFlexItem>
-          <div className="lnsInnerIndexPatternDataPanel__filterWrapper">
-            <EuiFormControlLayout
-              icon="search"
-              fullWidth
-              clear={{
-                title: i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
-                  defaultMessage: 'Clear name and type filters',
-                }),
-                'aria-label': i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
-                  defaultMessage: 'Clear name and type filters',
-                }),
-                onClick: () => {
-                  trackUiEvent('indexpattern_filters_cleared');
-                  clearLocalState();
-                },
+        <EuiFlexItem grow={false}>
+          <EuiFormControlLayout
+            icon="search"
+            fullWidth
+            clear={{
+              title: i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
+                defaultMessage: 'Clear name and type filters',
+              }),
+              'aria-label': i18n.translate('xpack.lens.indexPatterns.clearFiltersLabel', {
+                defaultMessage: 'Clear name and type filters',
+              }),
+              onClick: () => {
+                trackUiEvent('indexpattern_filters_cleared');
+                clearLocalState();
+              },
+            }}
+          >
+            <input
+              className="euiFieldText euiFieldText--fullWidth lnsInnerIndexPatternDataPanel__textField"
+              data-test-subj="lnsIndexPatternFieldSearch"
+              placeholder={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', {
+                defaultMessage: 'Search field names',
+                description: 'Search the list of fields in the index pattern for the provided text',
+              })}
+              value={localState.nameFilter}
+              onChange={(e) => {
+                setLocalState({ ...localState, nameFilter: e.target.value });
               }}
-            >
-              <input
-                className="euiFieldText euiFieldText--fullWidth lnsInnerIndexPatternDataPanel__textField"
-                data-test-subj="lnsIndexPatternFieldSearch"
-                placeholder={i18n.translate('xpack.lens.indexPatterns.filterByNameLabel', {
-                  defaultMessage: 'Search field names',
-                  description:
-                    'Search the list of fields in the index pattern for the provided text',
-                })}
-                value={localState.nameFilter}
-                onChange={(e) => {
-                  setLocalState({ ...localState, nameFilter: e.target.value });
-                }}
-                aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', {
-                  defaultMessage: 'Search fields',
-                })}
-              />
-            </EuiFormControlLayout>
-          </div>
-          <div className="lnsInnerIndexPatternDataPanel__filtersWrapper">
+              aria-label={i18n.translate('xpack.lens.indexPatterns.filterByNameAriaLabel', {
+                defaultMessage: 'Search fields',
+              })}
+            />
+          </EuiFormControlLayout>
+
+          <EuiSpacer size="xs" />
+
+          <EuiFilterGroup>
             <EuiPopover
               id="dataPanelTypeFilter"
               panelClassName="euiFilterGroup__popoverPanel"
               panelPaddingSize="none"
-              anchorPosition="rightDown"
+              anchorPosition="rightUp"
               display="block"
               isOpen={localState.isTypeFilterOpen}
               closePopover={() => setLocalState(() => ({ ...localState, isTypeFilterOpen: false }))}
               button={
-                <EuiFacetButton
+                <EuiFilterButton
+                  iconType="arrowDown"
+                  isSelected={localState.isTypeFilterOpen}
+                  numFilters={localState.typeFilter.length}
+                  hasActiveFilters={!!localState.typeFilter.length}
+                  numActiveFilters={localState.typeFilter.length}
                   data-test-subj="lnsIndexPatternFiltersToggle"
-                  className="lnsInnerIndexPatternDataPanel__filterButton"
-                  quantity={localState.typeFilter.length}
-                  icon={<EuiIcon type="filter" />}
-                  isSelected={localState.typeFilter.length ? true : false}
                   onClick={() => {
                     setLocalState((s) => ({
                       ...s,
@@ -386,11 +463,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
                     }));
                   }}
                 >
-                  {filterByTypeLabel}
-                </EuiFacetButton>
+                  {fieldFiltersLabel}
+                </EuiFilterButton>
               }
             >
-              <EuiPopoverTitle>{filterByTypeLabel}</EuiPopoverTitle>
               <FixedEuiContextMenuPanel
                 watchedItemProps={['icon', 'disabled']}
                 data-test-subj="lnsIndexPatternTypeFilterOptions"
@@ -416,22 +492,10 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
                   </EuiContextMenuItem>
                 ))}
               />
-              <EuiPopoverFooter>
-                <EuiSwitch
-                  compressed
-                  checked={!showEmptyFields}
-                  onChange={() => {
-                    trackUiEvent('indexpattern_existence_toggled');
-                    onToggleEmptyFields();
-                  }}
-                  label={i18n.translate('xpack.lens.indexPatterns.toggleEmptyFieldsSwitch', {
-                    defaultMessage: 'Only show fields with data',
-                  })}
-                  data-test-subj="lnsEmptyFilter"
-                />
-              </EuiPopoverFooter>
             </EuiPopover>
-          </div>
+          </EuiFilterGroup>
+        </EuiFlexItem>
+        <EuiFlexItem>
           <div
             className="lnsInnerIndexPatternDataPanel__listWrapper"
             ref={(el) => {
@@ -440,101 +504,95 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
                 setScrollContainer(el);
               }
             }}
-            onScroll={lazyScroll}
+            onScroll={throttle(lazyScroll, 100)}
           >
             <div className="lnsInnerIndexPatternDataPanel__list">
-              {specialFields.map((field) => (
+              {filteredFieldGroups.specialFields.map((field: IndexPatternField) => (
                 <FieldItem
-                  core={core}
-                  data={data}
-                  key={field.name}
-                  indexPattern={currentIndexPattern}
+                  {...fieldProps}
+                  exists={!!fieldGroups.availableFields.length}
                   field={field}
-                  highlight={hilight}
-                  exists={paginatedFields.length > 0}
-                  dateRange={dateRange}
-                  query={query}
-                  filters={filters}
                   hideDetails={true}
+                  key={field.name}
                 />
               ))}
-              {specialFields.length > 0 && (
-                <>
-                  <EuiSpacer size="s" />
-                  <EuiFormLabel>
-                    {i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', {
-                      defaultMessage: 'Individual fields',
-                    })}
-                  </EuiFormLabel>
-                  <EuiSpacer size="s" />
-                </>
-              )}
-              {paginatedFields.map((field) => {
-                const overallField = fieldByName[field.name];
-                return (
-                  <FieldItem
-                    core={core}
-                    data={data}
-                    indexPattern={currentIndexPattern}
-                    key={field.name}
-                    field={field}
-                    highlight={hilight}
-                    exists={
-                      overallField &&
-                      fieldExists(existingFields, currentIndexPattern.title, overallField.name)
+
+              <EuiSpacer size="s" />
+              <FieldsAccordion
+                initialIsOpen={localState.isAvailableAccordionOpen}
+                id="lnsIndexPatternAvailableFields"
+                label={i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', {
+                  defaultMessage: 'Available fields',
+                })}
+                exists={true}
+                hasLoaded={!!hasSyncedExistingFields}
+                fieldsCount={filteredFieldGroups.availableFields.length}
+                isFiltered={
+                  filteredFieldGroups.availableFields.length !== fieldGroups.availableFields.length
+                }
+                paginatedFields={paginatedAvailableFields}
+                fieldProps={fieldProps}
+                onToggle={(open) => {
+                  setLocalState((s) => ({
+                    ...s,
+                    isAvailableAccordionOpen: open,
+                  }));
+                  const displayedFieldLength =
+                    (open ? filteredFieldGroups.availableFields.length : 0) +
+                    (localState.isEmptyAccordionOpen ? filteredFieldGroups.emptyFields.length : 0);
+                  setPageSize(
+                    Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength))
+                  );
+                }}
+                renderCallout={
+                  <NoFieldsCallout
+                    isAffectedByGlobalFilter={!!filters.length}
+                    isAffectedByFieldFilter={
+                      !!(localState.typeFilter.length || localState.nameFilter.length)
                     }
-                    dateRange={dateRange}
-                    query={query}
-                    filters={filters}
+                    isAffectedByTimerange={true}
+                    existFieldsInIndex={!!fieldGroups.emptyFields.length}
                   />
-                );
-              })}
-
-              {paginatedFields.length === 0 && (
-                <EuiCallOut
-                  size="s"
-                  color="warning"
-                  title={
-                    localState.typeFilter.length || localState.nameFilter.length
-                      ? i18n.translate('xpack.lens.indexPatterns.noFilteredFieldsLabel', {
-                          defaultMessage: 'No fields match the current filters.',
-                        })
-                      : showEmptyFields
-                      ? i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', {
-                          defaultMessage: 'No fields exist in this index pattern.',
-                        })
-                      : i18n.translate('xpack.lens.indexPatterns.emptyFieldsWithDataLabel', {
-                          defaultMessage: 'Looks like you don’t have any data.',
-                        })
-                  }
-                >
-                  {(!showEmptyFields ||
-                    localState.typeFilter.length ||
-                    localState.nameFilter.length) && (
-                    <>
-                      <strong>
-                        {i18n.translate('xpack.lens.indexPatterns.noFields.tryText', {
-                          defaultMessage: 'Try:',
-                        })}
-                      </strong>
-                      <ul>
-                        <li>
-                          {i18n.translate('xpack.lens.indexPatterns.noFields.extendTimeBullet', {
-                            defaultMessage: 'Extending the time range',
-                          })}
-                        </li>
-                        <li>
-                          {i18n.translate('xpack.lens.indexPatterns.noFields.fieldFilterBullet', {
-                            defaultMessage:
-                              'Using {filterByTypeLabel} {arrow} to show fields without data',
-                            values: { filterByTypeLabel, arrow: '↑' },
-                          })}
-                        </li>
-                      </ul>
-                    </>
-                  )}
-                </EuiCallOut>
-              )}
+                }
+              />
+              <EuiSpacer size="m" />
+              <FieldsAccordion
+                initialIsOpen={localState.isEmptyAccordionOpen}
+                isFiltered={
+                  filteredFieldGroups.emptyFields.length !== fieldGroups.emptyFields.length
+                }
+                fieldsCount={filteredFieldGroups.emptyFields.length}
+                paginatedFields={paginatedEmptyFields}
+                hasLoaded={!!hasSyncedExistingFields}
+                exists={false}
+                fieldProps={fieldProps}
+                id="lnsIndexPatternEmptyFields"
+                label={i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', {
+                  defaultMessage: 'Empty fields',
+                })}
+                onToggle={(open) => {
+                  setLocalState((s) => ({
+                    ...s,
+                    isEmptyAccordionOpen: open,
+                  }));
+                  const displayedFieldLength =
+                    (localState.isAvailableAccordionOpen
+                      ? filteredFieldGroups.availableFields.length
+                      : 0) + (open ? filteredFieldGroups.emptyFields.length : 0);
+                  setPageSize(
+                    Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength))
+                  );
+                }}
+                renderCallout={
+                  <NoFieldsCallout
+                    isAffectedByFieldFilter={
+                      !!(localState.typeFilter.length || localState.nameFilter.length)
+                    }
+                    existFieldsInIndex={!!fieldGroups.emptyFields.length}
+                  />
+                }
+              />
+              <EuiSpacer size="m" />
             </div>
           </div>
         </EuiFlexItem>
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
index ebf5abd4fbfe9..ee9b6778650ef 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx
@@ -79,7 +79,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
       indexPatternRefs: [],
       indexPatterns: expectedIndexPatterns,
       currentIndexPatternId: '1',
-      showEmptyFields: false,
       existingFields: {
         'my-fake-index-pattern': {
           timestamp: true,
@@ -1258,7 +1257,6 @@ describe('IndexPatternDimensionEditorPanel', () => {
           },
         },
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           myLayer: {
             indexPatternId: 'foo',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx
index ee566951d2b76..35c510521b35b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx
@@ -27,7 +27,6 @@ export interface FieldChoice {
 
 export interface FieldSelectProps {
   currentIndexPattern: IndexPattern;
-  showEmptyFields: boolean;
   fieldMap: Record<string, IndexPatternField>;
   incompatibleSelectedOperationType: OperationType | null;
   selectedColumnOperationType?: OperationType;
@@ -40,7 +39,6 @@ export interface FieldSelectProps {
 
 export function FieldSelect({
   currentIndexPattern,
-  showEmptyFields,
   fieldMap,
   incompatibleSelectedOperationType,
   selectedColumnOperationType,
@@ -69,6 +67,10 @@ export function FieldSelect({
       (field) => fieldMap[field].type === 'document'
     );
 
+    const containsData = (field: string) =>
+      fieldMap[field].type === 'document' ||
+      fieldExists(existingFields, currentIndexPattern.title, field);
+
     function fieldNamesToOptions(items: string[]) {
       return items
         .map((field) => ({
@@ -82,12 +84,9 @@ export function FieldSelect({
                 ? selectedColumnOperationType
                 : undefined,
           },
-          exists:
-            fieldMap[field].type === 'document' ||
-            fieldExists(existingFields, currentIndexPattern.title, field),
+          exists: containsData(field),
           compatible: isCompatibleWithCurrentOperation(field),
         }))
-        .filter((field) => showEmptyFields || field.exists)
         .sort((a, b) => {
           if (a.compatible && !b.compatible) {
             return -1;
@@ -108,18 +107,33 @@ export function FieldSelect({
         }));
     }
 
-    const fieldOptions: unknown[] = fieldNamesToOptions(specialFields);
+    const [availableFields, emptyFields] = _.partition(normalFields, containsData);
 
-    if (fields.length > 0) {
-      fieldOptions.push({
-        label: i18n.translate('xpack.lens.indexPattern.individualFieldsLabel', {
-          defaultMessage: 'Individual fields',
-        }),
-        options: fieldNamesToOptions(normalFields),
-      });
-    }
+    const constructFieldsOptions = (fieldsArr: string[], label: string) =>
+      fieldsArr.length > 0 && {
+        label,
+        options: fieldNamesToOptions(fieldsArr),
+      };
+
+    const availableFieldsOptions = constructFieldsOptions(
+      availableFields,
+      i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', {
+        defaultMessage: 'Available fields',
+      })
+    );
+
+    const emptyFieldsOptions = constructFieldsOptions(
+      emptyFields,
+      i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', {
+        defaultMessage: 'Empty fields',
+      })
+    );
 
-    return fieldOptions;
+    return [
+      ...fieldNamesToOptions(specialFields),
+      availableFieldsOptions,
+      emptyFieldsOptions,
+    ].filter(Boolean);
   }, [
     incompatibleSelectedOperationType,
     selectedColumnOperationType,
@@ -127,7 +141,6 @@ export function FieldSelect({
     operationFieldSupportMatrix,
     currentIndexPattern,
     fieldMap,
-    showEmptyFields,
   ]);
 
   return (
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx
index 4468686aa41ea..eb2475756417e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx
@@ -200,7 +200,6 @@ export function PopoverEditor(props: PopoverEditorProps) {
           <FieldSelect
             currentIndexPattern={currentIndexPattern}
             existingFields={state.existingFields}
-            showEmptyFields={state.showEmptyFields}
             fieldMap={fieldMap}
             operationFieldSupportMatrix={operationFieldSupportMatrix}
             selectedColumnOperationType={selectedColumn && selectedColumn.operationType}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
index 511ba3c0442c7..e8dfbc250c539 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
@@ -7,7 +7,7 @@
 import React from 'react';
 import { act } from 'react-dom/test-utils';
 import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui';
-import { FieldItem, FieldItemProps } from './field_item';
+import { InnerFieldItem, FieldItemProps } from './field_item';
 import { coreMock } from 'src/core/public/mocks';
 import { mountWithIntl } from 'test_utils/enzyme_helpers';
 import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
@@ -94,7 +94,7 @@ describe('IndexPattern Field Item', () => {
     core.http.post.mockImplementationOnce(() => {
       return Promise.resolve({});
     });
-    const wrapper = mountWithIntl(<FieldItem {...defaultProps} />);
+    const wrapper = mountWithIntl(<InnerFieldItem {...defaultProps} />);
 
     await act(async () => {
       wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click');
@@ -119,7 +119,7 @@ describe('IndexPattern Field Item', () => {
       });
     });
 
-    const wrapper = mountWithIntl(<FieldItem {...defaultProps} />);
+    const wrapper = mountWithIntl(<InnerFieldItem {...defaultProps} />);
 
     wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click');
 
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index 6c00706cc8609..1a1a34d30f8a8 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -49,6 +49,8 @@ import { IndexPattern, IndexPatternField } from './types';
 import { LensFieldIcon } from './lens_field_icon';
 import { trackUiEvent } from '../lens_ui_telemetry';
 
+import { debouncedComponent } from '../debounced_component';
+
 export interface FieldItemProps {
   core: DatasourceDataPanelProps['core'];
   data: DataPublicPluginStart;
@@ -78,7 +80,7 @@ function wrapOnDot(str?: string) {
   return str ? str.replace(/\./g, '.\u200B') : '';
 }
 
-export const FieldItem = React.memo(function FieldItem(props: FieldItemProps) {
+export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
   const {
     core,
     field,
@@ -239,7 +241,9 @@ export const FieldItem = React.memo(function FieldItem(props: FieldItemProps) {
       <FieldItemPopoverContents {...state} {...props} />
     </EuiPopover>
   );
-});
+};
+
+export const FieldItem = debouncedComponent(InnerFieldItem);
 
 function FieldItemPopoverContents(props: State & FieldItemProps) {
   const {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx
new file mode 100644
index 0000000000000..41d90a4f8870f
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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 { EuiLoadingSpinner, EuiNotificationBadge } from '@elastic/eui';
+import { coreMock } from 'src/core/public/mocks';
+import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
+import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
+import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
+import { IndexPattern } from './types';
+import { FieldItem } from './field_item';
+import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion';
+
+describe('Fields Accordion', () => {
+  let defaultProps: FieldsAccordionProps;
+  let indexPattern: IndexPattern;
+  let core: ReturnType<typeof coreMock['createSetup']>;
+  let data: DataPublicPluginStart;
+  let fieldProps: FieldItemSharedProps;
+
+  beforeEach(() => {
+    indexPattern = {
+      id: '1',
+      title: 'my-fake-index-pattern',
+      timeFieldName: 'timestamp',
+      fields: [
+        {
+          name: 'timestamp',
+          type: 'date',
+          aggregatable: true,
+          searchable: true,
+        },
+        {
+          name: 'bytes',
+          type: 'number',
+          aggregatable: true,
+          searchable: true,
+        },
+      ],
+    } as IndexPattern;
+    core = coreMock.createSetup();
+    data = dataPluginMock.createStartContract();
+    core.http.post.mockClear();
+
+    fieldProps = {
+      indexPattern,
+      data,
+      core,
+      highlight: '',
+      dateRange: {
+        fromDate: 'now-7d',
+        toDate: 'now',
+      },
+      query: { query: '', language: 'lucene' },
+      filters: [],
+    };
+
+    defaultProps = {
+      initialIsOpen: true,
+      onToggle: jest.fn(),
+      id: 'id',
+      label: 'label',
+      hasLoaded: true,
+      fieldsCount: 2,
+      isFiltered: false,
+      paginatedFields: indexPattern.fields,
+      fieldProps,
+      renderCallout: <div id="lens-test-callout">Callout</div>,
+      exists: true,
+    };
+  });
+
+  it('renders correct number of Field Items', () => {
+    const wrapper = mountWithIntl(<FieldsAccordion {...defaultProps} />);
+    expect(wrapper.find(FieldItem).length).toEqual(2);
+  });
+
+  it('renders callout if no fields', () => {
+    const wrapper = shallowWithIntl(
+      <FieldsAccordion {...defaultProps} fieldsCount={0} paginatedFields={[]} />
+    );
+    expect(wrapper.find('#lens-test-callout').length).toEqual(1);
+  });
+
+  it('renders accented notificationBadge state if isFiltered', () => {
+    const wrapper = mountWithIntl(<FieldsAccordion {...defaultProps} isFiltered={true} />);
+    expect(wrapper.find(EuiNotificationBadge).prop('color')).toEqual('accent');
+  });
+
+  it('renders spinner if has not loaded', () => {
+    const wrapper = mountWithIntl(<FieldsAccordion {...defaultProps} hasLoaded={false} />);
+    expect(wrapper.find(EuiLoadingSpinner).length).toEqual(1);
+  });
+});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
new file mode 100644
index 0000000000000..b756cf81a9073
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
@@ -0,0 +1,101 @@
+/*
+ * 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 './datapanel.scss';
+import React, { memo, useCallback } from 'react';
+import {
+  EuiText,
+  EuiNotificationBadge,
+  EuiSpacer,
+  EuiAccordion,
+  EuiLoadingSpinner,
+} from '@elastic/eui';
+import { DataPublicPluginStart } from 'src/plugins/data/public';
+import { IndexPatternField } from './types';
+import { FieldItem } from './field_item';
+import { Query, Filter } from '../../../../../src/plugins/data/public';
+import { DatasourceDataPanelProps } from '../types';
+import { IndexPattern } from './types';
+
+export interface FieldItemSharedProps {
+  core: DatasourceDataPanelProps['core'];
+  data: DataPublicPluginStart;
+  indexPattern: IndexPattern;
+  highlight?: string;
+  query: Query;
+  dateRange: DatasourceDataPanelProps['dateRange'];
+  filters: Filter[];
+}
+
+export interface FieldsAccordionProps {
+  initialIsOpen: boolean;
+  onToggle: (open: boolean) => void;
+  id: string;
+  label: string;
+  hasLoaded: boolean;
+  fieldsCount: number;
+  isFiltered: boolean;
+  paginatedFields: IndexPatternField[];
+  fieldProps: FieldItemSharedProps;
+  renderCallout: JSX.Element;
+  exists: boolean;
+}
+
+export const InnerFieldsAccordion = function InnerFieldsAccordion({
+  initialIsOpen,
+  onToggle,
+  id,
+  label,
+  hasLoaded,
+  fieldsCount,
+  isFiltered,
+  paginatedFields,
+  fieldProps,
+  renderCallout,
+  exists,
+}: FieldsAccordionProps) {
+  const renderField = useCallback(
+    (field: IndexPatternField) => {
+      return <FieldItem {...fieldProps} key={field.name} field={field} exists={!!exists} />;
+    },
+    [fieldProps, exists]
+  );
+
+  return (
+    <EuiAccordion
+      initialIsOpen={initialIsOpen}
+      onToggle={onToggle}
+      data-test-subj={id}
+      id={id}
+      buttonContent={
+        <EuiText size="xs">
+          <strong>{label}</strong>
+        </EuiText>
+      }
+      extraAction={
+        hasLoaded ? (
+          <EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}>
+            {fieldsCount}
+          </EuiNotificationBadge>
+        ) : (
+          <EuiLoadingSpinner size="m" />
+        )
+      }
+    >
+      <EuiSpacer size="s" />
+      {hasLoaded &&
+        (!!fieldsCount ? (
+          <div className="lnsInnerIndexPatternDataPanel__fieldItems">
+            {paginatedFields && paginatedFields.map(renderField)}
+          </div>
+        ) : (
+          renderCallout
+        ))}
+    </EuiAccordion>
+  );
+};
+
+export const FieldsAccordion = memo(InnerFieldsAccordion);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index d8449143b569f..a69d7c055eaa7 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -127,7 +127,6 @@ function stateFromPersistedState(
     indexPatterns: expectedIndexPatterns,
     indexPatternRefs: [],
     existingFields: {},
-    showEmptyFields: true,
   };
 }
 
@@ -402,7 +401,6 @@ describe('IndexPattern Data Source', () => {
           },
         },
         currentIndexPatternId: '1',
-        showEmptyFields: false,
       };
       expect(indexPatternDatasource.insertLayer(state, 'newLayer')).toEqual({
         ...state,
@@ -423,7 +421,6 @@ describe('IndexPattern Data Source', () => {
       const state = {
         indexPatternRefs: [],
         existingFields: {},
-        showEmptyFields: false,
         indexPatterns: expectedIndexPatterns,
         layers: {
           first: {
@@ -458,7 +455,6 @@ describe('IndexPattern Data Source', () => {
         indexPatternDatasource.getLayers({
           indexPatternRefs: [],
           existingFields: {},
-          showEmptyFields: false,
           indexPatterns: expectedIndexPatterns,
           layers: {
             first: {
@@ -484,7 +480,6 @@ describe('IndexPattern Data Source', () => {
         indexPatternDatasource.getMetaData({
           indexPatternRefs: [],
           existingFields: {},
-          showEmptyFields: false,
           indexPatterns: expectedIndexPatterns,
           layers: {
             first: {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
index 5eca55cbfcbda..87d91b56d2a5c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx
@@ -146,7 +146,6 @@ function testInitialState(): IndexPatternPrivateState {
         },
       },
     },
-    showEmptyFields: false,
   };
 }
 
@@ -305,7 +304,6 @@ describe('IndexPattern Data Source suggestions', () => {
           indexPatternRefs: [],
           existingFields: {},
           currentIndexPatternId: '1',
-          showEmptyFields: false,
           indexPatterns: {
             1: {
               id: '1',
@@ -510,7 +508,6 @@ describe('IndexPattern Data Source suggestions', () => {
           indexPatternRefs: [],
           existingFields: {},
           currentIndexPatternId: '1',
-          showEmptyFields: false,
           indexPatterns: {
             1: {
               id: '1',
@@ -1049,7 +1046,6 @@ describe('IndexPattern Data Source suggestions', () => {
     it('returns no suggestions if there are no columns', () => {
       expect(
         getDatasourceSuggestionsFromCurrentState({
-          showEmptyFields: false,
           indexPatternRefs: [],
           existingFields: {},
           indexPatterns: expectedIndexPatterns,
@@ -1355,7 +1351,6 @@ describe('IndexPattern Data Source suggestions', () => {
             ],
           },
         },
-        showEmptyFields: true,
         layers: {
           first: {
             ...initialState.layers.first,
@@ -1475,7 +1470,6 @@ describe('IndexPattern Data Source suggestions', () => {
             ],
           },
         },
-        showEmptyFields: true,
         layers: {
           first: {
             ...initialState.layers.first,
@@ -1529,7 +1523,6 @@ describe('IndexPattern Data Source suggestions', () => {
             ],
           },
         },
-        showEmptyFields: true,
         layers: {
           first: {
             ...initialState.layers.first,
@@ -1560,7 +1553,6 @@ describe('IndexPattern Data Source suggestions', () => {
         existingFields: {},
         currentIndexPatternId: '1',
         indexPatterns: expectedIndexPatterns,
-        showEmptyFields: true,
         layers: {
           first: {
             ...initialState.layers.first,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx
index 0d16e2d054a77..9cbd624b42d3e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/layerpanel.test.tsx
@@ -22,7 +22,6 @@ const initialState: IndexPatternPrivateState = {
   ],
   existingFields: {},
   currentIndexPatternId: '1',
-  showEmptyFields: false,
   layers: {
     first: {
       indexPatternId: '1',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
index 55fd8a6d936d3..5e59627d8c335 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
@@ -294,7 +294,6 @@ describe('loader', () => {
           a: sampleIndexPatterns.a,
         },
         layers: {},
-        showEmptyFields: false,
       });
       expect(storage.set).toHaveBeenCalledWith('lens-settings', {
         indexPatternId: 'a',
@@ -363,7 +362,6 @@ describe('loader', () => {
           b: sampleIndexPatterns.b,
         },
         layers: {},
-        showEmptyFields: false,
       });
       expect(storage.set).toHaveBeenCalledWith('lens-settings', {
         indexPatternId: 'b',
@@ -416,7 +414,6 @@ describe('loader', () => {
           b: sampleIndexPatterns.b,
         },
         layers: savedState.layers,
-        showEmptyFields: false,
       });
 
       expect(storage.set).toHaveBeenCalledWith('lens-settings', {
@@ -434,7 +431,6 @@ describe('loader', () => {
         indexPatterns: {},
         existingFields: {},
         layers: {},
-        showEmptyFields: true,
       };
       const storage = createMockStorage({ indexPatternId: 'b' });
 
@@ -469,7 +465,6 @@ describe('loader', () => {
         existingFields: {},
         indexPatterns: {},
         layers: {},
-        showEmptyFields: true,
       };
 
       const storage = createMockStorage({ indexPatternId: 'b' });
@@ -527,7 +522,6 @@ describe('loader', () => {
             indexPatternId: 'a',
           },
         },
-        showEmptyFields: true,
       };
 
       const storage = createMockStorage({ indexPatternId: 'a' });
@@ -596,7 +590,6 @@ describe('loader', () => {
             indexPatternId: 'a',
           },
         },
-        showEmptyFields: true,
       };
 
       const storage = createMockStorage({ indexPatternId: 'b' });
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
index ca52ffe73a871..6c57988dfc7b6 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts
@@ -118,7 +118,6 @@ export async function loadInitialState({
       currentIndexPatternId,
       indexPatternRefs,
       indexPatterns,
-      showEmptyFields: false,
       existingFields: {},
     };
   }
@@ -128,7 +127,6 @@ export async function loadInitialState({
     indexPatternRefs,
     indexPatterns,
     layers: {},
-    showEmptyFields: false,
     existingFields: {},
   };
 }
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx
new file mode 100644
index 0000000000000..f32bf52339e1c
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.test.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import { NoFieldsCallout } from './no_fields_callout';
+
+describe('NoFieldCallout', () => {
+  it('renders properly for index with no fields', () => {
+    const component = shallow(
+      <NoFieldsCallout existFieldsInIndex={false} isAffectedByFieldFilter={false} />
+    );
+    expect(component).toMatchSnapshot();
+  });
+
+  it('renders properly when affected by field filters, global filter and timerange', () => {
+    const component = shallow(
+      <NoFieldsCallout
+        existFieldsInIndex={true}
+        isAffectedByFieldFilter={true}
+        isAffectedByTimerange={true}
+        isAffectedByGlobalFilter={true}
+      />
+    );
+    expect(component).toMatchSnapshot();
+  });
+
+  it('renders properly when affected by field filter', () => {
+    const component = shallow(
+      <NoFieldsCallout existFieldsInIndex={true} isAffectedByFieldFilter={true} />
+    );
+    expect(component).toMatchSnapshot();
+  });
+});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx
new file mode 100644
index 0000000000000..066d60f006207
--- /dev/null
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/no_fields_callout.tsx
@@ -0,0 +1,75 @@
+/*
+ * 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 { EuiCallOut } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+export const NoFieldsCallout = ({
+  isAffectedByFieldFilter,
+  existFieldsInIndex,
+  isAffectedByTimerange = false,
+  isAffectedByGlobalFilter = false,
+}: {
+  isAffectedByFieldFilter: boolean;
+  existFieldsInIndex: boolean;
+  isAffectedByTimerange?: boolean;
+  isAffectedByGlobalFilter?: boolean;
+}) => {
+  return (
+    <EuiCallOut
+      size="s"
+      color="warning"
+      title={
+        isAffectedByFieldFilter
+          ? i18n.translate('xpack.lens.indexPatterns.noFilteredFieldsLabel', {
+              defaultMessage: 'No fields match the selected filters.',
+            })
+          : existFieldsInIndex
+          ? i18n.translate('xpack.lens.indexPatterns.noDataLabel', {
+              defaultMessage: `There are no available fields that contain data.`,
+            })
+          : i18n.translate('xpack.lens.indexPatterns.noFieldsLabel', {
+              defaultMessage: 'No fields exist in this index pattern.',
+            })
+      }
+    >
+      {existFieldsInIndex && (
+        <>
+          <strong>
+            {i18n.translate('xpack.lens.indexPatterns.noFields.tryText', {
+              defaultMessage: 'Try:',
+            })}
+          </strong>
+          <ul>
+            {isAffectedByTimerange && (
+              <>
+                <li>
+                  {i18n.translate('xpack.lens.indexPatterns.noFields.extendTimeBullet', {
+                    defaultMessage: 'Extending the time range',
+                  })}
+                </li>
+              </>
+            )}
+            {isAffectedByFieldFilter ? (
+              <li>
+                {i18n.translate('xpack.lens.indexPatterns.noFields.fieldTypeFilterBullet', {
+                  defaultMessage: 'Using different field filters',
+                })}
+              </li>
+            ) : null}
+            {isAffectedByGlobalFilter ? (
+              <li>
+                {i18n.translate('xpack.lens.indexPatterns.noFields.globalFiltersBullet', {
+                  defaultMessage: 'Changing the global filters',
+                })}
+              </li>
+            ) : null}
+          </ul>
+        </>
+      )}
+    </EuiCallOut>
+  );
+};
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
index defc142d4976e..d0c7af42114e3 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
@@ -51,7 +51,6 @@ describe('date_histogram', () => {
       indexPatternRefs: [],
       existingFields: {},
       currentIndexPatternId: '1',
-      showEmptyFields: false,
       indexPatterns: {
         1: {
           id: '1',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx
index 89d02708a900c..1e1d83a0a5c4c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.test.tsx
@@ -34,7 +34,6 @@ describe('terms', () => {
       indexPatterns: {},
       existingFields: {},
       currentIndexPatternId: '1',
-      showEmptyFields: false,
       layers: {
         first: {
           indexPatternId: '1',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts
index e5d20839aae3d..a73f6e13d94c5 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts
@@ -147,7 +147,6 @@ describe('getOperationTypesForField', () => {
       indexPatternRefs: [],
       existingFields: {},
       currentIndexPatternId: '1',
-      showEmptyFields: false,
       indexPatterns: expectedIndexPatterns,
       layers: {
         first: {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts
index 074cb8f5bde17..65a2401fd689a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/state_helpers.test.ts
@@ -42,7 +42,6 @@ describe('state_helpers', () => {
         existingFields: {},
         indexPatterns: {},
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           first: {
             indexPatternId: '1',
@@ -96,7 +95,6 @@ describe('state_helpers', () => {
         existingFields: {},
         indexPatterns: {},
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           first: {
             indexPatternId: '1',
@@ -147,7 +145,6 @@ describe('state_helpers', () => {
         existingFields: {},
         indexPatterns: {},
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           first: {
             indexPatternId: '1',
@@ -188,7 +185,6 @@ describe('state_helpers', () => {
         existingFields: {},
         indexPatterns: {},
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           first: {
             indexPatternId: '1',
@@ -222,7 +218,6 @@ describe('state_helpers', () => {
         existingFields: {},
         indexPatterns: {},
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           first: {
             indexPatternId: '1',
@@ -284,7 +279,6 @@ describe('state_helpers', () => {
         existingFields: {},
         indexPatterns: {},
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           first: {
             indexPatternId: '1',
@@ -337,7 +331,6 @@ describe('state_helpers', () => {
         existingFields: {},
         indexPatterns: {},
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           first: {
             indexPatternId: '1',
@@ -417,7 +410,6 @@ describe('state_helpers', () => {
         existingFields: {},
         indexPatterns: {},
         currentIndexPatternId: '1',
-        showEmptyFields: false,
         layers: {
           first: {
             indexPatternId: '1',
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
index 563af40ed2720..35a82d8774130 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts
@@ -51,7 +51,6 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & {
    * indexPatternId -> fieldName -> boolean
    */
   existingFields: Record<string, Record<string, boolean>>;
-  showEmptyFields: boolean;
 };
 
 export interface IndexPatternRef {
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 2a7517540e708..ab7215ef923af 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8645,7 +8645,6 @@
     "xpack.lens.indexPattern.groupingSecondDateHistogram": "各 {target} の日付",
     "xpack.lens.indexPattern.groupingSecondTerms": "各 {target} のトップの値",
     "xpack.lens.indexPattern.indexPatternLoadError": "インデックスパターンの読み込み中にエラーが発生",
-    "xpack.lens.indexPattern.individualFieldsLabel": "個々のフィールド",
     "xpack.lens.indexPattern.invalidInterval": "無効な間隔値",
     "xpack.lens.indexPattern.invalidOperationLabel": "この関数を使用するには、別のフィールドを選択してください。",
     "xpack.lens.indexPattern.max": "最高",
@@ -8676,16 +8675,11 @@
     "xpack.lens.indexPattern.termsOf": "{name} のトップの値",
     "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]",
     "xpack.lens.indexPatterns.clearFiltersLabel": "名前とタイプフィルターを消去",
-    "xpack.lens.indexPatterns.emptyFieldsWithDataLabel": "データがないようです。",
     "xpack.lens.indexPatterns.filterByNameAriaLabel": "検索フィールド",
     "xpack.lens.indexPatterns.filterByNameLabel": "フィールドを検索",
-    "xpack.lens.indexPatterns.filterByTypeLabel": "タイプでフィルタリング",
     "xpack.lens.indexPatterns.noFields.extendTimeBullet": "時間範囲を拡張中",
-    "xpack.lens.indexPatterns.noFields.fieldFilterBullet": "{filterByTypeLabel} {arrow} を使用してデータなしのフィールドを表示",
     "xpack.lens.indexPatterns.noFields.tryText": "試行対象:",
     "xpack.lens.indexPatterns.noFieldsLabel": "このインデックスパターンにはフィールドがありません。",
-    "xpack.lens.indexPatterns.noFilteredFieldsLabel": "現在のフィルターと一致するフィールドはありません。",
-    "xpack.lens.indexPatterns.toggleEmptyFieldsSwitch": "データがあるフィールドだけを表示",
     "xpack.lens.indexPatternSuggestion.removeLayerLabel": "{indexPatternTitle}のみを表示",
     "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "レイヤー{layerNumber}のみを表示",
     "xpack.lens.lensSavedObjectLabel": "レンズビジュアライゼーション",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 9a55fee2b8898..a72b79c3ae0c7 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8649,7 +8649,6 @@
     "xpack.lens.indexPattern.groupingSecondDateHistogram": "每个 {target} 的日期",
     "xpack.lens.indexPattern.groupingSecondTerms": "每个 {target} 的排名最前值",
     "xpack.lens.indexPattern.indexPatternLoadError": "加载索引模式时出错",
-    "xpack.lens.indexPattern.individualFieldsLabel": "各个字段",
     "xpack.lens.indexPattern.invalidInterval": "时间间隔值无效",
     "xpack.lens.indexPattern.invalidOperationLabel": "要使用此函数,请选择不同的字段。",
     "xpack.lens.indexPattern.max": "最大值",
@@ -8680,16 +8679,11 @@
     "xpack.lens.indexPattern.termsOf": "{name} 的排名最前值",
     "xpack.lens.indexPattern.uniqueLabel": "{label} [{num}]",
     "xpack.lens.indexPatterns.clearFiltersLabel": "清除名称和类型筛选",
-    "xpack.lens.indexPatterns.emptyFieldsWithDataLabel": "似乎您没有任何数据。",
     "xpack.lens.indexPatterns.filterByNameAriaLabel": "搜索字段",
     "xpack.lens.indexPatterns.filterByNameLabel": "搜索字段",
-    "xpack.lens.indexPatterns.filterByTypeLabel": "按类型筛选",
     "xpack.lens.indexPatterns.noFields.extendTimeBullet": "延伸时间范围",
-    "xpack.lens.indexPatterns.noFields.fieldFilterBullet": "使用 {filterByTypeLabel} {arrow} 显示没有数据的字段",
     "xpack.lens.indexPatterns.noFields.tryText": "尝试:",
     "xpack.lens.indexPatterns.noFieldsLabel": "在此索引模式中不存在任何字段。",
-    "xpack.lens.indexPatterns.noFilteredFieldsLabel": "没有任何字段匹配当前筛选。",
-    "xpack.lens.indexPatterns.toggleEmptyFieldsSwitch": "仅显示具有数据的字段",
     "xpack.lens.indexPatternSuggestion.removeLayerLabel": "仅显示 {indexPatternTitle}",
     "xpack.lens.indexPatternSuggestion.removeLayerPositionLabel": "仅显示图层 {layerNumber}",
     "xpack.lens.lensSavedObjectLabel": "Lens 可视化",
diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts
index 3f048a9ee2aaa..bae11e1ea8a90 100644
--- a/x-pack/test/functional/page_objects/lens_page.ts
+++ b/x-pack/test/functional/page_objects/lens_page.ts
@@ -30,15 +30,6 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
       await testSubjects.click('lnsIndexPatternFiltersToggle');
     },
 
-    /**
-     * Toggles the field existence checkbox.
-     */
-    async toggleExistenceFilter() {
-      await this.toggleIndexPatternFiltersPopover();
-      await testSubjects.click('lnsEmptyFilter');
-      await this.toggleIndexPatternFiltersPopover();
-    },
-
     async findAllFields() {
       return await testSubjects.findAll('lnsFieldListPanelField');
     },

From 6ebf56ba66c89f382e68fa81d0f3d4837904aa22 Mon Sep 17 00:00:00 2001
From: Dmitry Lemeshko <dzmitry.lemechko@elastic.co>
Date: Fri, 26 Jun 2020 19:02:30 +0200
Subject: [PATCH 05/21] Adding saved_objects_page in OSS (#69900)

* add savedObjects own PO

* fix usage

* simplify functions

* fix test

* fix title parsing

* add missing await

* improve parsing

* wait for table is loaded

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 test/functional/apps/dashboard/time_zones.js  |  12 +-
 .../apps/management/_import_objects.js        | 199 ++++++++----------
 .../management/_mgmt_import_saved_objects.js  |  12 +-
 .../edit_saved_object.ts                      |  14 +-
 test/functional/page_objects/index.ts         |   2 +
 .../management/saved_objects_page.ts          | 184 ++++++++++++++++
 test/functional/page_objects/settings_page.ts | 179 +---------------
 .../saved_objects_management_security.ts      |  25 ++-
 .../copy_saved_objects_to_space_page.ts       |  16 +-
 9 files changed, 331 insertions(+), 312 deletions(-)
 create mode 100644 test/functional/page_objects/management/saved_objects_page.ts

diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js
index b0344a8b69064..4e95a14efb4d6 100644
--- a/test/functional/apps/dashboard/time_zones.js
+++ b/test/functional/apps/dashboard/time_zones.js
@@ -24,7 +24,13 @@ export default function ({ getService, getPageObjects }) {
   const pieChart = getService('pieChart');
   const esArchiver = getService('esArchiver');
   const kibanaServer = getService('kibanaServer');
-  const PageObjects = getPageObjects(['dashboard', 'timePicker', 'settings', 'common']);
+  const PageObjects = getPageObjects([
+    'dashboard',
+    'timePicker',
+    'settings',
+    'common',
+    'savedObjects',
+  ]);
 
   describe('dashboard time zones', function () {
     this.tags('includeFirefox');
@@ -36,10 +42,10 @@ export default function ({ getService, getPageObjects }) {
       });
       await PageObjects.settings.navigateTo();
       await PageObjects.settings.clickKibanaSavedObjects();
-      await PageObjects.settings.importFile(
+      await PageObjects.savedObjects.importFile(
         path.join(__dirname, 'exports', 'timezonetest_6_2_4.json')
       );
-      await PageObjects.settings.checkImportSucceeded();
+      await PageObjects.savedObjects.checkImportSucceeded();
       await PageObjects.common.navigateToApp('dashboard');
       await PageObjects.dashboard.preserveCrossAppState();
       await PageObjects.dashboard.loadSavedDashboard('time zone test');
diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js
index 6306d11eadb65..c69111be6972b 100644
--- a/test/functional/apps/management/_import_objects.js
+++ b/test/functional/apps/management/_import_objects.js
@@ -24,7 +24,7 @@ import { indexBy } from 'lodash';
 export default function ({ getService, getPageObjects }) {
   const kibanaServer = getService('kibanaServer');
   const esArchiver = getService('esArchiver');
-  const PageObjects = getPageObjects(['common', 'settings', 'header']);
+  const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']);
   const testSubjects = getService('testSubjects');
   const log = getService('log');
 
@@ -43,22 +43,19 @@ export default function ({ getService, getPageObjects }) {
       });
 
       it('should import saved objects', async function () {
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects.ndjson')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
         // get all the elements in the table, and index them by the 'title' visible text field
-        const elements = indexBy(
-          await PageObjects.settings.getSavedObjectElementsInTable(),
-          'title'
-        );
+        const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title');
         log.debug("check that 'Log Agents' is in table as a visualization");
         expect(elements['Log Agents'].objectType).to.eql('visualization');
 
         await elements['logstash-*'].relationshipsElement.click();
-        const flyout = indexBy(await PageObjects.settings.getRelationshipFlyout(), 'title');
+        const flyout = indexBy(await PageObjects.savedObjects.getRelationshipFlyout(), 'title');
         log.debug(
           "check that 'Shared-Item Visualization AreaChart' shows 'logstash-*' as it's Parent"
         );
@@ -68,18 +65,18 @@ export default function ({ getService, getPageObjects }) {
       });
 
       it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_conflicts.ndjson')
         );
-        await PageObjects.settings.checkImportConflictsWarning();
+        await PageObjects.savedObjects.checkImportConflictsWarning();
         await PageObjects.settings.associateIndexPattern(
           'd1e4c910-a2e6-11e7-bb30-233be9be6a15',
           'logstash-*'
         );
-        await PageObjects.settings.clickConfirmChanges();
+        await PageObjects.savedObjects.clickConfirmChanges();
         await PageObjects.header.waitUntilLoadingHasFinished();
-        await PageObjects.settings.clickImportDone();
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        await PageObjects.savedObjects.clickImportDone();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
         expect(isSavedObjectImported).to.be(true);
       });
@@ -87,14 +84,14 @@ export default function ({ getService, getPageObjects }) {
       it('should allow the user to override duplicate saved objects', async function () {
         // This data has already been loaded by the "visualize" esArchive. We'll load it again
         // so that we can override the existing visualization.
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_exists.ndjson'),
           false
         );
 
-        await PageObjects.settings.checkImportConflictsWarning();
+        await PageObjects.savedObjects.checkImportConflictsWarning();
         await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
-        await PageObjects.settings.clickConfirmChanges();
+        await PageObjects.savedObjects.clickConfirmChanges();
 
         // Override the visualization.
         await PageObjects.common.clickConfirmOnModal();
@@ -106,14 +103,14 @@ export default function ({ getService, getPageObjects }) {
       it('should allow the user to cancel overriding duplicate saved objects', async function () {
         // This data has already been loaded by the "visualize" esArchive. We'll load it again
         // so that we can be prompted to override the existing visualization.
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_exists.ndjson'),
           false
         );
 
-        await PageObjects.settings.checkImportConflictsWarning();
+        await PageObjects.savedObjects.checkImportConflictsWarning();
         await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
-        await PageObjects.settings.clickConfirmChanges();
+        await PageObjects.savedObjects.clickConfirmChanges();
 
         // *Don't* override the visualization.
         await PageObjects.common.clickCancelOnModal();
@@ -123,86 +120,80 @@ export default function ({ getService, getPageObjects }) {
       });
 
       it('should import saved objects linked to saved searches', async function () {
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_saved_search.ndjson')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object connected to saved search');
         expect(isSavedObjectImported).to.be(true);
       });
 
       it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.ndjson')
         );
-        await PageObjects.settings.checkNoneImported();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkNoneImported();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object connected to saved search');
         expect(isSavedObjectImported).to.be(false);
       });
 
       it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
-        const elements = indexBy(
-          await PageObjects.settings.getSavedObjectElementsInTable(),
-          'title'
-        );
+        const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title');
         await elements['logstash-*'].checkbox.click();
-        await PageObjects.settings.clickSavedObjectsDelete();
+        await PageObjects.savedObjects.clickDelete();
 
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_with_saved_search.ndjson')
         );
         // Wait for all the saves to happen
-        await PageObjects.settings.checkImportConflictsWarning();
-        await PageObjects.settings.clickConfirmChanges();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportConflictsWarning();
+        await PageObjects.savedObjects.clickConfirmChanges();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object connected to saved search');
         expect(isSavedObjectImported).to.be(false);
       });
 
       it('should import saved objects with index patterns when index patterns already exists', async () => {
         // First, import the objects
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object imported with index pattern');
         expect(isSavedObjectImported).to.be(true);
       });
 
       it('should import saved objects with index patterns when index patterns does not exists', async () => {
         // First, we need to delete the index pattern
-        const elements = indexBy(
-          await PageObjects.settings.getSavedObjectElementsInTable(),
-          'title'
-        );
+        const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title');
         await elements['logstash-*'].checkbox.click();
-        await PageObjects.settings.clickSavedObjectsDelete();
+        await PageObjects.savedObjects.clickDelete();
 
         // Then, import the objects
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_with_index_patterns.ndjson')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object imported with index pattern');
         expect(isSavedObjectImported).to.be(true);
       });
@@ -222,30 +213,30 @@ export default function ({ getService, getPageObjects }) {
       });
 
       it('should import saved objects', async function () {
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects.json')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('Log Agents');
         expect(isSavedObjectImported).to.be(true);
       });
 
       it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () {
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects-conflicts.json')
         );
-        await PageObjects.settings.checkImportLegacyWarning();
-        await PageObjects.settings.checkImportConflictsWarning();
+        await PageObjects.savedObjects.checkImportLegacyWarning();
+        await PageObjects.savedObjects.checkImportConflictsWarning();
         await PageObjects.settings.associateIndexPattern(
           'd1e4c910-a2e6-11e7-bb30-233be9be6a15',
           'logstash-*'
         );
-        await PageObjects.settings.clickConfirmChanges();
+        await PageObjects.savedObjects.clickConfirmChanges();
         await PageObjects.header.waitUntilLoadingHasFinished();
-        await PageObjects.settings.clickImportDone();
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        await PageObjects.savedObjects.clickImportDone();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object with index pattern conflict');
         expect(isSavedObjectImported).to.be(true);
       });
@@ -253,15 +244,15 @@ export default function ({ getService, getPageObjects }) {
       it('should allow the user to override duplicate saved objects', async function () {
         // This data has already been loaded by the "visualize" esArchive. We'll load it again
         // so that we can override the existing visualization.
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_exists.json'),
           false
         );
 
-        await PageObjects.settings.checkImportLegacyWarning();
-        await PageObjects.settings.checkImportConflictsWarning();
+        await PageObjects.savedObjects.checkImportLegacyWarning();
+        await PageObjects.savedObjects.checkImportConflictsWarning();
         await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
-        await PageObjects.settings.clickConfirmChanges();
+        await PageObjects.savedObjects.clickConfirmChanges();
 
         // Override the visualization.
         await PageObjects.common.clickConfirmOnModal();
@@ -273,15 +264,15 @@ export default function ({ getService, getPageObjects }) {
       it('should allow the user to cancel overriding duplicate saved objects', async function () {
         // This data has already been loaded by the "visualize" esArchive. We'll load it again
         // so that we can be prompted to override the existing visualization.
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_exists.json'),
           false
         );
 
-        await PageObjects.settings.checkImportLegacyWarning();
-        await PageObjects.settings.checkImportConflictsWarning();
+        await PageObjects.savedObjects.checkImportLegacyWarning();
+        await PageObjects.savedObjects.checkImportConflictsWarning();
         await PageObjects.settings.associateIndexPattern('logstash-*', 'logstash-*');
-        await PageObjects.settings.clickConfirmChanges();
+        await PageObjects.savedObjects.clickConfirmChanges();
 
         // *Don't* override the visualization.
         await PageObjects.common.clickCancelOnModal();
@@ -291,95 +282,89 @@ export default function ({ getService, getPageObjects }) {
       });
 
       it('should import saved objects linked to saved searches', async function () {
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_saved_search.json')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object connected to saved search');
         expect(isSavedObjectImported).to.be(true);
       });
 
       it('should not import saved objects linked to saved searches when saved search does not exist', async function () {
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json')
         );
-        await PageObjects.settings.checkImportFailedWarning();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportFailedWarning();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object connected to saved search');
         expect(isSavedObjectImported).to.be(false);
       });
 
       it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () {
         // First, import the saved search
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_saved_search.json')
         );
         // Wait for all the saves to happen
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
         // Second, we need to delete the index pattern
-        const elements = indexBy(
-          await PageObjects.settings.getSavedObjectElementsInTable(),
-          'title'
-        );
+        const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title');
         await elements['logstash-*'].checkbox.click();
-        await PageObjects.settings.clickSavedObjectsDelete();
+        await PageObjects.savedObjects.clickDelete();
 
         // Last, import a saved object connected to the saved search
         // This should NOT show the conflicts
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json')
         );
         // Wait for all the saves to happen
-        await PageObjects.settings.checkNoneImported();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkNoneImported();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object connected to saved search');
         expect(isSavedObjectImported).to.be(false);
       });
 
       it('should import saved objects with index patterns when index patterns already exists', async () => {
         // First, import the objects
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json')
         );
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object imported with index pattern');
         expect(isSavedObjectImported).to.be(true);
       });
 
       it('should import saved objects with index patterns when index patterns does not exists', async () => {
         // First, we need to delete the index pattern
-        const elements = indexBy(
-          await PageObjects.settings.getSavedObjectElementsInTable(),
-          'title'
-        );
+        const elements = indexBy(await PageObjects.savedObjects.getElementsInTable(), 'title');
         await elements['logstash-*'].checkbox.click();
-        await PageObjects.settings.clickSavedObjectsDelete();
+        await PageObjects.savedObjects.clickDelete();
 
         // Then, import the objects
-        await PageObjects.settings.importFile(
+        await PageObjects.savedObjects.importFile(
           path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json')
         );
-        await PageObjects.settings.checkImportSucceeded();
-        await PageObjects.settings.clickImportDone();
+        await PageObjects.savedObjects.checkImportSucceeded();
+        await PageObjects.savedObjects.clickImportDone();
 
-        const objects = await PageObjects.settings.getSavedObjectsInTable();
+        const objects = await PageObjects.savedObjects.getRowTitles();
         const isSavedObjectImported = objects.includes('saved object imported with index pattern');
         expect(isSavedObjectImported).to.be(true);
       });
diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js
index a8a0a19d4962d..3a9f8665fd33b 100644
--- a/test/functional/apps/management/_mgmt_import_saved_objects.js
+++ b/test/functional/apps/management/_mgmt_import_saved_objects.js
@@ -22,7 +22,7 @@ import path from 'path';
 
 export default function ({ getService, getPageObjects }) {
   const esArchiver = getService('esArchiver');
-  const PageObjects = getPageObjects(['common', 'settings', 'header']);
+  const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']);
 
   //in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization
   //that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238)
@@ -40,19 +40,19 @@ export default function ({ getService, getPageObjects }) {
 
     it('should import saved objects mgmt', async function () {
       await PageObjects.settings.clickKibanaSavedObjects();
-      await PageObjects.settings.importFile(
+      await PageObjects.savedObjects.importFile(
         path.join(__dirname, 'exports', 'mgmt_import_objects.json')
       );
       await PageObjects.settings.associateIndexPattern(
         '4c3f3c30-ac94-11e8-a651-614b2788174a',
         'logstash-*'
       );
-      await PageObjects.settings.clickConfirmChanges();
-      await PageObjects.settings.clickImportDone();
-      await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
+      await PageObjects.savedObjects.clickConfirmChanges();
+      await PageObjects.savedObjects.clickImportDone();
+      await PageObjects.savedObjects.waitTableIsLoaded();
 
       //instead of asserting on count- am asserting on the titles- which is more accurate than count.
-      const objects = await PageObjects.settings.getSavedObjectsInTable();
+      const objects = await PageObjects.savedObjects.getRowTitles();
       expect(objects.includes('mysavedsearch')).to.be(true);
       expect(objects.includes('mysavedviz')).to.be(true);
     });
diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts
index 6e4b820879ed3..2c9200c2f8d93 100644
--- a/test/functional/apps/saved_objects_management/edit_saved_object.ts
+++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts
@@ -25,7 +25,7 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
 export default function ({ getPageObjects, getService }: FtrProviderContext) {
   const esArchiver = getService('esArchiver');
   const testSubjects = getService('testSubjects');
-  const PageObjects = getPageObjects(['common', 'settings']);
+  const PageObjects = getPageObjects(['common', 'settings', 'savedObjects']);
   const browser = getService('browser');
   const find = getService('find');
 
@@ -79,7 +79,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
       await PageObjects.settings.navigateTo();
       await PageObjects.settings.clickKibanaSavedObjects();
 
-      let objects = await PageObjects.settings.getSavedObjectsInTable();
+      let objects = await PageObjects.savedObjects.getRowTitles();
       expect(objects.includes('A Dashboard')).to.be(true);
 
       await PageObjects.common.navigateToUrl(
@@ -99,7 +99,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
 
       await focusAndClickButton('savedObjectEditSave');
 
-      objects = await PageObjects.settings.getSavedObjectsInTable();
+      objects = await PageObjects.savedObjects.getRowTitles();
       expect(objects.includes('A Dashboard')).to.be(false);
       expect(objects.includes('Edited Dashboard')).to.be(true);
 
@@ -127,7 +127,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
       await focusAndClickButton('savedObjectEditDelete');
       await PageObjects.common.clickConfirmOnModal();
 
-      const objects = await PageObjects.settings.getSavedObjectsInTable();
+      const objects = await PageObjects.savedObjects.getRowTitles();
       expect(objects.includes('A Dashboard')).to.be(false);
     });
 
@@ -145,7 +145,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
       await PageObjects.settings.navigateTo();
       await PageObjects.settings.clickKibanaSavedObjects();
 
-      const objects = await PageObjects.settings.getSavedObjectsInTable();
+      const objects = await PageObjects.savedObjects.getRowTitles();
       expect(objects.includes('A Pie')).to.be(true);
 
       await PageObjects.common.navigateToUrl('management', testVisualizationUrl, {
@@ -160,7 +160,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
 
       await focusAndClickButton('savedObjectEditSave');
 
-      await PageObjects.settings.getSavedObjectsInTable();
+      await PageObjects.savedObjects.getRowTitles();
 
       await PageObjects.common.navigateToUrl('management', testVisualizationUrl, {
         shouldUseHashForSubUrl: false,
@@ -173,7 +173,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
 
       await focusAndClickButton('savedObjectEditSave');
 
-      await PageObjects.settings.getSavedObjectsInTable();
+      await PageObjects.savedObjects.getRowTitles();
 
       await PageObjects.common.navigateToUrl('management', testVisualizationUrl, {
         shouldUseHashForSubUrl: false,
diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts
index 10b09c742f58e..d3a8fb73ac3e5 100644
--- a/test/functional/page_objects/index.ts
+++ b/test/functional/page_objects/index.ts
@@ -38,6 +38,7 @@ import { VisualizeChartPageProvider } from './visualize_chart_page';
 import { TileMapPageProvider } from './tile_map_page';
 import { TagCloudPageProvider } from './tag_cloud_page';
 import { VegaChartPageProvider } from './vega_chart_page';
+import { SavedObjectsPageProvider } from './management/saved_objects_page';
 
 export const pageObjects = {
   common: CommonPageProvider,
@@ -61,4 +62,5 @@ export const pageObjects = {
   tileMap: TileMapPageProvider,
   tagCloud: TagCloudPageProvider,
   vegaChart: VegaChartPageProvider,
+  savedObjects: SavedObjectsPageProvider,
 };
diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts
new file mode 100644
index 0000000000000..d058695ea6819
--- /dev/null
+++ b/test/functional/page_objects/management/saved_objects_page.ts
@@ -0,0 +1,184 @@
+/*
+ * 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 { map as mapAsync } from 'bluebird';
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProviderContext) {
+  const log = getService('log');
+  const retry = getService('retry');
+  const browser = getService('browser');
+  const find = getService('find');
+  const testSubjects = getService('testSubjects');
+  const PageObjects = getPageObjects(['header', 'common']);
+
+  class SavedObjectsPage {
+    async searchForObject(objectName: string) {
+      const searchBox = await testSubjects.find('savedObjectSearchBar');
+      await searchBox.clearValue();
+      await searchBox.type(objectName);
+      await searchBox.pressKeys(browser.keys.ENTER);
+    }
+
+    async importFile(path: string, overwriteAll = true) {
+      log.debug(`importFile(${path})`);
+
+      log.debug(`Clicking importObjects`);
+      await testSubjects.click('importObjects');
+      await PageObjects.common.setFileInputPath(path);
+
+      if (!overwriteAll) {
+        log.debug(`Toggling overwriteAll`);
+        await testSubjects.click('importSavedObjectsOverwriteToggle');
+      } else {
+        log.debug(`Leaving overwriteAll alone`);
+      }
+      await testSubjects.click('importSavedObjectsImportBtn');
+      log.debug(`done importing the file`);
+
+      // Wait for all the saves to happen
+      await PageObjects.header.waitUntilLoadingHasFinished();
+    }
+
+    async checkImportSucceeded() {
+      await testSubjects.existOrFail('importSavedObjectsSuccess', { timeout: 20000 });
+    }
+
+    async checkNoneImported() {
+      await testSubjects.existOrFail('importSavedObjectsSuccessNoneImported', { timeout: 20000 });
+    }
+
+    async checkImportConflictsWarning() {
+      await testSubjects.existOrFail('importSavedObjectsConflictsWarning', { timeout: 20000 });
+    }
+
+    async checkImportLegacyWarning() {
+      await testSubjects.existOrFail('importSavedObjectsLegacyWarning', { timeout: 20000 });
+    }
+
+    async checkImportFailedWarning() {
+      await testSubjects.existOrFail('importSavedObjectsFailedWarning', { timeout: 20000 });
+    }
+
+    async clickImportDone() {
+      await testSubjects.click('importSavedObjectsDoneBtn');
+      await this.waitTableIsLoaded();
+    }
+
+    async clickConfirmChanges() {
+      await testSubjects.click('importSavedObjectsConfirmBtn');
+    }
+
+    async waitTableIsLoaded() {
+      return retry.try(async () => {
+        const exists = await find.existsByDisplayedByCssSelector(
+          '*[data-test-subj="savedObjectsTable"] .euiBasicTable-loading'
+        );
+        if (exists) {
+          throw new Error('Waiting');
+        }
+        return true;
+      });
+    }
+
+    async getElementsInTable() {
+      const rows = await testSubjects.findAll('~savedObjectsTableRow');
+      return mapAsync(rows, async (row) => {
+        const checkbox = await row.findByCssSelector('[data-test-subj*="checkboxSelectRow"]');
+        // return the object type aria-label="index patterns"
+        const objectType = await row.findByTestSubject('objectType');
+        const titleElement = await row.findByTestSubject('savedObjectsTableRowTitle');
+        // not all rows have inspect button - Advanced Settings objects don't
+        let inspectElement;
+        const innerHtml = await row.getAttribute('innerHTML');
+        if (innerHtml.includes('Inspect')) {
+          inspectElement = await row.findByTestSubject('savedObjectsTableAction-inspect');
+        } else {
+          inspectElement = null;
+        }
+        const relationshipsElement = await row.findByTestSubject(
+          'savedObjectsTableAction-relationships'
+        );
+        return {
+          checkbox,
+          objectType: await objectType.getAttribute('aria-label'),
+          titleElement,
+          title: await titleElement.getVisibleText(),
+          inspectElement,
+          relationshipsElement,
+        };
+      });
+    }
+
+    async getRowTitles() {
+      await this.waitTableIsLoaded();
+      const table = await testSubjects.find('savedObjectsTable');
+      const $ = await table.parseDomContent();
+      return $.findTestSubjects('savedObjectsTableRowTitle')
+        .toArray()
+        .map((cell) => $(cell).find('.euiTableCellContent').text());
+    }
+
+    async getRelationshipFlyout() {
+      const rows = await testSubjects.findAll('relationshipsTableRow');
+      return mapAsync(rows, async (row) => {
+        const objectType = await row.findByTestSubject('relationshipsObjectType');
+        const relationship = await row.findByTestSubject('directRelationship');
+        const titleElement = await row.findByTestSubject('relationshipsTitle');
+        const inspectElement = await row.findByTestSubject('relationshipsTableAction-inspect');
+        return {
+          objectType: await objectType.getAttribute('aria-label'),
+          relationship: await relationship.getVisibleText(),
+          titleElement,
+          title: await titleElement.getVisibleText(),
+          inspectElement,
+        };
+      });
+    }
+
+    async getTableSummary() {
+      const table = await testSubjects.find('savedObjectsTable');
+      const $ = await table.parseDomContent();
+      return $('tbody tr')
+        .toArray()
+        .map((row) => {
+          return {
+            title: $(row).find('td:nth-child(3) .euiTableCellContent').text(),
+            canViewInApp: Boolean($(row).find('td:nth-child(3) a').length),
+          };
+        });
+    }
+
+    async clickTableSelectAll() {
+      await testSubjects.click('checkboxSelectAll');
+    }
+
+    async canBeDeleted() {
+      return await testSubjects.isEnabled('savedObjectsManagementDelete');
+    }
+
+    async clickDelete() {
+      await testSubjects.click('savedObjectsManagementDelete');
+      await testSubjects.click('confirmModalConfirmButton');
+      await this.waitTableIsLoaded();
+    }
+  }
+
+  return new SavedObjectsPage();
+}
diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts
index f5b4eb7ad5de8..e491cd7e4fe40 100644
--- a/test/functional/page_objects/settings_page.ts
+++ b/test/functional/page_objects/settings_page.ts
@@ -29,7 +29,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
   const flyout = getService('flyout');
   const testSubjects = getService('testSubjects');
   const comboBox = getService('comboBox');
-  const PageObjects = getPageObjects(['header', 'common']);
+  const PageObjects = getPageObjects(['header', 'common', 'savedObjects']);
 
   class SettingsPage {
     async clickNavigation() {
@@ -47,7 +47,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
 
     async clickKibanaSavedObjects() {
       await testSubjects.click('objects');
-      await this.waitUntilSavedObjectsTableIsNotLoading();
+      await PageObjects.savedObjects.waitTableIsLoaded();
     }
 
     async clickKibanaIndexPatterns() {
@@ -68,13 +68,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
 
     async getAdvancedSettings(propertyName: string) {
       log.debug('in getAdvancedSettings');
-      const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`);
-      return await setting.getAttribute('value');
+      return await testSubjects.getAttribute(`advancedSetting-editField-${propertyName}`, 'value');
     }
 
     async expectDisabledAdvancedSetting(propertyName: string) {
-      const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`);
-      expect(setting.getAttribute('disabled')).to.eql('');
+      expect(
+        await testSubjects.getAttribute(`advancedSetting-editField-${propertyName}`, 'disabled')
+      ).to.eql('true');
     }
 
     async getAdvancedSettingCheckbox(propertyName: string) {
@@ -274,9 +274,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
     }
 
     async increasePopularity() {
-      const field = await testSubjects.find('editorFieldCount');
-      await field.clearValueWithKeyboard();
-      await field.type('1');
+      await testSubjects.setValue('editorFieldCount', '1', { clearWithKeyboard: true });
     }
 
     async getPopularity() {
@@ -499,9 +497,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
 
     async setScriptedFieldName(name: string) {
       log.debug('set scripted field name = ' + name);
-      const field = await testSubjects.find('editorFieldName');
-      await field.clearValue();
-      await field.type(name);
+      await testSubjects.setValue('editorFieldName', name);
     }
 
     async setScriptedFieldLanguage(language: string) {
@@ -568,9 +564,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
 
     async setScriptedFieldPopularity(popularity: string) {
       log.debug('set scripted field popularity = ' + popularity);
-      const field = await testSubjects.find('editorFieldCount');
-      await field.clearValue();
-      await field.type(popularity);
+      await testSubjects.setValue('editorFieldCount', popularity);
     }
 
     async setScriptedFieldScript(script: string) {
@@ -623,55 +617,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
       return scriptResults;
     }
 
-    async importFile(path: string, overwriteAll = true) {
-      log.debug(`importFile(${path})`);
-
-      log.debug(`Clicking importObjects`);
-      await testSubjects.click('importObjects');
-      await PageObjects.common.setFileInputPath(path);
-
-      if (!overwriteAll) {
-        log.debug(`Toggling overwriteAll`);
-        await testSubjects.click('importSavedObjectsOverwriteToggle');
-      } else {
-        log.debug(`Leaving overwriteAll alone`);
-      }
-      await testSubjects.click('importSavedObjectsImportBtn');
-      log.debug(`done importing the file`);
-
-      // Wait for all the saves to happen
-      await PageObjects.header.waitUntilLoadingHasFinished();
-    }
-
-    async checkImportSucceeded() {
-      await testSubjects.existOrFail('importSavedObjectsSuccess', { timeout: 20000 });
-    }
-
-    async checkNoneImported() {
-      await testSubjects.existOrFail('importSavedObjectsSuccessNoneImported', { timeout: 20000 });
-    }
-
-    async checkImportConflictsWarning() {
-      await testSubjects.existOrFail('importSavedObjectsConflictsWarning', { timeout: 20000 });
-    }
-
-    async checkImportLegacyWarning() {
-      await testSubjects.existOrFail('importSavedObjectsLegacyWarning', { timeout: 20000 });
-    }
-
-    async checkImportFailedWarning() {
-      await testSubjects.existOrFail('importSavedObjectsFailedWarning', { timeout: 20000 });
-    }
-
-    async clickImportDone() {
-      await testSubjects.click('importSavedObjectsDoneBtn');
-      await this.waitUntilSavedObjectsTableIsNotLoading();
-    }
-
-    async clickConfirmChanges() {
-      await testSubjects.click('importSavedObjectsConfirmBtn');
-    }
-
     async clickEditFieldFormat() {
       await testSubjects.click('editFieldFormat');
     }
@@ -686,112 +631,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
     async clickChangeIndexConfirmButton() {
       await testSubjects.click('changeIndexConfirmButton');
     }
-
-    async waitUntilSavedObjectsTableIsNotLoading() {
-      return retry.try(async () => {
-        const exists = await find.existsByDisplayedByCssSelector(
-          '*[data-test-subj="savedObjectsTable"] .euiBasicTable-loading'
-        );
-        if (exists) {
-          throw new Error('Waiting');
-        }
-        return true;
-      });
-    }
-
-    async getSavedObjectElementsInTable() {
-      const rows = await testSubjects.findAll('~savedObjectsTableRow');
-      return mapAsync(rows, async (row) => {
-        const checkbox = await row.findByCssSelector('[data-test-subj*="checkboxSelectRow"]');
-        // return the object type aria-label="index patterns"
-        const objectType = await row.findByTestSubject('objectType');
-        const titleElement = await row.findByTestSubject('savedObjectsTableRowTitle');
-        // not all rows have inspect button - Advanced Settings objects don't
-        let inspectElement;
-        const innerHtml = await row.getAttribute('innerHTML');
-        if (innerHtml.includes('Inspect')) {
-          inspectElement = await row.findByTestSubject('savedObjectsTableAction-inspect');
-        } else {
-          inspectElement = null;
-        }
-        const relationshipsElement = await row.findByTestSubject(
-          'savedObjectsTableAction-relationships'
-        );
-        return {
-          checkbox,
-          objectType: await objectType.getAttribute('aria-label'),
-          titleElement,
-          title: await titleElement.getVisibleText(),
-          inspectElement,
-          relationshipsElement,
-        };
-      });
-    }
-
-    async getSavedObjectsInTable() {
-      const table = await testSubjects.find('savedObjectsTable');
-      const cells = await table.findAllByTestSubject('savedObjectsTableRowTitle');
-
-      const objects = [];
-      for (const cell of cells) {
-        objects.push(await cell.getVisibleText());
-      }
-
-      return objects;
-    }
-
-    async getRelationshipFlyout() {
-      const rows = await testSubjects.findAll('relationshipsTableRow');
-      return mapAsync(rows, async (row) => {
-        const objectType = await row.findByTestSubject('relationshipsObjectType');
-        const relationship = await row.findByTestSubject('directRelationship');
-        const titleElement = await row.findByTestSubject('relationshipsTitle');
-        const inspectElement = await row.findByTestSubject('relationshipsTableAction-inspect');
-        return {
-          objectType: await objectType.getAttribute('aria-label'),
-          relationship: await relationship.getVisibleText(),
-          titleElement,
-          title: await titleElement.getVisibleText(),
-          inspectElement,
-        };
-      });
-    }
-
-    async getSavedObjectsTableSummary() {
-      const table = await testSubjects.find('savedObjectsTable');
-      const rows = await table.findAllByCssSelector('tbody tr');
-
-      const summary = [];
-      for (const row of rows) {
-        const titleCell = await row.findByCssSelector('td:nth-child(3)');
-        const title = await titleCell.getVisibleText();
-
-        const viewInAppButtons = await row.findAllByCssSelector('td:nth-child(3) a');
-        const canViewInApp = Boolean(viewInAppButtons.length);
-        summary.push({
-          title,
-          canViewInApp,
-        });
-      }
-
-      return summary;
-    }
-
-    async clickSavedObjectsTableSelectAll() {
-      const checkboxSelectAll = await testSubjects.find('checkboxSelectAll');
-      await checkboxSelectAll.click();
-    }
-
-    async canSavedObjectsBeDeleted() {
-      const deleteButton = await testSubjects.find('savedObjectsManagementDelete');
-      return await deleteButton.isEnabled();
-    }
-
-    async clickSavedObjectsDelete() {
-      await testSubjects.click('savedObjectsManagementDelete');
-      await testSubjects.click('confirmModalConfirmButton');
-      await this.waitUntilSavedObjectsTableIsNotLoading();
-    }
   }
 
   return new SettingsPage();
diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts
index 9969608bd2a45..819d03d811946 100644
--- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts
+++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts
@@ -10,7 +10,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
   const esArchiver = getService('esArchiver');
   const security = getService('security');
   const testSubjects = getService('testSubjects');
-  const PageObjects = getPageObjects(['common', 'settings', 'security', 'error', 'header']);
+  const PageObjects = getPageObjects([
+    'common',
+    'settings',
+    'security',
+    'error',
+    'header',
+    'savedObjects',
+  ]);
   let version: string = '';
 
   describe('feature controls saved objects management', () => {
@@ -66,7 +73,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
         });
 
         it('shows all saved objects', async () => {
-          const objects = await PageObjects.settings.getSavedObjectsInTable();
+          const objects = await PageObjects.savedObjects.getRowTitles();
           expect(objects).to.eql([
             'Advanced Settings [6.0.0]',
             `Advanced Settings [${version}]`,
@@ -77,7 +84,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
         });
 
         it('can view all saved objects in applications', async () => {
-          const bools = await PageObjects.settings.getSavedObjectsTableSummary();
+          const bools = await PageObjects.savedObjects.getTableSummary();
           expect(bools).to.eql([
             {
               title: 'Advanced Settings [6.0.0]',
@@ -103,8 +110,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
         });
 
         it('can delete all saved objects', async () => {
-          await PageObjects.settings.clickSavedObjectsTableSelectAll();
-          const actual = await PageObjects.settings.canSavedObjectsBeDeleted();
+          await PageObjects.savedObjects.clickTableSelectAll();
+          const actual = await PageObjects.savedObjects.canBeDeleted();
           expect(actual).to.be(true);
         });
       });
@@ -185,7 +192,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
         });
 
         it('shows all saved objects', async () => {
-          const objects = await PageObjects.settings.getSavedObjectsInTable();
+          const objects = await PageObjects.savedObjects.getRowTitles();
           expect(objects).to.eql([
             'Advanced Settings [6.0.0]',
             `Advanced Settings [${version}]`,
@@ -196,7 +203,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
         });
 
         it('cannot view any saved objects in applications', async () => {
-          const bools = await PageObjects.settings.getSavedObjectsTableSummary();
+          const bools = await PageObjects.savedObjects.getTableSummary();
           expect(bools).to.eql([
             {
               title: 'Advanced Settings [6.0.0]',
@@ -222,8 +229,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
         });
 
         it(`can't delete all saved objects`, async () => {
-          await PageObjects.settings.clickSavedObjectsTableSelectAll();
-          const actual = await PageObjects.settings.canSavedObjectsBeDeleted();
+          await PageObjects.savedObjects.clickTableSelectAll();
+          const actual = await PageObjects.savedObjects.canBeDeleted();
           expect(actual).to.be(false);
         });
       });
diff --git a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts
index 69e79d63d5fd5..03596aa68dbc6 100644
--- a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts
+++ b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts
@@ -10,21 +10,17 @@ function extractCountFromSummary(str: string) {
   return parseInt(str.split('\n')[1], 10);
 }
 
-export function CopySavedObjectsToSpacePageProvider({ getService }: FtrProviderContext) {
+export function CopySavedObjectsToSpacePageProvider({
+  getService,
+  getPageObjects,
+}: FtrProviderContext) {
   const testSubjects = getService('testSubjects');
-  const browser = getService('browser');
   const find = getService('find');
+  const { savedObjects } = getPageObjects(['savedObjects']);
 
   return {
-    async searchForObject(objectName: string) {
-      const searchBox = await testSubjects.find('savedObjectSearchBar');
-      await searchBox.clearValue();
-      await searchBox.type(objectName);
-      await searchBox.pressKeys(browser.keys.ENTER);
-    },
-
     async openCopyToSpaceFlyoutForObject(objectName: string) {
-      await this.searchForObject(objectName);
+      await savedObjects.searchForObject(objectName);
 
       // Click action button to show context menu
       await find.clickByCssSelector(

From 1c9c0fc339e7b5533b7294f5204ea5c5513c98a5 Mon Sep 17 00:00:00 2001
From: MadameSheema <snootchie.boochies@gmail.com>
Date: Fri, 26 Jun 2020 19:58:51 +0200
Subject: [PATCH 06/21] renames SIEM to Security Solution (#70070)

---
 test/scripts/jenkins_xpack.sh | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh
index 067ed213c49f5..bc927b1ed7b4d 100755
--- a/test/scripts/jenkins_xpack.sh
+++ b/test/scripts/jenkins_xpack.sh
@@ -15,9 +15,9 @@ if [[ -z "$CODE_COVERAGE" ]] ; then
   echo ""
   echo ""
 
-  echo " -> Running SIEM cyclic dependency test"
+  echo " -> Running Security Solution cyclic dependency test"
   cd "$XPACK_DIR"
-  checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps
+  checks-reporter-with-killswitch "X-Pack Security Solution cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps
   echo ""
   echo ""
 

From 497dfc7af3bd300f311ab7063aca9e19159c6ac9 Mon Sep 17 00:00:00 2001
From: CJ Cenizal <cj@cenizal.com>
Date: Fri, 26 Jun 2020 10:59:59 -0700
Subject: [PATCH 07/21] Add API integration test for deleting data streams.
 (#70020)

---
 .../index_management/data_streams.ts          | 160 ++++++++++++------
 1 file changed, 106 insertions(+), 54 deletions(-)

diff --git a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts
index e1756df42ca25..74ab59f2ffdc6 100644
--- a/x-pack/test/api_integration/apis/management/index_management/data_streams.ts
+++ b/x-pack/test/api_integration/apis/management/index_management/data_streams.ts
@@ -19,79 +19,131 @@ export default function ({ getService }: FtrProviderContext) {
   const supertest = getService('supertest');
   const es = getService('legacyEs');
 
-  const createDataStream = (name: string) => {
+  const createDataStream = async (name: string) => {
     // A data stream requires an index template before it can be created.
-    return es.dataManagement
-      .saveComposableIndexTemplate({
-        name,
-        body: {
-          // We need to match the names of backing indices with this template
-          index_patterns: [name + '*'],
-          template: {
-            mappings: {
-              properties: {
-                '@timestamp': {
-                  type: 'date',
-                },
+    await es.dataManagement.saveComposableIndexTemplate({
+      name,
+      body: {
+        // We need to match the names of backing indices with this template.
+        index_patterns: [name + '*'],
+        template: {
+          mappings: {
+            properties: {
+              '@timestamp': {
+                type: 'date',
               },
             },
           },
-          data_stream: {
-            timestamp_field: '@timestamp',
-          },
         },
-      })
-      .then(() =>
-        es.dataManagement.createDataStream({
-          name,
-        })
-      );
+        data_stream: {
+          timestamp_field: '@timestamp',
+        },
+      },
+    });
+
+    await es.dataManagement.createDataStream({ name });
   };
 
-  const deleteDataStream = (name: string) => {
-    return es.dataManagement
-      .deleteDataStream({
-        name,
-      })
-      .then(() =>
-        es.dataManagement.deleteComposableIndexTemplate({
-          name,
-        })
-      );
+  const deleteComposableIndexTemplate = async (name: string) => {
+    await es.dataManagement.deleteComposableIndexTemplate({ name });
   };
 
-  // Unskip once ES snapshot has been promoted that updates the data stream response
-  describe.skip('Data streams', function () {
-    const testDataStreamName = 'test-data-stream';
+  const deleteDataStream = async (name: string) => {
+    await es.dataManagement.deleteDataStream({ name });
+    await deleteComposableIndexTemplate(name);
+  };
 
+  describe('Data streams', function () {
     describe('Get', () => {
+      const testDataStreamName = 'test-data-stream';
+
       before(async () => await createDataStream(testDataStreamName));
       after(async () => await deleteDataStream(testDataStreamName));
 
-      describe('all data streams', () => {
-        it('returns an array of data streams', async () => {
-          const { body: dataStreams } = await supertest
-            .get(`${API_BASE_PATH}/data_streams`)
-            .set('kbn-xsrf', 'xxx')
-            .expect(200);
+      it('returns an array of all data streams', async () => {
+        const { body: dataStreams } = await supertest
+          .get(`${API_BASE_PATH}/data_streams`)
+          .set('kbn-xsrf', 'xxx')
+          .expect(200);
+
+        // ES determines these values so we'll just echo them back.
+        const { name: indexName, uuid } = dataStreams[0].indices[0];
+        expect(dataStreams).to.eql([
+          {
+            name: testDataStreamName,
+            timeStampField: { name: '@timestamp', mapping: { type: 'date' } },
+            indices: [
+              {
+                name: indexName,
+                uuid,
+              },
+            ],
+            generation: 1,
+          },
+        ]);
+      });
+
+      it('returns a single data stream by ID', async () => {
+        const { body: dataStream } = await supertest
+          .get(`${API_BASE_PATH}/data_streams/${testDataStreamName}`)
+          .set('kbn-xsrf', 'xxx')
+          .expect(200);
 
-          // ES determines these values so we'll just echo them back.
-          const { name: indexName, uuid } = dataStreams[0].indices[0];
-          expect(dataStreams).to.eql([
+        // ES determines these values so we'll just echo them back.
+        const { name: indexName, uuid } = dataStream.indices[0];
+        expect(dataStream).to.eql({
+          name: testDataStreamName,
+          timeStampField: { name: '@timestamp', mapping: { type: 'date' } },
+          indices: [
             {
-              name: testDataStreamName,
-              timeStampField: { name: '@timestamp', mapping: { type: 'date' } },
-              indices: [
-                {
-                  name: indexName,
-                  uuid,
-                },
-              ],
-              generation: 1,
+              name: indexName,
+              uuid,
             },
-          ]);
+          ],
+          generation: 1,
         });
       });
     });
+
+    describe('Delete', () => {
+      const testDataStreamName1 = 'test-data-stream1';
+      const testDataStreamName2 = 'test-data-stream2';
+
+      before(async () => {
+        await Promise.all([
+          createDataStream(testDataStreamName1),
+          createDataStream(testDataStreamName2),
+        ]);
+      });
+
+      after(async () => {
+        // The Delete API only deletes the data streams, so we still need to manually delete their
+        // related index patterns to clean up.
+        await Promise.all([
+          deleteComposableIndexTemplate(testDataStreamName1),
+          deleteComposableIndexTemplate(testDataStreamName2),
+        ]);
+      });
+
+      it('deletes multiple data streams', async () => {
+        await supertest
+          .post(`${API_BASE_PATH}/delete_data_streams`)
+          .set('kbn-xsrf', 'xxx')
+          .send({
+            dataStreams: [testDataStreamName1, testDataStreamName2],
+          })
+          .expect(200);
+
+        await supertest
+          .get(`${API_BASE_PATH}/data_streams/${testDataStreamName1}`)
+          .set('kbn-xsrf', 'xxx')
+          .expect(404);
+
+        await supertest
+          .get(`${API_BASE_PATH}/data_streams/${testDataStreamName2}`)
+          .set('kbn-xsrf', 'xxx')
+          .expect(404);
+      });
+    });
   });
 }

From 8aa2206e04ff59c9ea651a5257232aa52b00ff52 Mon Sep 17 00:00:00 2001
From: Nathan Reese <reese.nathan@gmail.com>
Date: Fri, 26 Jun 2020 12:12:35 -0600
Subject: [PATCH 08/21] [Maps] remove indexing state from redux (#69765)

* [Maps] remove indexing state from redux

* add indexing step

* tslint

* tslint fixes

* tslint item

* clear preview when file changes

* review feedback

* use prevState instead of this.state in setState

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 .../plugins/maps/public/actions/ui_actions.ts |  10 +-
 .../classes/layers/layer_wizard_registry.ts   |  20 +-
 .../create_client_file_source_editor.js       |  29 ---
 .../create_client_file_source_editor.tsx      | 160 +++++++++++++
 .../upload_layer_wizard.tsx                   | 111 ++-------
 .../flyout_body/flyout_body.tsx               |  18 +-
 .../add_layer_panel/flyout_body/index.ts      |  13 +-
 .../add_layer_panel/flyout_footer/index.ts    |  32 ---
 .../add_layer_panel/flyout_footer/view.tsx    |  65 -----
 .../add_layer_panel/index.ts                  |  19 +-
 .../add_layer_panel/view.tsx                  | 225 +++++++++++-------
 x-pack/plugins/maps/public/reducers/ui.ts     |  12 -
 .../maps/public/selectors/ui_selectors.ts     |   4 +-
 .../translations/translations/ja-JP.json      |   3 -
 .../translations/translations/zh-CN.json      |   3 -
 15 files changed, 356 insertions(+), 368 deletions(-)
 delete mode 100644 x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.js
 create mode 100644 x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.tsx
 delete mode 100644 x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts
 delete mode 100644 x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx

diff --git a/x-pack/plugins/maps/public/actions/ui_actions.ts b/x-pack/plugins/maps/public/actions/ui_actions.ts
index eaf6cf42eb735..8f2650beb012d 100644
--- a/x-pack/plugins/maps/public/actions/ui_actions.ts
+++ b/x-pack/plugins/maps/public/actions/ui_actions.ts
@@ -7,7 +7,7 @@
 import { Dispatch } from 'redux';
 import { MapStoreState } from '../reducers/store';
 import { getFlyoutDisplay } from '../selectors/ui_selectors';
-import { FLYOUT_STATE, INDEXING_STAGE } from '../reducers/ui';
+import { FLYOUT_STATE } from '../reducers/ui';
 import { trackMapSettings } from './map_actions';
 import { setSelectedLayer } from './layer_actions';
 
@@ -20,7 +20,6 @@ export const SET_READ_ONLY = 'SET_READ_ONLY';
 export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS';
 export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS';
 export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS';
-export const UPDATE_INDEXING_STAGE = 'UPDATE_INDEXING_STAGE';
 
 export function exitFullScreen() {
   return {
@@ -95,10 +94,3 @@ export function hideTOCDetails(layerId: string) {
     layerId,
   };
 }
-
-export function updateIndexingStage(stage: INDEXING_STAGE | null) {
-  return {
-    type: UPDATE_INDEXING_STAGE,
-    stage,
-  };
-}
diff --git a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts
index a255ffb00e312..0eb1d2c3b222c 100644
--- a/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts
+++ b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts
@@ -10,14 +10,18 @@ import { LayerDescriptor } from '../../../common/descriptor_types';
 import { LAYER_WIZARD_CATEGORY } from '../../../common/constants';
 
 export type RenderWizardArguments = {
-  previewLayers: (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => void;
+  previewLayers: (layerDescriptors: LayerDescriptor[]) => void;
   mapColors: string[];
-  // upload arguments
-  isIndexingTriggered: boolean;
-  onRemove: () => void;
-  onIndexReady: (indexReady: boolean) => void;
-  importSuccessHandler: (indexResponses: unknown) => void;
-  importErrorHandler: (indexResponses: unknown) => void;
+  // multi-step arguments for wizards that supply 'prerequisiteSteps'
+  currentStepId: string | null;
+  enableNextBtn: () => void;
+  disableNextBtn: () => void;
+  startStepLoading: () => void;
+  stopStepLoading: () => void;
+  // Typically, next step will be triggered via user clicking next button.
+  // However, this method is made available to trigger next step manually
+  // for async task completion that triggers the next step.
+  advanceToNextStep: () => void;
 };
 
 export type LayerWizard = {
@@ -25,7 +29,7 @@ export type LayerWizard = {
   checkVisibility?: () => Promise<boolean>;
   description: string;
   icon: string;
-  isIndexingSource?: boolean;
+  prerequisiteSteps?: Array<{ id: string; label: string }>;
   renderWizard(renderWizardArguments: RenderWizardArguments): ReactElement<any>;
   title: string;
 };
diff --git a/x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.js b/x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.js
deleted file mode 100644
index f9bfc4ddde91b..0000000000000
--- a/x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.js
+++ /dev/null
@@ -1,29 +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 { getFileUploadComponent } from '../../../kibana_services';
-
-export function ClientFileCreateSourceEditor({
-  previewGeojsonFile,
-  isIndexingTriggered = false,
-  onIndexingComplete,
-  onRemove,
-  onIndexReady,
-}) {
-  const FileUpload = getFileUploadComponent();
-  return (
-    <FileUpload
-      appName={'Maps'}
-      isIndexingTriggered={isIndexingTriggered}
-      onFileUpload={previewGeojsonFile}
-      onFileRemove={onRemove}
-      onIndexReady={onIndexReady}
-      transformDetails={'geo'}
-      onIndexingComplete={onIndexingComplete}
-    />
-  );
-}
diff --git a/x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.tsx
new file mode 100644
index 0000000000000..344bdc92489e0
--- /dev/null
+++ b/x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.tsx
@@ -0,0 +1,160 @@
+/*
+ * 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, { Component } from 'react';
+import { IFieldType } from 'src/plugins/data/public';
+import {
+  ES_GEO_FIELD_TYPE,
+  DEFAULT_MAX_RESULT_WINDOW,
+  SCALING_TYPES,
+} from '../../../../common/constants';
+import { getFileUploadComponent } from '../../../kibana_services';
+// @ts-ignore
+import { GeojsonFileSource } from './geojson_file_source';
+import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+// @ts-ignore
+import { createDefaultLayerDescriptor } from '../es_search_source';
+import { RenderWizardArguments } from '../../layers/layer_wizard_registry';
+
+export const INDEX_SETUP_STEP_ID = 'INDEX_SETUP_STEP_ID';
+export const INDEXING_STEP_ID = 'INDEXING_STEP_ID';
+
+enum INDEXING_STAGE {
+  READY = 'READY',
+  TRIGGERED = 'TRIGGERED',
+  SUCCESS = 'SUCCESS',
+  ERROR = 'ERROR',
+}
+
+interface State {
+  indexingStage: INDEXING_STAGE | null;
+}
+
+export class ClientFileCreateSourceEditor extends Component<RenderWizardArguments, State> {
+  private _isMounted: boolean = false;
+
+  state = {
+    indexingStage: null,
+  };
+
+  componentDidMount() {
+    this._isMounted = true;
+  }
+
+  componentWillUnmount() {
+    this._isMounted = false;
+  }
+
+  componentDidUpdate() {
+    if (
+      this.props.currentStepId === INDEXING_STEP_ID &&
+      this.state.indexingStage === INDEXING_STAGE.READY
+    ) {
+      this.setState({ indexingStage: INDEXING_STAGE.TRIGGERED });
+      this.props.startStepLoading();
+    }
+  }
+
+  _onFileUpload = (geojsonFile: unknown, name: string) => {
+    if (!this._isMounted) {
+      return;
+    }
+
+    if (!geojsonFile) {
+      this.props.previewLayers([]);
+      return;
+    }
+
+    const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name);
+    const layerDescriptor = VectorLayer.createDescriptor(
+      { sourceDescriptor },
+      this.props.mapColors
+    );
+    this.props.previewLayers([layerDescriptor]);
+  };
+
+  _onIndexingComplete = (indexResponses: { indexDataResp: unknown; indexPatternResp: unknown }) => {
+    if (!this._isMounted) {
+      return;
+    }
+
+    this.props.advanceToNextStep();
+
+    const { indexDataResp, indexPatternResp } = indexResponses;
+
+    // @ts-ignore
+    const indexCreationFailed = !(indexDataResp && indexDataResp.success);
+    // @ts-ignore
+    const allDocsFailed = indexDataResp.failures.length === indexDataResp.docCount;
+    // @ts-ignore
+    const indexPatternCreationFailed = !(indexPatternResp && indexPatternResp.success);
+    if (indexCreationFailed || allDocsFailed || indexPatternCreationFailed) {
+      this.setState({ indexingStage: INDEXING_STAGE.ERROR });
+      return;
+    }
+
+    // @ts-ignore
+    const { fields, id: indexPatternId } = indexPatternResp;
+    const geoField = fields.find((field: IFieldType) =>
+      [ES_GEO_FIELD_TYPE.GEO_POINT as string, ES_GEO_FIELD_TYPE.GEO_SHAPE as string].includes(
+        field.type
+      )
+    );
+    if (!indexPatternId || !geoField) {
+      this.setState({ indexingStage: INDEXING_STAGE.ERROR });
+      this.props.previewLayers([]);
+    } else {
+      const esSearchSourceConfig = {
+        indexPatternId,
+        geoField: geoField.name,
+        // Only turn on bounds filter for large doc counts
+        // @ts-ignore
+        filterByMapBounds: indexDataResp.docCount > DEFAULT_MAX_RESULT_WINDOW,
+        scalingType:
+          geoField.type === ES_GEO_FIELD_TYPE.GEO_POINT
+            ? SCALING_TYPES.CLUSTERS
+            : SCALING_TYPES.LIMIT,
+      };
+      this.setState({ indexingStage: INDEXING_STAGE.SUCCESS });
+      this.props.previewLayers([
+        createDefaultLayerDescriptor(esSearchSourceConfig, this.props.mapColors),
+      ]);
+    }
+  };
+
+  // Called on file upload screen when UI state changes
+  _onIndexReady = (indexReady: boolean) => {
+    if (!this._isMounted) {
+      return;
+    }
+    this.setState({ indexingStage: indexReady ? INDEXING_STAGE.READY : null });
+    if (indexReady) {
+      this.props.enableNextBtn();
+    } else {
+      this.props.disableNextBtn();
+    }
+  };
+
+  // Called on file upload screen when upload file is changed or removed
+  _onFileRemove = () => {
+    this.props.previewLayers([]);
+  };
+
+  render() {
+    const FileUpload = getFileUploadComponent();
+    return (
+      <FileUpload
+        appName={'Maps'}
+        isIndexingTriggered={this.state.indexingStage === INDEXING_STAGE.TRIGGERED}
+        onFileUpload={this._onFileUpload}
+        onFileRemove={this._onFileRemove}
+        onIndexReady={this._onIndexReady}
+        transformDetails={'geo'}
+        onIndexingComplete={this._onIndexingComplete}
+      />
+    );
+  }
+}
diff --git a/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx
index 0a224f75b981d..05b4b18eb3ed4 100644
--- a/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx
+++ b/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx
@@ -6,102 +6,37 @@
 
 import { i18n } from '@kbn/i18n';
 import React from 'react';
-import { IFieldType } from 'src/plugins/data/public';
-import {
-  ES_GEO_FIELD_TYPE,
-  DEFAULT_MAX_RESULT_WINDOW,
-  SCALING_TYPES,
-} from '../../../../common/constants';
-// @ts-ignore
-import { createDefaultLayerDescriptor } from '../es_search_source';
 import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry';
-// @ts-ignore
-import { ClientFileCreateSourceEditor } from './create_client_file_source_editor';
-// @ts-ignore
-import { GeojsonFileSource } from './geojson_file_source';
-import { VectorLayer } from '../../layers/vector_layer/vector_layer';
+import {
+  ClientFileCreateSourceEditor,
+  INDEX_SETUP_STEP_ID,
+  INDEXING_STEP_ID,
+} from './create_client_file_source_editor';
 
 export const uploadLayerWizardConfig: LayerWizard = {
   categories: [],
-  description: i18n.translate('xpack.maps.source.geojsonFileDescription', {
+  description: i18n.translate('xpack.maps.fileUploadWizard.description', {
     defaultMessage: 'Index GeoJSON data in Elasticsearch',
   }),
   icon: 'importAction',
-  isIndexingSource: true,
-  renderWizard: ({
-    previewLayers,
-    mapColors,
-    isIndexingTriggered,
-    onRemove,
-    onIndexReady,
-    importSuccessHandler,
-    importErrorHandler,
-  }: RenderWizardArguments) => {
-    function previewGeojsonFile(geojsonFile: unknown, name: string) {
-      if (!geojsonFile) {
-        previewLayers([]);
-        return;
-      }
-      const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name);
-      const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors);
-      // TODO figure out a better way to handle passing this information back to layer_addpanel
-      previewLayers([layerDescriptor], true);
-    }
-
-    function viewIndexedData(indexResponses: {
-      indexDataResp: unknown;
-      indexPatternResp: unknown;
-    }) {
-      const { indexDataResp, indexPatternResp } = indexResponses;
-
-      // @ts-ignore
-      const indexCreationFailed = !(indexDataResp && indexDataResp.success);
-      // @ts-ignore
-      const allDocsFailed = indexDataResp.failures.length === indexDataResp.docCount;
-      // @ts-ignore
-      const indexPatternCreationFailed = !(indexPatternResp && indexPatternResp.success);
-
-      if (indexCreationFailed || allDocsFailed || indexPatternCreationFailed) {
-        importErrorHandler(indexResponses);
-        return;
-      }
-      // @ts-ignore
-      const { fields, id: indexPatternId } = indexPatternResp;
-      const geoField = fields.find((field: IFieldType) =>
-        [ES_GEO_FIELD_TYPE.GEO_POINT as string, ES_GEO_FIELD_TYPE.GEO_SHAPE as string].includes(
-          field.type
-        )
-      );
-      if (!indexPatternId || !geoField) {
-        previewLayers([]);
-      } else {
-        const esSearchSourceConfig = {
-          indexPatternId,
-          geoField: geoField.name,
-          // Only turn on bounds filter for large doc counts
-          // @ts-ignore
-          filterByMapBounds: indexDataResp.docCount > DEFAULT_MAX_RESULT_WINDOW,
-          scalingType:
-            geoField.type === ES_GEO_FIELD_TYPE.GEO_POINT
-              ? SCALING_TYPES.CLUSTERS
-              : SCALING_TYPES.LIMIT,
-        };
-        previewLayers([createDefaultLayerDescriptor(esSearchSourceConfig, mapColors)]);
-        importSuccessHandler(indexResponses);
-      }
-    }
-
-    return (
-      <ClientFileCreateSourceEditor
-        previewGeojsonFile={previewGeojsonFile}
-        isIndexingTriggered={isIndexingTriggered}
-        onIndexingComplete={viewIndexedData}
-        onRemove={onRemove}
-        onIndexReady={onIndexReady}
-      />
-    );
+  prerequisiteSteps: [
+    {
+      id: INDEX_SETUP_STEP_ID,
+      label: i18n.translate('xpack.maps.fileUploadWizard.importFileSetupLabel', {
+        defaultMessage: 'Import file',
+      }),
+    },
+    {
+      id: INDEXING_STEP_ID,
+      label: i18n.translate('xpack.maps.fileUploadWizard.indexingLabel', {
+        defaultMessage: 'Importing file',
+      }),
+    },
+  ],
+  renderWizard: (renderWizardArguments: RenderWizardArguments) => {
+    return <ClientFileCreateSourceEditor {...renderWizardArguments} />;
   },
-  title: i18n.translate('xpack.maps.source.geojsonFileTitle', {
+  title: i18n.translate('xpack.maps.fileUploadWizard.title', {
     defaultMessage: 'Upload GeoJSON',
   }),
 };
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx
index b287064938ce5..38474b84114fa 100644
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx
+++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/flyout_body.tsx
@@ -15,25 +15,27 @@ type Props = RenderWizardArguments & {
   layerWizard: LayerWizard | null;
   onClear: () => void;
   onWizardSelect: (layerWizard: LayerWizard) => void;
+  showBackButton: boolean;
 };
 
 export const FlyoutBody = (props: Props) => {
   function renderContent() {
-    if (!props.layerWizard) {
+    if (!props.layerWizard || !props.currentStepId) {
       return <LayerWizardSelect onSelect={props.onWizardSelect} />;
     }
 
     const renderWizardArgs = {
       previewLayers: props.previewLayers,
       mapColors: props.mapColors,
-      isIndexingTriggered: props.isIndexingTriggered,
-      onRemove: props.onRemove,
-      onIndexReady: props.onIndexReady,
-      importSuccessHandler: props.importSuccessHandler,
-      importErrorHandler: props.importErrorHandler,
+      currentStepId: props.currentStepId,
+      enableNextBtn: props.enableNextBtn,
+      disableNextBtn: props.disableNextBtn,
+      startStepLoading: props.startStepLoading,
+      stopStepLoading: props.stopStepLoading,
+      advanceToNextStep: props.advanceToNextStep,
     };
 
-    const backButton = props.isIndexingTriggered ? null : (
+    const backButton = props.showBackButton ? (
       <Fragment>
         <EuiButtonEmpty size="xs" flush="left" onClick={props.onClear} iconType="arrowLeft">
           <FormattedMessage
@@ -43,7 +45,7 @@ export const FlyoutBody = (props: Props) => {
         </EuiButtonEmpty>
         <EuiSpacer size="s" />
       </Fragment>
-    );
+    ) : null;
 
     return (
       <Fragment>
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/index.ts b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/index.ts
index d285c8ddebf3c..2cc35abdb53ea 100644
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/index.ts
+++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_body/index.ts
@@ -6,25 +6,14 @@
 
 import { connect } from 'react-redux';
 import { FlyoutBody } from './flyout_body';
-import { INDEXING_STAGE } from '../../../reducers/ui';
-import { updateIndexingStage } from '../../../actions';
-import { getIndexingStage } from '../../../selectors/ui_selectors';
 import { MapStoreState } from '../../../reducers/store';
 import { getMapColors } from '../../../selectors/map_selectors';
 
 function mapStateToProps(state: MapStoreState) {
   return {
-    isIndexingTriggered: getIndexingStage(state) === INDEXING_STAGE.TRIGGERED,
     mapColors: getMapColors(state),
   };
 }
 
-const mapDispatchToProps = {
-  onIndexReady: (indexReady: boolean) =>
-    indexReady ? updateIndexingStage(INDEXING_STAGE.READY) : updateIndexingStage(null),
-  importSuccessHandler: () => updateIndexingStage(INDEXING_STAGE.SUCCESS),
-  importErrorHandler: () => updateIndexingStage(INDEXING_STAGE.ERROR),
-};
-
-const connected = connect(mapStateToProps, mapDispatchToProps)(FlyoutBody);
+const connected = connect(mapStateToProps, {})(FlyoutBody);
 export { connected as FlyoutBody };
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts
deleted file mode 100644
index 470e83f2d8090..0000000000000
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/index.ts
+++ /dev/null
@@ -1,32 +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 { AnyAction, Dispatch } from 'redux';
-import { connect } from 'react-redux';
-import { FlyoutFooter } from './view';
-import { hasPreviewLayers, isLoadingPreviewLayers } from '../../../selectors/map_selectors';
-import { removePreviewLayers, updateFlyout } from '../../../actions';
-import { MapStoreState } from '../../../reducers/store';
-import { FLYOUT_STATE } from '../../../reducers/ui';
-
-function mapStateToProps(state: MapStoreState) {
-  return {
-    hasPreviewLayers: hasPreviewLayers(state),
-    isLoading: isLoadingPreviewLayers(state),
-  };
-}
-
-function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
-  return {
-    closeFlyout: () => {
-      dispatch(updateFlyout(FLYOUT_STATE.NONE));
-      dispatch<any>(removePreviewLayers());
-    },
-  };
-}
-
-const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps)(FlyoutFooter);
-export { connectedFlyOut as FlyoutFooter };
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx
deleted file mode 100644
index 2e122324c50fb..0000000000000
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/flyout_footer/view.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import {
-  EuiFlexGroup,
-  EuiFlexItem,
-  EuiFlyoutFooter,
-  EuiButtonEmpty,
-  EuiButton,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n/react';
-
-interface Props {
-  onClick: () => void;
-  showNextButton: boolean;
-  disableNextButton: boolean;
-  nextButtonText: string;
-  closeFlyout: () => void;
-  hasPreviewLayers: boolean;
-  isLoading: boolean;
-}
-
-export const FlyoutFooter = ({
-  onClick,
-  showNextButton,
-  disableNextButton,
-  nextButtonText,
-  closeFlyout,
-  hasPreviewLayers,
-  isLoading,
-}: Props) => {
-  const nextButton = showNextButton ? (
-    <EuiButton
-      data-test-subj="importFileButton"
-      disabled={disableNextButton || !hasPreviewLayers || isLoading}
-      isLoading={isLoading}
-      iconSide="right"
-      iconType={'sortRight'}
-      onClick={onClick}
-      fill
-    >
-      {nextButtonText}
-    </EuiButton>
-  ) : null;
-
-  return (
-    <EuiFlyoutFooter className="mapLayerPanel__footer">
-      <EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
-        <EuiFlexItem grow={false}>
-          <EuiButtonEmpty onClick={closeFlyout} flush="left" data-test-subj="layerAddCancelButton">
-            <FormattedMessage
-              id="xpack.maps.addLayerPanel.footer.cancelButtonLabel"
-              defaultMessage="Cancel"
-            />
-          </EuiButtonEmpty>
-        </EuiFlexItem>
-        <EuiFlexItem grow={false}>{nextButton}</EuiFlexItem>
-      </EuiFlexGroup>
-    </EuiFlyoutFooter>
-  );
-};
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/index.ts b/x-pack/plugins/maps/public/connected_components/add_layer_panel/index.ts
index 5527733f55710..8b5dc2a0e50bf 100644
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/index.ts
+++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/index.ts
@@ -7,25 +7,22 @@
 import { AnyAction, Dispatch } from 'redux';
 import { connect } from 'react-redux';
 import { AddLayerPanel } from './view';
-import { FLYOUT_STATE, INDEXING_STAGE } from '../../reducers/ui';
-import { getFlyoutDisplay, getIndexingStage } from '../../selectors/ui_selectors';
+import { FLYOUT_STATE } from '../../reducers/ui';
 import {
   addPreviewLayers,
   promotePreviewLayers,
+  removePreviewLayers,
   setFirstPreviewLayerToSelectedLayer,
   updateFlyout,
-  updateIndexingStage,
 } from '../../actions';
 import { MapStoreState } from '../../reducers/store';
 import { LayerDescriptor } from '../../../common/descriptor_types';
+import { hasPreviewLayers, isLoadingPreviewLayers } from '../../selectors/map_selectors';
 
 function mapStateToProps(state: MapStoreState) {
-  const indexingStage = getIndexingStage(state);
   return {
-    flyoutVisible: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE,
-    isIndexingTriggered: indexingStage === INDEXING_STAGE.TRIGGERED,
-    isIndexingSuccess: indexingStage === INDEXING_STAGE.SUCCESS,
-    isIndexingReady: indexingStage === INDEXING_STAGE.READY,
+    hasPreviewLayers: hasPreviewLayers(state),
+    isLoadingPreviewLayers: isLoadingPreviewLayers(state),
   };
 }
 
@@ -39,8 +36,10 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>) {
       dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL));
       dispatch<any>(promotePreviewLayers());
     },
-    setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)),
-    resetIndexing: () => dispatch(updateIndexingStage(null)),
+    closeFlyout: () => {
+      dispatch(updateFlyout(FLYOUT_STATE.NONE));
+      dispatch<any>(removePreviewLayers());
+    },
   };
 }
 
diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx
index c1b6dcc1e12a6..e2529fff66f3b 100644
--- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx
+++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx
@@ -4,141 +4,194 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import React, { Component, Fragment } from 'react';
-import { EuiTitle, EuiFlyoutHeader } from '@elastic/eui';
+import React, { Component } from 'react';
+import {
+  EuiTitle,
+  EuiFlyoutHeader,
+  EuiFlyoutFooter,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiButton,
+  EuiButtonEmpty,
+} from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
-import { FlyoutFooter } from './flyout_footer';
+import { FormattedMessage } from '@kbn/i18n/react';
 import { FlyoutBody } from './flyout_body';
 import { LayerDescriptor } from '../../../common/descriptor_types';
 import { LayerWizard } from '../../classes/layers/layer_wizard_registry';
 
+const ADD_LAYER_STEP_ID = 'ADD_LAYER_STEP_ID';
+const ADD_LAYER_STEP_LABEL = i18n.translate('xpack.maps.addLayerPanel.addLayer', {
+  defaultMessage: 'Add layer',
+});
+const SELECT_WIZARD_LABEL = ADD_LAYER_STEP_LABEL;
+
 interface Props {
-  flyoutVisible: boolean;
-  isIndexingReady: boolean;
-  isIndexingSuccess: boolean;
-  isIndexingTriggered: boolean;
   addPreviewLayers: (layerDescriptors: LayerDescriptor[]) => void;
+  closeFlyout: () => void;
+  hasPreviewLayers: boolean;
+  isLoadingPreviewLayers: boolean;
   promotePreviewLayers: () => void;
-  resetIndexing: () => void;
-  setIndexingTriggered: () => void;
 }
 
 interface State {
-  importView: boolean;
-  isIndexingSource: boolean;
-  layerImportAddReady: boolean;
+  currentStepIndex: number;
+  currentStep: { id: string; label: string } | null;
+  layerSteps: Array<{ id: string; label: string }> | null;
   layerWizard: LayerWizard | null;
+  isNextStepBtnEnabled: boolean;
+  isStepLoading: boolean;
 }
 
-export class AddLayerPanel extends Component<Props, State> {
-  private _isMounted: boolean = false;
+const INITIAL_STATE: State = {
+  currentStepIndex: 0,
+  currentStep: null,
+  layerSteps: null,
+  layerWizard: null,
+  isNextStepBtnEnabled: false,
+  isStepLoading: false,
+};
 
+export class AddLayerPanel extends Component<Props, State> {
   state = {
-    layerWizard: null,
-    isIndexingSource: false,
-    importView: false,
-    layerImportAddReady: false,
+    ...INITIAL_STATE,
   };
 
-  componentDidMount() {
-    this._isMounted = true;
-  }
-
-  componentWillUnmount() {
-    this._isMounted = false;
-  }
-
-  componentDidUpdate() {
-    if (!this.state.layerImportAddReady && this.props.isIndexingSuccess) {
-      this.setState({ layerImportAddReady: true });
-    }
-  }
+  _previewLayers = (layerDescriptors: LayerDescriptor[]) => {
+    this.props.addPreviewLayers(layerDescriptors);
+  };
 
-  _previewLayers = (layerDescriptors: LayerDescriptor[], isIndexingSource?: boolean) => {
-    if (!this._isMounted) {
-      return;
-    }
+  _clearLayerWizard = () => {
+    this.setState(INITIAL_STATE);
+    this.props.addPreviewLayers([]);
+  };
 
-    this.setState({ isIndexingSource: layerDescriptors.length ? !!isIndexingSource : false });
-    this.props.addPreviewLayers(layerDescriptors);
+  _onWizardSelect = (layerWizard: LayerWizard) => {
+    const layerSteps = [
+      ...(layerWizard.prerequisiteSteps ? layerWizard.prerequisiteSteps : []),
+      {
+        id: ADD_LAYER_STEP_ID,
+        label: ADD_LAYER_STEP_LABEL,
+      },
+    ];
+    this.setState({
+      ...INITIAL_STATE,
+      layerWizard,
+      layerSteps,
+      currentStep: layerSteps[0],
+    });
   };
 
-  _clearLayerData = ({ keepSourceType = false }: { keepSourceType: boolean }) => {
-    if (!this._isMounted) {
+  _onNext = () => {
+    if (!this.state.layerSteps) {
       return;
     }
 
-    const newState: Partial<State> = {
-      isIndexingSource: false,
-    };
-    if (!keepSourceType) {
-      newState.layerWizard = null;
-      newState.importView = false;
+    if (this.state.layerSteps.length - 1 === this.state.currentStepIndex) {
+      // last step
+      this.props.promotePreviewLayers();
+    } else {
+      this.setState((prevState) => {
+        const nextIndex = prevState.currentStepIndex + 1;
+        return {
+          currentStepIndex: nextIndex,
+          currentStep: prevState.layerSteps![nextIndex],
+          isNextStepBtnEnabled: false,
+          isStepLoading: false,
+        };
+      });
     }
-    // @ts-ignore
-    this.setState(newState);
+  };
 
-    this.props.addPreviewLayers([]);
+  _enableNextBtn = () => {
+    this.setState({ isNextStepBtnEnabled: true });
   };
 
-  _onWizardSelect = (layerWizard: LayerWizard) => {
-    this.setState({ layerWizard, importView: !!layerWizard.isIndexingSource });
+  _disableNextBtn = () => {
+    this.setState({ isNextStepBtnEnabled: false });
   };
 
-  _layerAddHandler = () => {
-    if (this.state.isIndexingSource && !this.props.isIndexingTriggered) {
-      this.props.setIndexingTriggered();
-    } else {
-      this.props.promotePreviewLayers();
-      if (this.state.importView) {
-        this.setState({
-          layerImportAddReady: false,
-        });
-        this.props.resetIndexing();
-      }
-    }
+  _startStepLoading = () => {
+    this.setState({ isStepLoading: true });
   };
 
-  render() {
-    if (!this.props.flyoutVisible) {
+  _stopStepLoading = () => {
+    this.setState({ isStepLoading: false });
+  };
+
+  _renderNextButton() {
+    if (!this.state.currentStep) {
       return null;
     }
 
-    const panelDescription =
-      this.state.layerImportAddReady || !this.state.importView
-        ? i18n.translate('xpack.maps.addLayerPanel.addLayer', {
-            defaultMessage: 'Add layer',
-          })
-        : i18n.translate('xpack.maps.addLayerPanel.importFile', {
-            defaultMessage: 'Import file',
-          });
-    const isNextBtnEnabled = this.state.importView
-      ? this.props.isIndexingReady || this.props.isIndexingSuccess
-      : true;
+    let isDisabled = !this.state.isNextStepBtnEnabled;
+    let isLoading = this.state.isStepLoading;
+    if (this.state.currentStep.id === ADD_LAYER_STEP_ID) {
+      isDisabled = !this.props.hasPreviewLayers;
+      isLoading = this.props.isLoadingPreviewLayers;
+    } else {
+      isDisabled = !this.state.isNextStepBtnEnabled;
+      isLoading = this.state.isStepLoading;
+    }
 
     return (
-      <Fragment>
+      <EuiFlexItem grow={false}>
+        <EuiButton
+          data-test-subj="importFileButton"
+          disabled={isDisabled || isLoading}
+          isLoading={isLoading}
+          iconSide="right"
+          iconType={'sortRight'}
+          onClick={this._onNext}
+          fill
+        >
+          {this.state.currentStep.label}
+        </EuiButton>
+      </EuiFlexItem>
+    );
+  }
+
+  render() {
+    return (
+      <>
         <EuiFlyoutHeader hasBorder className="mapLayerPanel__header">
           <EuiTitle size="s">
-            <h2>{panelDescription}</h2>
+            <h2>{this.state.currentStep ? this.state.currentStep.label : SELECT_WIZARD_LABEL}</h2>
           </EuiTitle>
         </EuiFlyoutHeader>
 
         <FlyoutBody
           layerWizard={this.state.layerWizard}
-          onClear={() => this._clearLayerData({ keepSourceType: false })}
-          onRemove={() => this._clearLayerData({ keepSourceType: true })}
+          onClear={this._clearLayerWizard}
           onWizardSelect={this._onWizardSelect}
           previewLayers={this._previewLayers}
+          showBackButton={!this.state.isStepLoading}
+          currentStepId={this.state.currentStep ? this.state.currentStep.id : null}
+          enableNextBtn={this._enableNextBtn}
+          disableNextBtn={this._disableNextBtn}
+          startStepLoading={this._startStepLoading}
+          stopStepLoading={this._stopStepLoading}
+          advanceToNextStep={this._onNext}
         />
 
-        <FlyoutFooter
-          showNextButton={!!this.state.layerWizard}
-          disableNextButton={!isNextBtnEnabled}
-          onClick={this._layerAddHandler}
-          nextButtonText={panelDescription}
-        />
-      </Fragment>
+        <EuiFlyoutFooter className="mapLayerPanel__footer">
+          <EuiFlexGroup justifyContent="spaceBetween" responsive={false}>
+            <EuiFlexItem grow={false}>
+              <EuiButtonEmpty
+                onClick={this.props.closeFlyout}
+                flush="left"
+                data-test-subj="layerAddCancelButton"
+              >
+                <FormattedMessage
+                  id="xpack.maps.addLayerPanel.footer.cancelButtonLabel"
+                  defaultMessage="Cancel"
+                />
+              </EuiButtonEmpty>
+            </EuiFlexItem>
+            {this._renderNextButton()}
+          </EuiFlexGroup>
+        </EuiFlyoutFooter>
+      </>
     );
   }
 }
diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts
index ff521c92568b3..2ea0798d1e768 100644
--- a/x-pack/plugins/maps/public/reducers/ui.ts
+++ b/x-pack/plugins/maps/public/reducers/ui.ts
@@ -15,7 +15,6 @@ import {
   SET_OPEN_TOC_DETAILS,
   SHOW_TOC_DETAILS,
   HIDE_TOC_DETAILS,
-  UPDATE_INDEXING_STAGE,
 } from '../actions';
 
 export enum FLYOUT_STATE {
@@ -25,13 +24,6 @@ export enum FLYOUT_STATE {
   MAP_SETTINGS_PANEL = 'MAP_SETTINGS_PANEL',
 }
 
-export enum INDEXING_STAGE {
-  READY = 'READY',
-  TRIGGERED = 'TRIGGERED',
-  SUCCESS = 'SUCCESS',
-  ERROR = 'ERROR',
-}
-
 export type MapUiState = {
   flyoutDisplay: FLYOUT_STATE;
   isFullScreen: boolean;
@@ -39,7 +31,6 @@ export type MapUiState = {
   isLayerTOCOpen: boolean;
   isSetViewOpen: boolean;
   openTOCDetails: string[];
-  importIndexingStage: INDEXING_STAGE | null;
 };
 
 export const DEFAULT_IS_LAYER_TOC_OPEN = true;
@@ -53,7 +44,6 @@ export const DEFAULT_MAP_UI_STATE = {
   // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state.
   // This also makes for easy read/write access for embeddables.
   openTOCDetails: [],
-  importIndexingStage: null,
 };
 
 // Reducer
@@ -85,8 +75,6 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) {
           return layerId !== action.layerId;
         }),
       };
-    case UPDATE_INDEXING_STAGE:
-      return { ...state, importIndexingStage: action.stage };
     default:
       return state;
   }
diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts
index 32d4beeb381d7..a87fc60ec43ea 100644
--- a/x-pack/plugins/maps/public/selectors/ui_selectors.ts
+++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts
@@ -6,7 +6,7 @@
 
 import { MapStoreState } from '../reducers/store';
 
-import { FLYOUT_STATE, INDEXING_STAGE } from '../reducers/ui';
+import { FLYOUT_STATE } from '../reducers/ui';
 
 export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay;
 export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen;
@@ -14,5 +14,3 @@ export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerT
 export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails;
 export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen;
 export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly;
-export const getIndexingStage = ({ ui }: MapStoreState): INDEXING_STAGE | null =>
-  ui.importIndexingStage;
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index ab7215ef923af..e6e9111e6b43c 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -8925,7 +8925,6 @@
     "xpack.maps.addLayerPanel.addLayer": "レイヤーを追加",
     "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "レイヤーを変更",
     "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル",
-    "xpack.maps.addLayerPanel.importFile": "ファイルのインポート",
     "xpack.maps.aggs.defaultCountLabel": "カウント",
     "xpack.maps.appTitle": "マップ",
     "xpack.maps.blendedVectorLayer.clusteredLayerName": "クラスター化 {displayName}",
@@ -9216,8 +9215,6 @@
     "xpack.maps.source.esSource.requestFailedErrorMessage": "Elasticsearch 検索リクエストに失敗。エラー: {message}",
     "xpack.maps.source.esSource.stylePropsMetaRequestDescription": "シンボル化バンドを計算するために使用されるフィールドメタデータを取得するElasticsearchリクエスト。",
     "xpack.maps.source.esSource.stylePropsMetaRequestName": "{layerName} - メタデータ",
-    "xpack.maps.source.geojsonFileDescription": "ElasticsearchでGeoJSONデータにインデックスします",
-    "xpack.maps.source.geojsonFileTitle": "GeoJSONをアップロード",
     "xpack.maps.source.kbnRegionMap.noConfigErrorMessage": "{name} の map.regionmap 構成が見つかりません",
     "xpack.maps.source.kbnRegionMap.noLayerAvailableHelptext": "ベクターレイヤーが利用できません。システム管理者に、kibana.yml で「map.regionmap」を設定するよう依頼してください。",
     "xpack.maps.source.kbnRegionMap.vectorLayerLabel": "ベクターレイヤー",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index a72b79c3ae0c7..7086b48290c7f 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -8929,7 +8929,6 @@
     "xpack.maps.addLayerPanel.addLayer": "添加图层",
     "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "更改图层",
     "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "鍙栨秷",
-    "xpack.maps.addLayerPanel.importFile": "导入文件",
     "xpack.maps.aggs.defaultCountLabel": "计数",
     "xpack.maps.appTitle": "Maps",
     "xpack.maps.blendedVectorLayer.clusteredLayerName": "集群 {displayName}",
@@ -9220,8 +9219,6 @@
     "xpack.maps.source.esSource.requestFailedErrorMessage": "Elasticsearch 搜索请求失败,错误:{message}",
     "xpack.maps.source.esSource.stylePropsMetaRequestDescription": "检索用于计算符号化带的字段元数据的 Elasticsearch 请求。",
     "xpack.maps.source.esSource.stylePropsMetaRequestName": "{layerName} - 元数据",
-    "xpack.maps.source.geojsonFileDescription": "在 Elasticsearch 索引 GeoJSON 文件",
-    "xpack.maps.source.geojsonFileTitle": "上传 GeoJSON",
     "xpack.maps.source.kbnRegionMap.noConfigErrorMessage": "找不到 {name} 的 map.regionmap 配置",
     "xpack.maps.source.kbnRegionMap.noLayerAvailableHelptext": "没有可用的矢量图层。让您的系统管理员在 kibana.yml 中设置“map.regionmap”。",
     "xpack.maps.source.kbnRegionMap.vectorLayerLabel": "矢量图层",

From e4043b736b800fade10ab80b03aeb7b0292e0540 Mon Sep 17 00:00:00 2001
From: Yara Tercero <yctercero@users.noreply.github.com>
Date: Fri, 26 Jun 2020 14:15:35 -0400
Subject: [PATCH 09/21] [SIEM][Exceptions] - Cleaned up and updated exception
 list item comment structure (#69532)

### Summary

This PR is a follow up to #68864 . That PR used a partial to differentiate between new and existing comments, this meant that comments could be updated when they shouldn't. It was decided in our discussion of exception list schemas that comments should be append only. This PR assures that's the case, but also leaves it open to editing comments (via API). It checks to make sure that users can only update their own comments.
---
 .../create_exception_list_item_schema.test.ts |  54 ++-
 .../create_exception_list_item_schema.ts      |   6 +-
 .../update_exception_list_item_schema.ts      |   8 +-
 .../common/schemas/types/comments.mock.ts     |  21 +-
 .../common/schemas/types/comments.test.ts     | 217 +++++++++
 .../lists/common/schemas/types/comments.ts    |  32 +-
 .../schemas/types/create_comments.mock.ts     |  12 +
 .../schemas/types/create_comments.test.ts     | 134 ++++++
 .../common/schemas/types/create_comments.ts   |  18 +
 .../types/default_comments_array.test.ts      |  68 +++
 .../schemas/types/default_comments_array.ts   |  29 +-
 .../default_create_comments_array.test.ts     |  66 +++
 .../types/default_create_comments_array.ts    |  28 ++
 .../default_update_comments_array.test.ts     |  70 +++
 .../types/default_update_comments_array.ts    |  28 ++
 .../lists/common/schemas/types/index.ts       |   8 +-
 .../schemas/types/update_comments.mock.ts     |  14 +
 .../schemas/types/update_comments.test.ts     | 108 +++++
 .../common/schemas/types/update_comments.ts   |  14 +
 .../lists/public/exceptions/api.test.ts       |   2 +-
 x-pack/plugins/lists/public/exceptions/api.ts |   2 +-
 .../server/saved_objects/exception_list.ts    |   6 +
 .../updates/simple_update_item.json           |   9 +-
 .../create_exception_list_item.ts             |   9 +-
 .../exception_list_client_types.ts            |   7 +-
 .../update_exception_list_item.ts             |  13 +-
 .../services/exception_lists/utils.test.ts    | 437 ++++++++++++++++++
 .../server/services/exception_lists/utils.ts  | 106 ++++-
 .../components/exceptions/helpers.test.tsx    |  10 +-
 .../common/components/exceptions/helpers.tsx  |   5 +-
 .../common/components/exceptions/types.ts     |   6 -
 .../exception_item/exception_details.test.tsx |  14 +-
 .../viewer/exception_item/index.stories.tsx   |   6 +-
 .../viewer/exception_item/index.test.tsx      |   6 +-
 .../public/lists_plugin_deps.ts               |   1 +
 35 files changed, 1437 insertions(+), 137 deletions(-)
 create mode 100644 x-pack/plugins/lists/common/schemas/types/comments.test.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/create_comments.test.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/create_comments.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/update_comments.test.ts
 create mode 100644 x-pack/plugins/lists/common/schemas/types/update_comments.ts
 create mode 100644 x-pack/plugins/lists/server/services/exception_lists/utils.test.ts

diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts
index ccafe70406ecb..34551b74d8c9f 100644
--- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts
+++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.test.ts
@@ -8,6 +8,9 @@ import { left } from 'fp-ts/lib/Either';
 import { pipe } from 'fp-ts/lib/pipeable';
 
 import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps';
+import { getCreateCommentsArrayMock } from '../types/create_comments.mock';
+import { getCommentsMock } from '../types/comments.mock';
+import { CommentsArray } from '../types';
 
 import {
   CreateExceptionListItemSchema,
@@ -26,7 +29,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual(payload);
   });
 
-  test('it should not accept an undefined for "description"', () => {
+  test('it should not validate an undefined for "description"', () => {
     const payload = getCreateExceptionListItemSchemaMock();
     delete payload.description;
     const decoded = createExceptionListItemSchema.decode(payload);
@@ -38,7 +41,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual({});
   });
 
-  test('it should not accept an undefined for "name"', () => {
+  test('it should not validate an undefined for "name"', () => {
     const payload = getCreateExceptionListItemSchemaMock();
     delete payload.name;
     const decoded = createExceptionListItemSchema.decode(payload);
@@ -50,7 +53,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual({});
   });
 
-  test('it should not accept an undefined for "type"', () => {
+  test('it should not validate an undefined for "type"', () => {
     const payload = getCreateExceptionListItemSchemaMock();
     delete payload.type;
     const decoded = createExceptionListItemSchema.decode(payload);
@@ -62,7 +65,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual({});
   });
 
-  test('it should not accept an undefined for "list_id"', () => {
+  test('it should not validate an undefined for "list_id"', () => {
     const inputPayload = getCreateExceptionListItemSchemaMock();
     delete inputPayload.list_id;
     const decoded = createExceptionListItemSchema.decode(inputPayload);
@@ -74,7 +77,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual({});
   });
 
-  test('it should accept an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
+  test('it should validate an undefined for "meta" but strip it out and generate a correct body not counting the auto generated uuid', () => {
     const payload = getCreateExceptionListItemSchemaMock();
     const outputPayload = getCreateExceptionListItemSchemaMock();
     delete payload.meta;
@@ -87,7 +90,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual(outputPayload);
   });
 
-  test('it should accept an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
+  test('it should validate an undefined for "comments" but return an array and generate a correct body not counting the auto generated uuid', () => {
     const inputPayload = getCreateExceptionListItemSchemaMock();
     const outputPayload = getCreateExceptionListItemSchemaMock();
     delete inputPayload.comments;
@@ -100,7 +103,34 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual(outputPayload);
   });
 
-  test('it should accept an undefined for "entries" but return an array', () => {
+  test('it should validate "comments" array', () => {
+    const inputPayload = {
+      ...getCreateExceptionListItemSchemaMock(),
+      comments: getCreateCommentsArrayMock(),
+    };
+    const decoded = createExceptionListItemSchema.decode(inputPayload);
+    const checked = exactCheck(inputPayload, decoded);
+    const message = pipe(checked, foldLeftRight);
+    delete (message.schema as CreateExceptionListItemSchema).item_id;
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual(inputPayload);
+  });
+
+  test('it should NOT validate "comments" with "created_at" or "created_by" values', () => {
+    const inputPayload: Omit<CreateExceptionListItemSchema, 'comments'> & {
+      comments?: CommentsArray;
+    } = {
+      ...getCreateExceptionListItemSchemaMock(),
+      comments: [getCommentsMock()],
+    };
+    const decoded = createExceptionListItemSchema.decode(inputPayload);
+    const checked = exactCheck(inputPayload, decoded);
+    const message = pipe(checked, foldLeftRight);
+    expect(getPaths(left(message.errors))).toEqual(['invalid keys "created_at,created_by"']);
+    expect(message.schema).toEqual({});
+  });
+
+  test('it should validate an undefined for "entries" but return an array', () => {
     const inputPayload = getCreateExceptionListItemSchemaMock();
     const outputPayload = getCreateExceptionListItemSchemaMock();
     delete inputPayload.entries;
@@ -113,7 +143,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual(outputPayload);
   });
 
-  test('it should accept an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => {
+  test('it should validate an undefined for "namespace_type" but return enum "single" and generate a correct body not counting the auto generated uuid', () => {
     const inputPayload = getCreateExceptionListItemSchemaMock();
     const outputPayload = getCreateExceptionListItemSchemaMock();
     delete inputPayload.namespace_type;
@@ -126,7 +156,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual(outputPayload);
   });
 
-  test('it should accept an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
+  test('it should validate an undefined for "tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
     const inputPayload = getCreateExceptionListItemSchemaMock();
     const outputPayload = getCreateExceptionListItemSchemaMock();
     delete inputPayload.tags;
@@ -139,7 +169,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual(outputPayload);
   });
 
-  test('it should accept an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
+  test('it should validate an undefined for "_tags" but return an array and generate a correct body not counting the auto generated uuid', () => {
     const inputPayload = getCreateExceptionListItemSchemaMock();
     const outputPayload = getCreateExceptionListItemSchemaMock();
     delete inputPayload._tags;
@@ -152,7 +182,7 @@ describe('create_exception_list_item_schema', () => {
     expect(message.schema).toEqual(outputPayload);
   });
 
-  test('it should accept an undefined for "item_id" and auto generate a uuid', () => {
+  test('it should validate an undefined for "item_id" and auto generate a uuid', () => {
     const inputPayload = getCreateExceptionListItemSchemaMock();
     delete inputPayload.item_id;
     const decoded = createExceptionListItemSchema.decode(inputPayload);
@@ -164,7 +194,7 @@ describe('create_exception_list_item_schema', () => {
     );
   });
 
-  test('it should accept an undefined for "item_id" and generate a correct body not counting the uuid', () => {
+  test('it should validate an undefined for "item_id" and generate a correct body not counting the uuid', () => {
     const inputPayload = getCreateExceptionListItemSchemaMock();
     delete inputPayload.item_id;
     const decoded = createExceptionListItemSchema.decode(inputPayload);
diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts
index f593b5d164035..fb452ac89576d 100644
--- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.ts
@@ -23,7 +23,7 @@ import {
   tags,
 } from '../common/schemas';
 import { Identity, RequiredKeepUndefined } from '../../types';
-import { CommentsPartialArray, DefaultCommentsPartialArray, DefaultEntryArray } from '../types';
+import { CreateCommentsArray, DefaultCreateCommentsArray, DefaultEntryArray } from '../types';
 import { EntriesArray } from '../types/entries';
 import { DefaultUuid } from '../../siem_common_deps';
 
@@ -39,7 +39,7 @@ export const createExceptionListItemSchema = t.intersection([
   t.exact(
     t.partial({
       _tags, // defaults to empty array if not set during decode
-      comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode
+      comments: DefaultCreateCommentsArray, // defaults to empty array if not set during decode
       entries: DefaultEntryArray, // defaults to empty array if not set during decode
       item_id: DefaultUuid, // defaults to GUID (uuid v4) if not set during decode
       meta, // defaults to undefined if not set during decode
@@ -63,7 +63,7 @@ export type CreateExceptionListItemSchemaDecoded = Identity<
     '_tags' | 'tags' | 'item_id' | 'entries' | 'namespace_type' | 'comments'
   > & {
     _tags: _Tags;
-    comments: CommentsPartialArray;
+    comments: CreateCommentsArray;
     tags: Tags;
     item_id: ItemId;
     entries: EntriesArray;
diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts
index c32b15fecb571..582fabdc160f9 100644
--- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts
+++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.ts
@@ -23,10 +23,10 @@ import {
 } from '../common/schemas';
 import { Identity, RequiredKeepUndefined } from '../../types';
 import {
-  CommentsPartialArray,
-  DefaultCommentsPartialArray,
   DefaultEntryArray,
+  DefaultUpdateCommentsArray,
   EntriesArray,
+  UpdateCommentsArray,
 } from '../types';
 
 export const updateExceptionListItemSchema = t.intersection([
@@ -40,7 +40,7 @@ export const updateExceptionListItemSchema = t.intersection([
   t.exact(
     t.partial({
       _tags, // defaults to empty array if not set during decode
-      comments: DefaultCommentsPartialArray, // defaults to empty array if not set during decode
+      comments: DefaultUpdateCommentsArray, // defaults to empty array if not set during decode
       entries: DefaultEntryArray, // defaults to empty array if not set during decode
       id, // defaults to undefined if not set during decode
       item_id: t.union([t.string, t.undefined]),
@@ -65,7 +65,7 @@ export type UpdateExceptionListItemSchemaDecoded = Identity<
     '_tags' | 'tags' | 'entries' | 'namespace_type' | 'comments'
   > & {
     _tags: _Tags;
-    comments: CommentsPartialArray;
+    comments: UpdateCommentsArray;
     tags: Tags;
     entries: EntriesArray;
     namespace_type: NamespaceType;
diff --git a/x-pack/plugins/lists/common/schemas/types/comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/comments.mock.ts
index ee58fafe074c7..9e56ac292f8b5 100644
--- a/x-pack/plugins/lists/common/schemas/types/comments.mock.ts
+++ b/x-pack/plugins/lists/common/schemas/types/comments.mock.ts
@@ -6,17 +6,12 @@
 
 import { DATE_NOW, USER } from '../../constants.mock';
 
-import { CommentsArray } from './comments';
+import { Comments, CommentsArray } from './comments';
 
-export const getCommentsMock = (): CommentsArray => [
-  {
-    comment: 'some comment',
-    created_at: DATE_NOW,
-    created_by: USER,
-  },
-  {
-    comment: 'some other comment',
-    created_at: DATE_NOW,
-    created_by: 'lily',
-  },
-];
+export const getCommentsMock = (): Comments => ({
+  comment: 'some old comment',
+  created_at: DATE_NOW,
+  created_by: USER,
+});
+
+export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()];
diff --git a/x-pack/plugins/lists/common/schemas/types/comments.test.ts b/x-pack/plugins/lists/common/schemas/types/comments.test.ts
new file mode 100644
index 0000000000000..29bfde03abcc8
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/comments.test.ts
@@ -0,0 +1,217 @@
+/*
+ * 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 { pipe } from 'fp-ts/lib/pipeable';
+import { left } from 'fp-ts/lib/Either';
+
+import { DATE_NOW } from '../../constants.mock';
+import { foldLeftRight, getPaths } from '../../siem_common_deps';
+
+import { getCommentsArrayMock, getCommentsMock } from './comments.mock';
+import {
+  Comments,
+  CommentsArray,
+  CommentsArrayOrUndefined,
+  comments,
+  commentsArray,
+  commentsArrayOrUndefined,
+} from './comments';
+
+describe('Comments', () => {
+  describe('comments', () => {
+    test('it should validate a comments', () => {
+      const payload = getCommentsMock();
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should validate with "updated_at" and "updated_by"', () => {
+      const payload = getCommentsMock();
+      payload.updated_at = DATE_NOW;
+      payload.updated_by = 'someone';
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should not validate when undefined', () => {
+      const payload = undefined;
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "undefined" supplied to "({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)"',
+        'Invalid value "undefined" supplied to "({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when "comment" is not a string', () => {
+      const payload: Omit<Comments, 'comment'> & { comment: string[] } = {
+        ...getCommentsMock(),
+        comment: ['some value'],
+      };
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "["some value"]" supplied to "comment"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when "created_at" is not a string', () => {
+      const payload: Omit<Comments, 'created_at'> & { created_at: number } = {
+        ...getCommentsMock(),
+        created_at: 1,
+      };
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "created_at"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when "created_by" is not a string', () => {
+      const payload: Omit<Comments, 'created_by'> & { created_by: number } = {
+        ...getCommentsMock(),
+        created_by: 1,
+      };
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "created_by"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when "updated_at" is not a string', () => {
+      const payload: Omit<Comments, 'updated_at'> & { updated_at: number } = {
+        ...getCommentsMock(),
+        updated_at: 1,
+      };
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "updated_at"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when "updated_by" is not a string', () => {
+      const payload: Omit<Comments, 'updated_by'> & { updated_by: number } = {
+        ...getCommentsMock(),
+        updated_by: 1,
+      };
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "updated_by"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should strip out extra keys', () => {
+      const payload: Comments & {
+        extraKey?: string;
+      } = getCommentsMock();
+      payload.extraKey = 'some value';
+      const decoded = comments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(getCommentsMock());
+    });
+  });
+
+  describe('commentsArray', () => {
+    test('it should validate an array of comments', () => {
+      const payload = getCommentsArrayMock();
+      const decoded = commentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should validate when a comments includes "updated_at" and "updated_by"', () => {
+      const commentsPayload = getCommentsMock();
+      commentsPayload.updated_at = DATE_NOW;
+      commentsPayload.updated_by = 'someone';
+      const payload = [{ ...commentsPayload }, ...getCommentsArrayMock()];
+      const decoded = commentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should not validate when undefined', () => {
+      const payload = undefined;
+      const decoded = commentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "undefined" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when array includes non comments types', () => {
+      const payload = ([1] as unknown) as CommentsArray;
+      const decoded = commentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+        'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+  });
+
+  describe('commentsArrayOrUndefined', () => {
+    test('it should validate an array of comments', () => {
+      const payload = getCommentsArrayMock();
+      const decoded = commentsArrayOrUndefined.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should validate when undefined', () => {
+      const payload = undefined;
+      const decoded = commentsArrayOrUndefined.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should not validate when array includes non comments types', () => {
+      const payload = ([1] as unknown) as CommentsArrayOrUndefined;
+      const decoded = commentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+        'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+  });
+});
diff --git a/x-pack/plugins/lists/common/schemas/types/comments.ts b/x-pack/plugins/lists/common/schemas/types/comments.ts
index d61608c3508f4..0ee3b05c8102f 100644
--- a/x-pack/plugins/lists/common/schemas/types/comments.ts
+++ b/x-pack/plugins/lists/common/schemas/types/comments.ts
@@ -5,36 +5,24 @@
  */
 import * as t from 'io-ts';
 
-export const comment = t.exact(
-  t.type({
-    comment: t.string,
-    created_at: t.string, // TODO: Make this into an ISO Date string check,
-    created_by: t.string,
-  })
-);
-
-export const commentsArray = t.array(comment);
-export type CommentsArray = t.TypeOf<typeof commentsArray>;
-export type Comment = t.TypeOf<typeof comment>;
-export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]);
-export type CommentsArrayOrUndefined = t.TypeOf<typeof commentsArrayOrUndefined>;
-
-export const commentPartial = t.intersection([
+export const comments = t.intersection([
   t.exact(
     t.type({
       comment: t.string,
+      created_at: t.string, // TODO: Make this into an ISO Date string check,
+      created_by: t.string,
     })
   ),
   t.exact(
     t.partial({
-      created_at: t.string, // TODO: Make this into an ISO Date string check,
-      created_by: t.string,
+      updated_at: t.string,
+      updated_by: t.string,
     })
   ),
 ]);
 
-export const commentsPartialArray = t.array(commentPartial);
-export type CommentsPartialArray = t.TypeOf<typeof commentsPartialArray>;
-export type CommentPartial = t.TypeOf<typeof commentPartial>;
-export const commentsPartialArrayOrUndefined = t.union([commentsPartialArray, t.undefined]);
-export type CommentsPartialArrayOrUndefined = t.TypeOf<typeof commentsPartialArrayOrUndefined>;
+export const commentsArray = t.array(comments);
+export type CommentsArray = t.TypeOf<typeof commentsArray>;
+export type Comments = t.TypeOf<typeof comments>;
+export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]);
+export type CommentsArrayOrUndefined = t.TypeOf<typeof commentsArrayOrUndefined>;
diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts
new file mode 100644
index 0000000000000..60a59432275ca
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/create_comments.mock.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { CreateComments, CreateCommentsArray } from './create_comments';
+
+export const getCreateCommentsMock = (): CreateComments => ({
+  comment: 'some comments',
+});
+
+export const getCreateCommentsArrayMock = (): CreateCommentsArray => [getCreateCommentsMock()];
diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts
new file mode 100644
index 0000000000000..d2680750e05e4
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/create_comments.test.ts
@@ -0,0 +1,134 @@
+/*
+ * 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 { pipe } from 'fp-ts/lib/pipeable';
+import { left } from 'fp-ts/lib/Either';
+
+import { foldLeftRight, getPaths } from '../../siem_common_deps';
+
+import { getCreateCommentsArrayMock, getCreateCommentsMock } from './create_comments.mock';
+import {
+  CreateComments,
+  CreateCommentsArray,
+  CreateCommentsArrayOrUndefined,
+  createComments,
+  createCommentsArray,
+  createCommentsArrayOrUndefined,
+} from './create_comments';
+
+describe('CreateComments', () => {
+  describe('createComments', () => {
+    test('it should validate a comments', () => {
+      const payload = getCreateCommentsMock();
+      const decoded = createComments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should not validate when undefined', () => {
+      const payload = undefined;
+      const decoded = createComments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "undefined" supplied to "{| comment: string |}"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when "comment" is not a string', () => {
+      const payload: Omit<CreateComments, 'comment'> & { comment: string[] } = {
+        ...getCreateCommentsMock(),
+        comment: ['some value'],
+      };
+      const decoded = createComments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "["some value"]" supplied to "comment"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should strip out extra keys', () => {
+      const payload: CreateComments & {
+        extraKey?: string;
+      } = getCreateCommentsMock();
+      payload.extraKey = 'some value';
+      const decoded = createComments.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(getCreateCommentsMock());
+    });
+  });
+
+  describe('createCommentsArray', () => {
+    test('it should validate an array of comments', () => {
+      const payload = getCreateCommentsArrayMock();
+      const decoded = createCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should not validate when undefined', () => {
+      const payload = undefined;
+      const decoded = createCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "undefined" supplied to "Array<{| comment: string |}>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when array includes non comments types', () => {
+      const payload = ([1] as unknown) as CreateCommentsArray;
+      const decoded = createCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "Array<{| comment: string |}>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+  });
+
+  describe('createCommentsArrayOrUndefined', () => {
+    test('it should validate an array of comments', () => {
+      const payload = getCreateCommentsArrayMock();
+      const decoded = createCommentsArrayOrUndefined.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should validate when undefined', () => {
+      const payload = undefined;
+      const decoded = createCommentsArrayOrUndefined.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should not validate when array includes non comments types', () => {
+      const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined;
+      const decoded = createCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "Array<{| comment: string |}>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+  });
+});
diff --git a/x-pack/plugins/lists/common/schemas/types/create_comments.ts b/x-pack/plugins/lists/common/schemas/types/create_comments.ts
new file mode 100644
index 0000000000000..c34419298ef93
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/create_comments.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 * as t from 'io-ts';
+
+export const createComments = t.exact(
+  t.type({
+    comment: t.string,
+  })
+);
+
+export const createCommentsArray = t.array(createComments);
+export type CreateCommentsArray = t.TypeOf<typeof createCommentsArray>;
+export type CreateComments = t.TypeOf<typeof createComments>;
+export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]);
+export type CreateCommentsArrayOrUndefined = t.TypeOf<typeof createCommentsArrayOrUndefined>;
diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts
new file mode 100644
index 0000000000000..3a4241aaec82d
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.test.ts
@@ -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 { pipe } from 'fp-ts/lib/pipeable';
+import { left } from 'fp-ts/lib/Either';
+
+import { foldLeftRight, getPaths } from '../../siem_common_deps';
+
+import { DefaultCommentsArray } from './default_comments_array';
+import { CommentsArray } from './comments';
+import { getCommentsArrayMock } from './comments.mock';
+
+describe('default_comments_array', () => {
+  test('it should validate an empty array', () => {
+    const payload: CommentsArray = [];
+    const decoded = DefaultCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual(payload);
+  });
+
+  test('it should validate an array of comments', () => {
+    const payload: CommentsArray = getCommentsArrayMock();
+    const decoded = DefaultCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual(payload);
+  });
+
+  test('it should NOT validate an array of numbers', () => {
+    const payload = [1];
+    const decoded = DefaultCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    // TODO: Known weird error formatting that is on our list to address
+    expect(getPaths(left(message.errors))).toEqual([
+      'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+      'Invalid value "1" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+    ]);
+    expect(message.schema).toEqual({});
+  });
+
+  test('it should NOT validate an array of strings', () => {
+    const payload = ['some string'];
+    const decoded = DefaultCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([
+      'Invalid value "some string" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+      'Invalid value "some string" supplied to "Array<({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>)>"',
+    ]);
+    expect(message.schema).toEqual({});
+  });
+
+  test('it should return a default array entry', () => {
+    const payload = null;
+    const decoded = DefaultCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual([]);
+  });
+});
diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts
index e824d481b3618..e8be299246ab8 100644
--- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts
+++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts
@@ -7,14 +7,9 @@
 import * as t from 'io-ts';
 import { Either } from 'fp-ts/lib/Either';
 
-import { CommentsArray, CommentsPartialArray, comment, commentPartial } from './comments';
+import { CommentsArray, comments } from './comments';
 
 export type DefaultCommentsArrayC = t.Type<CommentsArray, CommentsArray, unknown>;
-export type DefaultCommentsPartialArrayC = t.Type<
-  CommentsPartialArray,
-  CommentsPartialArray,
-  unknown
->;
 
 /**
  * Types the DefaultCommentsArray as:
@@ -26,24 +21,8 @@ export const DefaultCommentsArray: DefaultCommentsArrayC = new t.Type<
   unknown
 >(
   'DefaultCommentsArray',
-  t.array(comment).is,
-  (input, context): Either<t.Errors, CommentsArray> =>
-    input == null ? t.success([]) : t.array(comment).validate(input, context),
-  t.identity
-);
-
-/**
- * Types the DefaultCommentsPartialArray as:
- *   - If null or undefined, then a default array of type entry will be set
- */
-export const DefaultCommentsPartialArray: DefaultCommentsPartialArrayC = new t.Type<
-  CommentsPartialArray,
-  CommentsPartialArray,
-  unknown
->(
-  'DefaultCommentsPartialArray',
-  t.array(commentPartial).is,
-  (input, context): Either<t.Errors, CommentsPartialArray> =>
-    input == null ? t.success([]) : t.array(commentPartial).validate(input, context),
+  t.array(comments).is,
+  (input): Either<t.Errors, CommentsArray> =>
+    input == null ? t.success([]) : t.array(comments).decode(input),
   t.identity
 );
diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts
new file mode 100644
index 0000000000000..f5ef7d0ad96bd
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.test.ts
@@ -0,0 +1,66 @@
+/*
+ * 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 { pipe } from 'fp-ts/lib/pipeable';
+import { left } from 'fp-ts/lib/Either';
+
+import { foldLeftRight, getPaths } from '../../siem_common_deps';
+
+import { DefaultCreateCommentsArray } from './default_create_comments_array';
+import { CreateCommentsArray } from './create_comments';
+import { getCreateCommentsArrayMock } from './create_comments.mock';
+
+describe('default_create_comments_array', () => {
+  test('it should validate an empty array', () => {
+    const payload: CreateCommentsArray = [];
+    const decoded = DefaultCreateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual(payload);
+  });
+
+  test('it should validate an array of comments', () => {
+    const payload: CreateCommentsArray = getCreateCommentsArrayMock();
+    const decoded = DefaultCreateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual(payload);
+  });
+
+  test('it should NOT validate an array of numbers', () => {
+    const payload = [1];
+    const decoded = DefaultCreateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    // TODO: Known weird error formatting that is on our list to address
+    expect(getPaths(left(message.errors))).toEqual([
+      'Invalid value "1" supplied to "Array<{| comment: string |}>"',
+    ]);
+    expect(message.schema).toEqual({});
+  });
+
+  test('it should NOT validate an array of strings', () => {
+    const payload = ['some string'];
+    const decoded = DefaultCreateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([
+      'Invalid value "some string" supplied to "Array<{| comment: string |}>"',
+    ]);
+    expect(message.schema).toEqual({});
+  });
+
+  test('it should return a default array entry', () => {
+    const payload = null;
+    const decoded = DefaultCreateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual([]);
+  });
+});
diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts
new file mode 100644
index 0000000000000..51431b9c39850
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts
@@ -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 * as t from 'io-ts';
+import { Either } from 'fp-ts/lib/Either';
+
+import { CreateCommentsArray, createComments } from './create_comments';
+
+export type DefaultCreateCommentsArrayC = t.Type<CreateCommentsArray, CreateCommentsArray, unknown>;
+
+/**
+ * Types the DefaultCreateComments as:
+ *   - If null or undefined, then a default array of type entry will be set
+ */
+export const DefaultCreateCommentsArray: DefaultCreateCommentsArrayC = new t.Type<
+  CreateCommentsArray,
+  CreateCommentsArray,
+  unknown
+>(
+  'DefaultCreateComments',
+  t.array(createComments).is,
+  (input): Either<t.Errors, CreateCommentsArray> =>
+    input == null ? t.success([]) : t.array(createComments).decode(input),
+  t.identity
+);
diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts
new file mode 100644
index 0000000000000..b023e73cb9328
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.test.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { pipe } from 'fp-ts/lib/pipeable';
+import { left } from 'fp-ts/lib/Either';
+
+import { foldLeftRight, getPaths } from '../../siem_common_deps';
+
+import { DefaultUpdateCommentsArray } from './default_update_comments_array';
+import { UpdateCommentsArray } from './update_comments';
+import { getUpdateCommentsArrayMock } from './update_comments.mock';
+
+describe('default_update_comments_array', () => {
+  test('it should validate an empty array', () => {
+    const payload: UpdateCommentsArray = [];
+    const decoded = DefaultUpdateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual(payload);
+  });
+
+  test('it should validate an array of comments', () => {
+    const payload: UpdateCommentsArray = getUpdateCommentsArrayMock();
+    const decoded = DefaultUpdateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual(payload);
+  });
+
+  test('it should NOT validate an array of numbers', () => {
+    const payload = [1];
+    const decoded = DefaultUpdateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    // TODO: Known weird error formatting that is on our list to address
+    expect(getPaths(left(message.errors))).toEqual([
+      'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+      'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+      'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+    ]);
+    expect(message.schema).toEqual({});
+  });
+
+  test('it should NOT validate an array of strings', () => {
+    const payload = ['some string'];
+    const decoded = DefaultUpdateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([
+      'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+      'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+      'Invalid value "some string" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+    ]);
+    expect(message.schema).toEqual({});
+  });
+
+  test('it should return a default array entry', () => {
+    const payload = null;
+    const decoded = DefaultUpdateCommentsArray.decode(payload);
+    const message = pipe(decoded, foldLeftRight);
+
+    expect(getPaths(left(message.errors))).toEqual([]);
+    expect(message.schema).toEqual([]);
+  });
+});
diff --git a/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts
new file mode 100644
index 0000000000000..c2593826a6358
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/default_update_comments_array.ts
@@ -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 * as t from 'io-ts';
+import { Either } from 'fp-ts/lib/Either';
+
+import { UpdateCommentsArray, updateCommentsArray } from './update_comments';
+
+export type DefaultUpdateCommentsArrayC = t.Type<UpdateCommentsArray, UpdateCommentsArray, unknown>;
+
+/**
+ * Types the DefaultCommentsUpdate as:
+ *   - If null or undefined, then a default array of type entry will be set
+ */
+export const DefaultUpdateCommentsArray: DefaultUpdateCommentsArrayC = new t.Type<
+  UpdateCommentsArray,
+  UpdateCommentsArray,
+  unknown
+>(
+  'DefaultCreateComments',
+  updateCommentsArray.is,
+  (input): Either<t.Errors, UpdateCommentsArray> =>
+    input == null ? t.success([]) : updateCommentsArray.decode(input),
+  t.identity
+);
diff --git a/x-pack/plugins/lists/common/schemas/types/index.ts b/x-pack/plugins/lists/common/schemas/types/index.ts
index 97f2b0f59a5fd..16433e00f2b16 100644
--- a/x-pack/plugins/lists/common/schemas/types/index.ts
+++ b/x-pack/plugins/lists/common/schemas/types/index.ts
@@ -3,8 +3,12 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
+export * from './comments';
+export * from './create_comments';
+export * from './update_comments';
 export * from './default_comments_array';
-export * from './default_entries_array';
+export * from './default_create_comments_array';
+export * from './default_update_comments_array';
 export * from './default_namespace';
-export * from './comments';
+export * from './default_entries_array';
 export * from './entries';
diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts
new file mode 100644
index 0000000000000..3e963c2607dc5
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/update_comments.mock.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 { getCommentsMock } from './comments.mock';
+import { getCreateCommentsMock } from './create_comments.mock';
+import { UpdateCommentsArray } from './update_comments';
+
+export const getUpdateCommentsArrayMock = (): UpdateCommentsArray => [
+  getCommentsMock(),
+  getCreateCommentsMock(),
+];
diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts
new file mode 100644
index 0000000000000..7668504b031b5
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/update_comments.test.ts
@@ -0,0 +1,108 @@
+/*
+ * 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 { pipe } from 'fp-ts/lib/pipeable';
+import { left } from 'fp-ts/lib/Either';
+
+import { foldLeftRight, getPaths } from '../../siem_common_deps';
+
+import { getUpdateCommentsArrayMock } from './update_comments.mock';
+import {
+  UpdateCommentsArray,
+  UpdateCommentsArrayOrUndefined,
+  updateCommentsArray,
+  updateCommentsArrayOrUndefined,
+} from './update_comments';
+import { getCommentsMock } from './comments.mock';
+import { getCreateCommentsMock } from './create_comments.mock';
+
+describe('CommentsUpdate', () => {
+  describe('updateCommentsArray', () => {
+    test('it should validate an array of comments', () => {
+      const payload = getUpdateCommentsArrayMock();
+      const decoded = updateCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should validate an array of existing comments', () => {
+      const payload = [getCommentsMock()];
+      const decoded = updateCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should validate an array of new comments', () => {
+      const payload = [getCreateCommentsMock()];
+      const decoded = updateCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should not validate when undefined', () => {
+      const payload = undefined;
+      const decoded = updateCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "undefined" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+
+    test('it should not validate when array includes non comments types', () => {
+      const payload = ([1] as unknown) as UpdateCommentsArray;
+      const decoded = updateCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+        'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+        'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+  });
+
+  describe('updateCommentsArrayOrUndefined', () => {
+    test('it should validate an array of comments', () => {
+      const payload = getUpdateCommentsArrayMock();
+      const decoded = updateCommentsArrayOrUndefined.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should validate when undefined', () => {
+      const payload = undefined;
+      const decoded = updateCommentsArrayOrUndefined.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([]);
+      expect(message.schema).toEqual(payload);
+    });
+
+    test('it should not validate when array includes non comments types', () => {
+      const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined;
+      const decoded = updateCommentsArray.decode(payload);
+      const message = pipe(decoded, foldLeftRight);
+
+      expect(getPaths(left(message.errors))).toEqual([
+        'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+        'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+        'Invalid value "1" supplied to "Array<(({| comment: string, created_at: string, created_by: string |} & Partial<{| updated_at: string, updated_by: string |}>) | {| comment: string |})>"',
+      ]);
+      expect(message.schema).toEqual({});
+    });
+  });
+});
diff --git a/x-pack/plugins/lists/common/schemas/types/update_comments.ts b/x-pack/plugins/lists/common/schemas/types/update_comments.ts
new file mode 100644
index 0000000000000..4a21bfa363d45
--- /dev/null
+++ b/x-pack/plugins/lists/common/schemas/types/update_comments.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 * as t from 'io-ts';
+
+import { comments } from './comments';
+import { createComments } from './create_comments';
+
+export const updateCommentsArray = t.array(t.union([comments, createComments]));
+export type UpdateCommentsArray = t.TypeOf<typeof updateCommentsArray>;
+export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]);
+export type UpdateCommentsArrayOrUndefined = t.TypeOf<typeof updateCommentsArrayOrUndefined>;
diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts
index 72a689650ea2d..975641b9bebe2 100644
--- a/x-pack/plugins/lists/public/exceptions/api.test.ts
+++ b/x-pack/plugins/lists/public/exceptions/api.test.ts
@@ -250,7 +250,7 @@ describe('Exceptions Lists API', () => {
       });
       // TODO Would like to just use getExceptionListSchemaMock() here, but
       // validation returns object in different order, making the strings not match
-      expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
+      expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
         body: JSON.stringify(payload),
         method: 'PUT',
         signal: abortCtrl.signal,
diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts
index 2ab7695d8c17c..a581cfd08ecc1 100644
--- a/x-pack/plugins/lists/public/exceptions/api.ts
+++ b/x-pack/plugins/lists/public/exceptions/api.ts
@@ -176,7 +176,7 @@ export const updateExceptionListItem = async ({
 
   if (validatedRequest != null) {
     try {
-      const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_URL, {
+      const response = await http.fetch<ExceptionListItemSchema>(EXCEPTION_LIST_ITEM_URL, {
         body: JSON.stringify(listItem),
         method: 'PUT',
         signal,
diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts
index 57bc63e6f7e35..fc04c5e278d64 100644
--- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts
+++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts
@@ -77,6 +77,12 @@ export const exceptionListItemMapping: SavedObjectsType['mappings'] = {
         created_by: {
           type: 'keyword',
         },
+        updated_at: {
+          type: 'keyword',
+        },
+        updated_by: {
+          type: 'keyword',
+        },
       },
     },
     entries: {
diff --git a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json
index 33c9303c7b523..08bd95b7d124c 100644
--- a/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json
+++ b/x-pack/plugins/lists/server/scripts/exception_lists/updates/simple_update_item.json
@@ -5,14 +5,7 @@
   "type": "simple",
   "description": "This is a sample change here this list",
   "name": "Sample Endpoint Exception List update change",
-  "comments": [
-    {
-      "comment": "this was an old comment.",
-      "created_by": "lily",
-      "created_at": "2020-04-20T15:25:31.830Z"
-    },
-    { "comment": "this is a newly added comment" }
-  ],
+  "comments": [{ "comment": "this is a newly added comment" }],
   "entries": [
     {
       "field": "event.category",
diff --git a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts
index 22a9fbcfb53af..a84283aeabbba 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/create_exception_list_item.ts
@@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server';
 import uuid from 'uuid';
 
 import {
-  CommentsPartialArray,
+  CreateCommentsArray,
   Description,
   EntriesArray,
   ExceptionListItemSchema,
@@ -25,13 +25,13 @@ import {
 
 import {
   getSavedObjectType,
-  transformComments,
+  transformCreateCommentsToComments,
   transformSavedObjectToExceptionListItem,
 } from './utils';
 
 interface CreateExceptionListItemOptions {
   _tags: _Tags;
-  comments: CommentsPartialArray;
+  comments: CreateCommentsArray;
   listId: ListId;
   itemId: ItemId;
   savedObjectsClient: SavedObjectsClientContract;
@@ -64,9 +64,10 @@ export const createExceptionListItem = async ({
 }: CreateExceptionListItemOptions): Promise<ExceptionListItemSchema> => {
   const savedObjectType = getSavedObjectType({ namespaceType });
   const dateNow = new Date().toISOString();
+  const transformedComments = transformCreateCommentsToComments({ comments, user });
   const savedObject = await savedObjectsClient.create<ExceptionListSoSchema>(savedObjectType, {
     _tags,
-    comments: transformComments({ comments, user }),
+    comments: transformedComments,
     created_at: dateNow,
     created_by: user,
     description,
diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
index 03f5de516561b..203d32911a6df 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts
@@ -7,7 +7,7 @@
 import { SavedObjectsClientContract } from 'kibana/server';
 
 import {
-  CommentsPartialArray,
+  CreateCommentsArray,
   Description,
   DescriptionOrUndefined,
   EntriesArray,
@@ -30,6 +30,7 @@ import {
   SortOrderOrUndefined,
   Tags,
   TagsOrUndefined,
+  UpdateCommentsArray,
   _Tags,
   _TagsOrUndefined,
 } from '../../../common/schemas';
@@ -88,7 +89,7 @@ export interface GetExceptionListItemOptions {
 
 export interface CreateExceptionListItemOptions {
   _tags: _Tags;
-  comments: CommentsPartialArray;
+  comments: CreateCommentsArray;
   entries: EntriesArray;
   itemId: ItemId;
   listId: ListId;
@@ -102,7 +103,7 @@ export interface CreateExceptionListItemOptions {
 
 export interface UpdateExceptionListItemOptions {
   _tags: _TagsOrUndefined;
-  comments: CommentsPartialArray;
+  comments: UpdateCommentsArray;
   entries: EntriesArrayOrUndefined;
   id: IdOrUndefined;
   itemId: ItemIdOrUndefined;
diff --git a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts
index 7ca9bfd83ab64..5578063fd9b6c 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/update_exception_list_item.ts
@@ -7,7 +7,6 @@
 import { SavedObjectsClientContract } from 'kibana/server';
 
 import {
-  CommentsPartialArray,
   DescriptionOrUndefined,
   EntriesArrayOrUndefined,
   ExceptionListItemSchema,
@@ -19,19 +18,20 @@ import {
   NameOrUndefined,
   NamespaceType,
   TagsOrUndefined,
+  UpdateCommentsArrayOrUndefined,
   _TagsOrUndefined,
 } from '../../../common/schemas';
 
 import {
   getSavedObjectType,
-  transformComments,
   transformSavedObjectUpdateToExceptionListItem,
+  transformUpdateCommentsToComments,
 } from './utils';
 import { getExceptionListItem } from './get_exception_list_item';
 
 interface UpdateExceptionListItemOptions {
   id: IdOrUndefined;
-  comments: CommentsPartialArray;
+  comments: UpdateCommentsArrayOrUndefined;
   _tags: _TagsOrUndefined;
   name: NameOrUndefined;
   description: DescriptionOrUndefined;
@@ -71,12 +71,17 @@ export const updateExceptionListItem = async ({
   if (exceptionListItem == null) {
     return null;
   } else {
+    const transformedComments = transformUpdateCommentsToComments({
+      comments,
+      existingComments: exceptionListItem.comments,
+      user,
+    });
     const savedObject = await savedObjectsClient.update<ExceptionListSoSchema>(
       savedObjectType,
       exceptionListItem.id,
       {
         _tags,
-        comments: transformComments({ comments, user }),
+        comments: transformedComments,
         description,
         entries,
         meta,
diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts
new file mode 100644
index 0000000000000..9cc2aacd88458
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/exception_lists/utils.test.ts
@@ -0,0 +1,437 @@
+/*
+ * 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 sinon from 'sinon';
+import moment from 'moment';
+
+import { DATE_NOW, USER } from '../../../common/constants.mock';
+
+import {
+  isCommentEqual,
+  transformCreateCommentsToComments,
+  transformUpdateComments,
+  transformUpdateCommentsToComments,
+} from './utils';
+
+describe('utils', () => {
+  const anchor = '2020-06-17T20:34:51.337Z';
+  const unix = moment(anchor).valueOf();
+  let clock: sinon.SinonFakeTimers;
+
+  beforeEach(() => {
+    clock = sinon.useFakeTimers(unix);
+  });
+
+  afterEach(() => {
+    clock.restore();
+  });
+
+  describe('#transformUpdateCommentsToComments', () => {
+    test('it returns empty array if "comments" is undefined and no comments exist', () => {
+      const comments = transformUpdateCommentsToComments({
+        comments: undefined,
+        existingComments: [],
+        user: 'lily',
+      });
+
+      expect(comments).toEqual([]);
+    });
+
+    test('it formats newly added comments', () => {
+      const comments = transformUpdateCommentsToComments({
+        comments: [
+          { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          { comment: 'Im a new comment' },
+        ],
+        existingComments: [
+          { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+        ],
+        user: 'lily',
+      });
+
+      expect(comments).toEqual([
+        {
+          comment: 'Im an old comment',
+          created_at: anchor,
+          created_by: 'lily',
+        },
+        {
+          comment: 'Im a new comment',
+          created_at: anchor,
+          created_by: 'lily',
+        },
+      ]);
+    });
+
+    test('it formats multiple newly added comments', () => {
+      const comments = transformUpdateCommentsToComments({
+        comments: [
+          { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          { comment: 'Im a new comment' },
+          { comment: 'Im another new comment' },
+        ],
+        existingComments: [
+          { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+        ],
+        user: 'lily',
+      });
+
+      expect(comments).toEqual([
+        {
+          comment: 'Im an old comment',
+          created_at: anchor,
+          created_by: 'lily',
+        },
+        {
+          comment: 'Im a new comment',
+          created_at: anchor,
+          created_by: 'lily',
+        },
+        {
+          comment: 'Im another new comment',
+          created_at: anchor,
+          created_by: 'lily',
+        },
+      ]);
+    });
+
+    test('it should not throw if comments match existing comments', () => {
+      const comments = transformUpdateCommentsToComments({
+        comments: [{ comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' }],
+        existingComments: [
+          { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+        ],
+        user: 'lily',
+      });
+
+      expect(comments).toEqual([
+        {
+          comment: 'Im an old comment',
+          created_at: anchor,
+          created_by: 'lily',
+        },
+      ]);
+    });
+
+    test('it does not throw if user tries to update one of their own existing comments', () => {
+      const comments = transformUpdateCommentsToComments({
+        comments: [
+          {
+            comment: 'Im an old comment that is trying to be updated',
+            created_at: DATE_NOW,
+            created_by: 'lily',
+          },
+        ],
+        existingComments: [
+          { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+        ],
+        user: 'lily',
+      });
+
+      expect(comments).toEqual([
+        {
+          comment: 'Im an old comment that is trying to be updated',
+          created_at: DATE_NOW,
+          created_by: 'lily',
+          updated_at: anchor,
+          updated_by: 'lily',
+        },
+      ]);
+    });
+
+    test('it throws an error if user tries to update their comment, without passing in the "created_at" and "created_by" properties', () => {
+      expect(() =>
+        transformUpdateCommentsToComments({
+          comments: [
+            {
+              comment: 'Im an old comment that is trying to be updated',
+            },
+          ],
+          existingComments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          ],
+          user: 'lily',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"When trying to update a comment, \\"created_at\\" and \\"created_by\\" must be present"`
+      );
+    });
+
+    test('it throws an error if user tries to delete comments', () => {
+      expect(() =>
+        transformUpdateCommentsToComments({
+          comments: [],
+          existingComments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          ],
+          user: 'lily',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"Comments cannot be deleted, only new comments may be added"`
+      );
+    });
+
+    test('it throws if user tries to update existing comment timestamp', () => {
+      expect(() =>
+        transformUpdateCommentsToComments({
+          comments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }],
+          existingComments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          ],
+          user: 'bane',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`);
+    });
+
+    test('it throws if user tries to update existing comment author', () => {
+      expect(() =>
+        transformUpdateCommentsToComments({
+          comments: [{ comment: 'Im an old comment', created_at: anchor, created_by: 'lily' }],
+          existingComments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'me!' },
+          ],
+          user: 'bane',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`);
+    });
+
+    test('it throws if user tries to update an existing comment that is not their own', () => {
+      expect(() =>
+        transformUpdateCommentsToComments({
+          comments: [
+            {
+              comment: 'Im an old comment that is trying to be updated',
+              created_at: DATE_NOW,
+              created_by: 'lily',
+            },
+          ],
+          existingComments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          ],
+          user: 'bane',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`);
+    });
+
+    test('it throws if user tries to update order of comments', () => {
+      expect(() =>
+        transformUpdateCommentsToComments({
+          comments: [
+            { comment: 'Im a new comment' },
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          ],
+          existingComments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          ],
+          user: 'lily',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(
+        `"When trying to update a comment, \\"created_at\\" and \\"created_by\\" must be present"`
+      );
+    });
+
+    test('it throws an error if user tries to add comment formatted as existing comment when none yet exist', () => {
+      expect(() =>
+        transformUpdateCommentsToComments({
+          comments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+            { comment: 'Im a new comment' },
+          ],
+          existingComments: [],
+          user: 'lily',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(`"Only new comments may be added"`);
+    });
+
+    test('it throws if empty comment exists', () => {
+      expect(() =>
+        transformUpdateCommentsToComments({
+          comments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+            { comment: '    ' },
+          ],
+          existingComments: [
+            { comment: 'Im an old comment', created_at: DATE_NOW, created_by: 'lily' },
+          ],
+          user: 'lily',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(`"Empty comments not allowed"`);
+    });
+  });
+
+  describe('#transformCreateCommentsToComments', () => {
+    test('it returns "undefined" if "comments" is "undefined"', () => {
+      const comments = transformCreateCommentsToComments({
+        comments: undefined,
+        user: 'lily',
+      });
+
+      expect(comments).toBeUndefined();
+    });
+
+    test('it formats newly added comments', () => {
+      const comments = transformCreateCommentsToComments({
+        comments: [{ comment: 'Im a new comment' }, { comment: 'Im another new comment' }],
+        user: 'lily',
+      });
+
+      expect(comments).toEqual([
+        {
+          comment: 'Im a new comment',
+          created_at: anchor,
+          created_by: 'lily',
+        },
+        {
+          comment: 'Im another new comment',
+          created_at: anchor,
+          created_by: 'lily',
+        },
+      ]);
+    });
+
+    test('it throws an error if user tries to add an empty comment', () => {
+      expect(() =>
+        transformCreateCommentsToComments({
+          comments: [{ comment: '   ' }],
+          user: 'lily',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(`"Empty comments not allowed"`);
+    });
+  });
+
+  describe('#transformUpdateComments', () => {
+    test('it updates comment and adds "updated_at" and "updated_by"', () => {
+      const comments = transformUpdateComments({
+        comment: {
+          comment: 'Im an old comment that is trying to be updated',
+          created_at: DATE_NOW,
+          created_by: 'lily',
+        },
+        existingComment: {
+          comment: 'Im an old comment',
+          created_at: DATE_NOW,
+          created_by: 'lily',
+        },
+        user: 'lily',
+      });
+
+      expect(comments).toEqual({
+        comment: 'Im an old comment that is trying to be updated',
+        created_at: '2020-04-20T15:25:31.830Z',
+        created_by: 'lily',
+        updated_at: anchor,
+        updated_by: 'lily',
+      });
+    });
+
+    test('it throws if user tries to update an existing comment that is not their own', () => {
+      expect(() =>
+        transformUpdateComments({
+          comment: {
+            comment: 'Im an old comment that is trying to be updated',
+            created_at: DATE_NOW,
+            created_by: 'lily',
+          },
+          existingComment: {
+            comment: 'Im an old comment',
+            created_at: DATE_NOW,
+            created_by: 'lily',
+          },
+          user: 'bane',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(`"Not authorized to edit others comments"`);
+    });
+
+    test('it throws if user tries to update an existing comments timestamp', () => {
+      expect(() =>
+        transformUpdateComments({
+          comment: {
+            comment: 'Im an old comment that is trying to be updated',
+            created_at: anchor,
+            created_by: 'lily',
+          },
+          existingComment: {
+            comment: 'Im an old comment',
+            created_at: DATE_NOW,
+            created_by: 'lily',
+          },
+          user: 'lily',
+        })
+      ).toThrowErrorMatchingInlineSnapshot(`"Unable to update comment"`);
+    });
+  });
+
+  describe('#isCommentEqual', () => {
+    test('it returns false if "comment" values differ', () => {
+      const result = isCommentEqual(
+        {
+          comment: 'some old comment',
+          created_at: DATE_NOW,
+          created_by: USER,
+        },
+        {
+          comment: 'some older comment',
+          created_at: DATE_NOW,
+          created_by: USER,
+        }
+      );
+
+      expect(result).toBeFalsy();
+    });
+
+    test('it returns false if "created_at" values differ', () => {
+      const result = isCommentEqual(
+        {
+          comment: 'some old comment',
+          created_at: DATE_NOW,
+          created_by: USER,
+        },
+        {
+          comment: 'some old comment',
+          created_at: anchor,
+          created_by: USER,
+        }
+      );
+
+      expect(result).toBeFalsy();
+    });
+
+    test('it returns false if "created_by" values differ', () => {
+      const result = isCommentEqual(
+        {
+          comment: 'some old comment',
+          created_at: DATE_NOW,
+          created_by: USER,
+        },
+        {
+          comment: 'some old comment',
+          created_at: DATE_NOW,
+          created_by: 'lily',
+        }
+      );
+
+      expect(result).toBeFalsy();
+    });
+
+    test('it returns true if comment values are equivalent', () => {
+      const result = isCommentEqual(
+        {
+          comment: 'some old comment',
+          created_at: DATE_NOW,
+          created_by: USER,
+        },
+        {
+          created_at: DATE_NOW,
+          created_by: USER,
+          // Disabling to assure that order doesn't matter
+          // eslint-disable-next-line sort-keys
+          comment: 'some old comment',
+        }
+      );
+
+      expect(result).toBeTruthy();
+    });
+  });
+});
diff --git a/x-pack/plugins/lists/server/services/exception_lists/utils.ts b/x-pack/plugins/lists/server/services/exception_lists/utils.ts
index 5690a42bed87e..14b5309f67dc9 100644
--- a/x-pack/plugins/lists/server/services/exception_lists/utils.ts
+++ b/x-pack/plugins/lists/server/services/exception_lists/utils.ts
@@ -6,15 +6,21 @@
 
 import { SavedObject, SavedObjectsFindResponse, SavedObjectsUpdateResponse } from 'kibana/server';
 
+import { ErrorWithStatusCode } from '../../error_with_status_code';
 import {
+  Comments,
+  CommentsArray,
   CommentsArrayOrUndefined,
-  CommentsPartialArrayOrUndefined,
+  CreateComments,
+  CreateCommentsArrayOrUndefined,
   ExceptionListItemSchema,
   ExceptionListSchema,
   ExceptionListSoSchema,
   FoundExceptionListItemSchema,
   FoundExceptionListSchema,
   NamespaceType,
+  UpdateCommentsArrayOrUndefined,
+  comments as commentsSchema,
 } from '../../../common/schemas';
 import {
   SavedObjectType,
@@ -251,21 +257,103 @@ export const transformSavedObjectsToFoundExceptionList = ({
   };
 };
 
-export const transformComments = ({
+/*
+ * Determines whether two comments are equal, this is a very
+ * naive implementation, not meant to be used for deep equality of complex objects
+ */
+export const isCommentEqual = (commentA: Comments, commentB: Comments): boolean => {
+  const a = Object.values(commentA).sort().join();
+  const b = Object.values(commentB).sort().join();
+
+  return a === b;
+};
+
+export const transformUpdateCommentsToComments = ({
+  comments,
+  existingComments,
+  user,
+}: {
+  comments: UpdateCommentsArrayOrUndefined;
+  existingComments: CommentsArray;
+  user: string;
+}): CommentsArray => {
+  const newComments = comments ?? [];
+
+  if (newComments.length < existingComments.length) {
+    throw new ErrorWithStatusCode(
+      'Comments cannot be deleted, only new comments may be added',
+      403
+    );
+  } else {
+    return newComments.flatMap((c, index) => {
+      const existingComment = existingComments[index];
+
+      if (commentsSchema.is(existingComment) && !commentsSchema.is(c)) {
+        throw new ErrorWithStatusCode(
+          'When trying to update a comment, "created_at" and "created_by" must be present',
+          403
+        );
+      } else if (commentsSchema.is(c) && existingComment == null) {
+        throw new ErrorWithStatusCode('Only new comments may be added', 403);
+      } else if (
+        commentsSchema.is(c) &&
+        existingComment != null &&
+        !isCommentEqual(c, existingComment)
+      ) {
+        return transformUpdateComments({ comment: c, existingComment, user });
+      } else {
+        return transformCreateCommentsToComments({ comments: [c], user }) ?? [];
+      }
+    });
+  }
+};
+
+export const transformUpdateComments = ({
+  comment,
+  existingComment,
+  user,
+}: {
+  comment: Comments;
+  existingComment: Comments;
+  user: string;
+}): Comments => {
+  if (comment.created_by !== user) {
+    // existing comment is being edited, can only be edited by author
+    throw new ErrorWithStatusCode('Not authorized to edit others comments', 401);
+  } else if (existingComment.created_at !== comment.created_at) {
+    throw new ErrorWithStatusCode('Unable to update comment', 403);
+  } else if (comment.comment.trim().length === 0) {
+    throw new ErrorWithStatusCode('Empty comments not allowed', 403);
+  } else {
+    const dateNow = new Date().toISOString();
+
+    return {
+      ...comment,
+      updated_at: dateNow,
+      updated_by: user,
+    };
+  }
+};
+
+export const transformCreateCommentsToComments = ({
   comments,
   user,
 }: {
-  comments: CommentsPartialArrayOrUndefined;
+  comments: CreateCommentsArrayOrUndefined;
   user: string;
 }): CommentsArrayOrUndefined => {
   const dateNow = new Date().toISOString();
   if (comments != null) {
-    return comments.map((comment) => {
-      return {
-        comment: comment.comment,
-        created_at: comment.created_at ?? dateNow,
-        created_by: comment.created_by ?? user,
-      };
+    return comments.map((c: CreateComments) => {
+      if (c.comment.trim().length === 0) {
+        throw new ErrorWithStatusCode('Empty comments not allowed', 403);
+      } else {
+        return {
+          comment: c.comment,
+          created_at: dateNow,
+          created_by: user,
+        };
+      }
     });
   } else {
     return comments;
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
index 244819080c93d..b936aea047690 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx
@@ -37,7 +37,7 @@ import {
   getEntryMatchAnyMock,
   getEntriesArrayMock,
 } from '../../../../../lists/common/schemas/types/entries.mock';
-import { getCommentsMock } from '../../../../../lists/common/schemas/types/comments.mock';
+import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/comments.mock';
 
 describe('Exception helpers', () => {
   beforeEach(() => {
@@ -382,7 +382,7 @@ describe('Exception helpers', () => {
 
   describe('#getFormattedComments', () => {
     test('it returns formatted comment object with username and timestamp', () => {
-      const payload = getCommentsMock();
+      const payload = getCommentsArrayMock();
       const result = getFormattedComments(payload);
 
       expect(result[0].username).toEqual('some user');
@@ -390,7 +390,7 @@ describe('Exception helpers', () => {
     });
 
     test('it returns formatted timeline icon with comment users initial', () => {
-      const payload = getCommentsMock();
+      const payload = getCommentsArrayMock();
       const result = getFormattedComments(payload);
 
       const wrapper = mount<React.ReactElement>(result[0].timelineIcon as React.ReactElement);
@@ -399,12 +399,12 @@ describe('Exception helpers', () => {
     });
 
     test('it returns comment text', () => {
-      const payload = getCommentsMock();
+      const payload = getCommentsArrayMock();
       const result = getFormattedComments(payload);
 
       const wrapper = mount<React.ReactElement>(result[0].children as React.ReactElement);
 
-      expect(wrapper.text()).toEqual('some comment');
+      expect(wrapper.text()).toEqual('some old comment');
     });
   });
 });
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
index 164940db619f9..ae4131f9f62c2 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx
@@ -10,9 +10,10 @@ import { capitalize } from 'lodash';
 import moment from 'moment';
 
 import * as i18n from './translations';
-import { FormattedEntry, OperatorOption, DescriptionListItem, Comment } from './types';
+import { FormattedEntry, OperatorOption, DescriptionListItem } from './types';
 import { EXCEPTION_OPERATORS, isOperator } from './operators';
 import {
+  CommentsArray,
   Entry,
   EntriesArray,
   ExceptionListItemSchema,
@@ -183,7 +184,7 @@ export const getDescriptionListContent = (
  *
  * @param comments ExceptionItem.comments
  */
-export const getFormattedComments = (comments: Comment[]): EuiCommentProps[] =>
+export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] =>
   comments.map((comment) => ({
     username: comment.created_by,
     timestamp: moment(comment.created_at).format('on MMM Do YYYY @ HH:mm:ss'),
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
index 24c328462ce2f..ed2be64b4430f 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts
@@ -26,12 +26,6 @@ export interface DescriptionListItem {
   description: NonNullable<ReactNode>;
 }
 
-export interface Comment {
-  created_by: string;
-  created_at: string;
-  comment: string;
-}
-
 export enum ExceptionListType {
   DETECTION_ENGINE = 'detection',
   ENDPOINT = 'endpoint',
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx
index 3ea8507d82a15..f5b34b7838d25 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx
@@ -12,7 +12,7 @@ import moment from 'moment-timezone';
 
 import { ExceptionDetails } from './exception_details';
 import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
-import { getCommentsMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
+import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
 
 describe('ExceptionDetails', () => {
   beforeEach(() => {
@@ -42,7 +42,7 @@ describe('ExceptionDetails', () => {
 
   test('it renders comments button if comments exist', () => {
     const exceptionItem = getExceptionListItemSchemaMock();
-    exceptionItem.comments = getCommentsMock();
+    exceptionItem.comments = getCommentsArrayMock();
     const wrapper = mount(
       <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
         <ExceptionDetails
@@ -60,7 +60,7 @@ describe('ExceptionDetails', () => {
 
   test('it renders correct number of comments', () => {
     const exceptionItem = getExceptionListItemSchemaMock();
-    exceptionItem.comments = [getCommentsMock()[0]];
+    exceptionItem.comments = [getCommentsArrayMock()[0]];
     const wrapper = mount(
       <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
         <ExceptionDetails
@@ -78,7 +78,7 @@ describe('ExceptionDetails', () => {
 
   test('it renders comments plural if more than one', () => {
     const exceptionItem = getExceptionListItemSchemaMock();
-    exceptionItem.comments = getCommentsMock();
+    exceptionItem.comments = getCommentsArrayMock();
     const wrapper = mount(
       <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
         <ExceptionDetails
@@ -96,7 +96,7 @@ describe('ExceptionDetails', () => {
 
   test('it renders comments show text if "showComments" is false', () => {
     const exceptionItem = getExceptionListItemSchemaMock();
-    exceptionItem.comments = getCommentsMock();
+    exceptionItem.comments = getCommentsArrayMock();
     const wrapper = mount(
       <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
         <ExceptionDetails
@@ -114,7 +114,7 @@ describe('ExceptionDetails', () => {
 
   test('it renders comments hide text if "showComments" is true', () => {
     const exceptionItem = getExceptionListItemSchemaMock();
-    exceptionItem.comments = getCommentsMock();
+    exceptionItem.comments = getCommentsArrayMock();
     const wrapper = mount(
       <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
         <ExceptionDetails
@@ -133,7 +133,7 @@ describe('ExceptionDetails', () => {
   test('it invokes "onCommentsClick" when comments button clicked', () => {
     const mockOnCommentsClick = jest.fn();
     const exceptionItem = getExceptionListItemSchemaMock();
-    exceptionItem.comments = getCommentsMock();
+    exceptionItem.comments = getCommentsArrayMock();
     const wrapper = mount(
       <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
         <ExceptionDetails
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx
index b5f18feb48502..56b029aaee81e 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx
@@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
 
 import { ExceptionItem } from './';
 import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
-import { getCommentsMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
+import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
 
 addDecorator((storyFn) => (
   <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider>
@@ -68,7 +68,7 @@ storiesOf('Components|ExceptionItem', module)
     const payload = getExceptionListItemSchemaMock();
     payload._tags = [];
     payload.description = '';
-    payload.comments = getCommentsMock();
+    payload.comments = getCommentsArrayMock();
     payload.entries = [
       {
         field: 'actingProcess.file.signer',
@@ -106,7 +106,7 @@ storiesOf('Components|ExceptionItem', module)
   })
   .add('with everything', () => {
     const payload = getExceptionListItemSchemaMock();
-    payload.comments = getCommentsMock();
+    payload.comments = getCommentsArrayMock();
     return (
       <ExceptionItem
         loadingItemIds={[]}
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx
index 6541939527596..0e2908fc34232 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx
@@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
 
 import { ExceptionItem } from './';
 import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
-import { getCommentsMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
+import { getCommentsArrayMock } from '../../../../../../../lists/common/schemas/types/comments.mock';
 
 describe('ExceptionItem', () => {
   it('it renders ExceptionDetails and ExceptionEntries', () => {
@@ -83,7 +83,7 @@ describe('ExceptionItem', () => {
   it('it renders comment accordion closed to begin with', () => {
     const mockOnDeleteException = jest.fn();
     const exceptionItem = getExceptionListItemSchemaMock();
-    exceptionItem.comments = getCommentsMock();
+    exceptionItem.comments = getCommentsArrayMock();
     const wrapper = mount(
       <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
         <ExceptionItem
@@ -102,7 +102,7 @@ describe('ExceptionItem', () => {
   it('it renders comment accordion open when showComments is true', () => {
     const mockOnDeleteException = jest.fn();
     const exceptionItem = getExceptionListItemSchemaMock();
-    exceptionItem.comments = getCommentsMock();
+    exceptionItem.comments = getCommentsArrayMock();
     const wrapper = mount(
       <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
         <ExceptionItem
diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts
index 575ff26330a46..f3a724a755a48 100644
--- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts
+++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts
@@ -15,6 +15,7 @@ export {
   UseExceptionListSuccess,
 } from '../../lists/public';
 export {
+  CommentsArray,
   ExceptionListSchema,
   ExceptionListItemSchema,
   Entry,

From 6808903d5704fa712f9a4170b3bf8fc877231545 Mon Sep 17 00:00:00 2001
From: Christos Nasikas <christos.nasikas@elastic.co>
Date: Fri, 26 Jun 2020 21:31:41 +0300
Subject: [PATCH 10/21] [SIEM][CASE] Persist callout when dismissed (#68372)

---
 x-pack/plugins/security_solution/package.json |   3 +-
 .../no_write_alerts_callout/translations.ts   |   4 +-
 .../cases/components/callout/callout.test.tsx |  89 +++++++
 .../cases/components/callout/callout.tsx      |  53 ++++
 .../cases/components/callout/helpers.test.tsx |  28 +++
 .../cases/components/callout/helpers.tsx      |  12 +-
 .../cases/components/callout/index.test.tsx   | 234 +++++++++++++-----
 .../public/cases/components/callout/index.tsx | 136 +++++-----
 .../cases/components/callout/translations.ts  |   4 +-
 .../public/cases/components/callout/types.ts  |  12 +
 .../use_push_to_service/helpers.tsx           |   9 +-
 .../use_push_to_service/index.test.tsx        |  16 +-
 .../components/use_push_to_service/index.tsx  |  11 +-
 .../public/cases/pages/case.tsx               |   6 +-
 .../public/cases/pages/case_details.tsx       |   6 +-
 .../use_messages_storage.test.tsx             |  85 +++++++
 .../local_storage/use_messages_storage.tsx    |  52 ++++
 .../public/common/mock/kibana_react.ts        |   3 +
 .../timeline/header/translations.ts           |   2 +-
 yarn.lock                                     |   7 +
 20 files changed, 618 insertions(+), 154 deletions(-)
 create mode 100644 x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx
 create mode 100644 x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx
 create mode 100644 x-pack/plugins/security_solution/public/cases/components/callout/helpers.test.tsx
 create mode 100644 x-pack/plugins/security_solution/public/cases/components/callout/types.ts
 create mode 100644 x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx
 create mode 100644 x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx

diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json
index 108ed66958856..1ce5243bf7950 100644
--- a/x-pack/plugins/security_solution/package.json
+++ b/x-pack/plugins/security_solution/package.json
@@ -13,7 +13,8 @@
     "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts"
   },
   "devDependencies": {
-    "@types/lodash": "^4.14.110"
+    "@types/lodash": "^4.14.110",
+    "@types/md5": "^2.2.0"
   },
   "dependencies": {
     "@types/rbush": "^3.0.0",
diff --git a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts b/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts
index d036c422b2fb9..211bd21c915c0 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts
+++ b/x-pack/plugins/security_solution/public/alerts/components/no_write_alerts_callout/translations.ts
@@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
 export const NO_WRITE_ALERTS_CALLOUT_TITLE = i18n.translate(
   'xpack.securitySolution.detectionEngine.noWriteAlertsCallOutTitle',
   {
-    defaultMessage: 'Alerts index permissions required',
+    defaultMessage: 'You cannot change alert states',
   }
 );
 
@@ -17,7 +17,7 @@ export const NO_WRITE_ALERTS_CALLOUT_MSG = i18n.translate(
   'xpack.securitySolution.detectionEngine.noWriteAlertsCallOutMsg',
   {
     defaultMessage:
-      'You are currently missing the required permissions to update alerts. Please contact your administrator for further assistance.',
+      'You only have permissions to view alerts. If you need to update alert states (open or close alerts), contact your Kibana administrator.',
   }
 );
 
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx
new file mode 100644
index 0000000000000..7a344d9360b7d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/callout.test.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 { CallOut, CallOutProps } from './callout';
+
+describe('Callout', () => {
+  const defaultProps: CallOutProps = {
+    id: 'md5-hex',
+    type: 'primary',
+    title: 'a tittle',
+    messages: [
+      {
+        id: 'generic-error',
+        title: 'message-one',
+        description: <p>{'error'}</p>,
+      },
+    ],
+    showCallOut: true,
+    handleDismissCallout: jest.fn(),
+  };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('It renders the callout', () => {
+    const wrapper = mount(<CallOut {...defaultProps} />);
+    expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy();
+    expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy();
+    expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy();
+  });
+
+  it('hides the callout', () => {
+    const wrapper = mount(<CallOut {...defaultProps} showCallOut={false} />);
+    expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeFalsy();
+  });
+
+  it('does not shows any messages when the list is empty', () => {
+    const wrapper = mount(<CallOut {...defaultProps} messages={[]} />);
+    expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy();
+  });
+
+  it('transform the button color correctly - primary', () => {
+    const wrapper = mount(<CallOut {...defaultProps} />);
+    const className =
+      wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ??
+      '';
+    expect(className.includes('euiButton--primary')).toBeTruthy();
+  });
+
+  it('transform the button color correctly - success', () => {
+    const wrapper = mount(<CallOut {...defaultProps} type={'success'} />);
+    const className =
+      wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ??
+      '';
+    expect(className.includes('euiButton--secondary')).toBeTruthy();
+  });
+
+  it('transform the button color correctly - warning', () => {
+    const wrapper = mount(<CallOut {...defaultProps} type={'warning'} />);
+    const className =
+      wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ??
+      '';
+    expect(className.includes('euiButton--warning')).toBeTruthy();
+  });
+
+  it('transform the button color correctly - danger', () => {
+    const wrapper = mount(<CallOut {...defaultProps} type={'danger'} />);
+    const className =
+      wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ??
+      '';
+    expect(className.includes('euiButton--danger')).toBeTruthy();
+  });
+
+  it('dismiss the callout correctly', () => {
+    const wrapper = mount(<CallOut {...defaultProps} messages={[]} />);
+    expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy();
+    wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click');
+    wrapper.update();
+
+    expect(defaultProps.handleDismissCallout).toHaveBeenCalledWith('md5-hex', 'primary');
+  });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx
new file mode 100644
index 0000000000000..e1ebe5c5db17e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/callout.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 { EuiCallOut, EuiButton, EuiDescriptionList } from '@elastic/eui';
+import { isEmpty } from 'lodash/fp';
+import React, { memo, useCallback } from 'react';
+
+import { ErrorMessage } from './types';
+import * as i18n from './translations';
+
+export interface CallOutProps {
+  id: string;
+  type: NonNullable<ErrorMessage['errorType']>;
+  title: string;
+  messages: ErrorMessage[];
+  showCallOut: boolean;
+  handleDismissCallout: (id: string, type: NonNullable<ErrorMessage['errorType']>) => void;
+}
+
+const CallOutComponent = ({
+  id,
+  type,
+  title,
+  messages,
+  showCallOut,
+  handleDismissCallout,
+}: CallOutProps) => {
+  const handleCallOut = useCallback(() => handleDismissCallout(id, type), [
+    handleDismissCallout,
+    id,
+    type,
+  ]);
+
+  return showCallOut ? (
+    <EuiCallOut title={title} color={type} iconType="gear" data-test-subj={`case-callout-${id}`}>
+      {!isEmpty(messages) && (
+        <EuiDescriptionList data-test-subj={`callout-messages-${id}`} listItems={messages} />
+      )}
+      <EuiButton
+        data-test-subj={`callout-dismiss-${id}`}
+        color={type === 'success' ? 'secondary' : type}
+        onClick={handleCallOut}
+      >
+        {i18n.DISMISS_CALLOUT}
+      </EuiButton>
+    </EuiCallOut>
+  ) : null;
+};
+
+export const CallOut = memo(CallOutComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.test.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.test.tsx
new file mode 100644
index 0000000000000..c5fb7f3fa4477
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.test.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 md5 from 'md5';
+import { createCalloutId } from './helpers';
+
+describe('createCalloutId', () => {
+  it('creates id correctly with one id', () => {
+    const digest = md5('one');
+    const id = createCalloutId(['one']);
+    expect(id).toBe(digest);
+  });
+
+  it('creates id correctly with multiples ids', () => {
+    const digest = md5('one|two|three');
+    const id = createCalloutId(['one', 'two', 'three']);
+    expect(id).toBe(digest);
+  });
+
+  it('creates id correctly with multiples ids and delimiter', () => {
+    const digest = md5('one,two,three');
+    const id = createCalloutId(['one', 'two', 'three'], ',');
+    expect(id).toBe(digest);
+  });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx
index 3237104274473..23c1abda66a7c 100644
--- a/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/helpers.tsx
@@ -3,10 +3,18 @@
  * 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 md5 from 'md5';
 
 import * as i18n from './translations';
+import { ErrorMessage } from './types';
 
-export const savedObjectReadOnly = {
+export const savedObjectReadOnlyErrorMessage: ErrorMessage = {
+  id: 'read-only-privileges-error',
   title: i18n.READ_ONLY_SAVED_OBJECT_TITLE,
-  description: i18n.READ_ONLY_SAVED_OBJECT_MSG,
+  description: <>{i18n.READ_ONLY_SAVED_OBJECT_MSG}</>,
+  errorType: 'warning',
 };
+
+export const createCalloutId = (ids: string[], delimiter: string = '|'): string =>
+  md5(ids.join(delimiter));
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/index.test.tsx
index ee3faeb2ceeb5..6d8917218c7c5 100644
--- a/x-pack/plugins/security_solution/public/cases/components/callout/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/index.test.tsx
@@ -7,104 +7,210 @@
 import React from 'react';
 import { mount } from 'enzyme';
 
-import { CaseCallOut } from '.';
+import { useMessagesStorage } from '../../../common/containers/local_storage/use_messages_storage';
+import { TestProviders } from '../../../common/mock';
+import { createCalloutId } from './helpers';
+import { CaseCallOut, CaseCallOutProps } from '.';
 
-const defaultProps = {
-  title: 'hey title',
+jest.mock('../../../common/containers/local_storage/use_messages_storage');
+
+const useSecurityLocalStorageMock = useMessagesStorage as jest.Mock;
+const securityLocalStorageMock = {
+  getMessages: jest.fn(() => []),
+  addMessage: jest.fn(),
 };
 
 describe('CaseCallOut ', () => {
-  it('Renders single message callout', () => {
-    const props = {
-      ...defaultProps,
-      message: 'we have one message',
-    };
-
-    const wrapper = mount(<CaseCallOut {...props} />);
-
-    expect(wrapper.find(`[data-test-subj="callout-message-primary"]`).last().exists()).toBeTruthy();
+  beforeEach(() => {
+    jest.clearAllMocks();
+    useSecurityLocalStorageMock.mockImplementation(() => securityLocalStorageMock);
   });
 
-  it('Renders multi message callout', () => {
-    const props = {
-      ...defaultProps,
+  it('renders a callout correctly', () => {
+    const props: CaseCallOutProps = {
+      title: 'hey title',
       messages: [
-        { ...defaultProps, description: <p>{'we have two messages'}</p> },
-        { ...defaultProps, description: <p>{'for real'}</p> },
+        { id: 'message-one', title: 'title', description: <p>{'we have two messages'}</p> },
+        { id: 'message-two', title: 'title', description: <p>{'for real'}</p> },
       ],
     };
-    const wrapper = mount(<CaseCallOut {...props} />);
-    expect(wrapper.find(`[data-test-subj="callout-message-primary"]`).last().exists()).toBeFalsy();
-    expect(
-      wrapper.find(`[data-test-subj="callout-messages-primary"]`).last().exists()
-    ).toBeTruthy();
+    const wrapper = mount(
+      <TestProviders>
+        <CaseCallOut {...props} />
+      </TestProviders>
+    );
+
+    const id = createCalloutId(['message-one', 'message-two']);
+    expect(wrapper.find(`[data-test-subj="callout-messages-${id}"]`).last().exists()).toBeTruthy();
   });
 
-  it('it shows the correct type of callouts', () => {
-    const props = {
-      ...defaultProps,
+  it('groups the messages correctly', () => {
+    const props: CaseCallOutProps = {
+      title: 'hey title',
       messages: [
         {
-          ...defaultProps,
+          id: 'message-one',
+          title: 'title one',
           description: <p>{'we have two messages'}</p>,
-          errorType: 'danger' as 'primary' | 'success' | 'warning' | 'danger',
+          errorType: 'danger',
         },
-        { ...defaultProps, description: <p>{'for real'}</p> },
+        { id: 'message-two', title: 'title two', description: <p>{'for real'}</p> },
       ],
     };
-    const wrapper = mount(<CaseCallOut {...props} />);
-    expect(wrapper.find(`[data-test-subj="callout-messages-danger"]`).last().exists()).toBeTruthy();
 
+    const wrapper = mount(
+      <TestProviders>
+        <CaseCallOut {...props} />
+      </TestProviders>
+    );
+
+    const idDanger = createCalloutId(['message-one']);
+    const idPrimary = createCalloutId(['message-two']);
+
+    expect(
+      wrapper.find(`[data-test-subj="case-callout-${idPrimary}"]`).last().exists()
+    ).toBeTruthy();
     expect(
-      wrapper.find(`[data-test-subj="callout-messages-primary"]`).last().exists()
+      wrapper.find(`[data-test-subj="case-callout-${idDanger}"]`).last().exists()
     ).toBeTruthy();
   });
 
-  it('it applies the correct color to button', () => {
-    const props = {
-      ...defaultProps,
+  it('dismisses the callout correctly', () => {
+    const props: CaseCallOutProps = {
+      title: 'hey title',
       messages: [
-        {
-          ...defaultProps,
-          description: <p>{'one'}</p>,
-          errorType: 'danger' as 'primary' | 'success' | 'warning' | 'danger',
-        },
-        {
-          ...defaultProps,
-          description: <p>{'two'}</p>,
-          errorType: 'success' as 'primary' | 'success' | 'warning' | 'danger',
-        },
-        {
-          ...defaultProps,
-          description: <p>{'three'}</p>,
-          errorType: 'primary' as 'primary' | 'success' | 'warning' | 'danger',
-        },
+        { id: 'message-one', title: 'title', description: <p>{'we have two messages'}</p> },
       ],
     };
+    const wrapper = mount(
+      <TestProviders>
+        <CaseCallOut {...props} />
+      </TestProviders>
+    );
 
-    const wrapper = mount(<CaseCallOut {...props} />);
+    const id = createCalloutId(['message-one']);
+
+    expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeTruthy();
+    wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click');
+    expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).exists()).toBeFalsy();
+  });
+
+  it('persist the callout of type primary when dismissed', () => {
+    const props: CaseCallOutProps = {
+      title: 'hey title',
+      messages: [
+        { id: 'message-one', title: 'title', description: <p>{'we have two messages'}</p> },
+      ],
+    };
 
-    expect(wrapper.find(`[data-test-subj="callout-dismiss-danger"]`).first().prop('color')).toBe(
-      'danger'
+    const wrapper = mount(
+      <TestProviders>
+        <CaseCallOut {...props} />
+      </TestProviders>
     );
 
-    expect(wrapper.find(`[data-test-subj="callout-dismiss-success"]`).first().prop('color')).toBe(
-      'secondary'
+    const id = createCalloutId(['message-one']);
+    expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith('case');
+    wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click');
+    expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith('case', id);
+  });
+
+  it('do not show the callout if is in the localStorage', () => {
+    const props: CaseCallOutProps = {
+      title: 'hey title',
+      messages: [
+        { id: 'message-one', title: 'title', description: <p>{'we have two messages'}</p> },
+      ],
+    };
+
+    const id = createCalloutId(['message-one']);
+
+    useSecurityLocalStorageMock.mockImplementation(() => ({
+      ...securityLocalStorageMock,
+      getMessages: jest.fn(() => [id]),
+    }));
+
+    const wrapper = mount(
+      <TestProviders>
+        <CaseCallOut {...props} />
+      </TestProviders>
     );
 
-    expect(wrapper.find(`[data-test-subj="callout-dismiss-primary"]`).first().prop('color')).toBe(
-      'primary'
+    expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeFalsy();
+  });
+
+  it('do not persist a callout of type danger', () => {
+    const props: CaseCallOutProps = {
+      title: 'hey title',
+      messages: [
+        {
+          id: 'message-one',
+          title: 'title one',
+          description: <p>{'we have two messages'}</p>,
+          errorType: 'danger',
+        },
+      ],
+    };
+
+    const wrapper = mount(
+      <TestProviders>
+        <CaseCallOut {...props} />
+      </TestProviders>
     );
+
+    const id = createCalloutId(['message-one']);
+    wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click');
+    wrapper.update();
+    expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled();
   });
 
-  it('Dismisses callout', () => {
-    const props = {
-      ...defaultProps,
-      message: 'we have one message',
+  it('do not persist a callout of type warning', () => {
+    const props: CaseCallOutProps = {
+      title: 'hey title',
+      messages: [
+        {
+          id: 'message-one',
+          title: 'title one',
+          description: <p>{'we have two messages'}</p>,
+          errorType: 'warning',
+        },
+      ],
     };
-    const wrapper = mount(<CaseCallOut {...props} />);
-    expect(wrapper.find(`[data-test-subj="case-call-out-primary"]`).exists()).toBeTruthy();
-    wrapper.find(`[data-test-subj="callout-dismiss-primary"]`).last().simulate('click');
-    expect(wrapper.find(`[data-test-subj="case-call-out-primary"]`).exists()).toBeFalsy();
+
+    const wrapper = mount(
+      <TestProviders>
+        <CaseCallOut {...props} />
+      </TestProviders>
+    );
+
+    const id = createCalloutId(['message-one']);
+    wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click');
+    wrapper.update();
+    expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled();
+  });
+
+  it('do not persist a callout of type success', () => {
+    const props: CaseCallOutProps = {
+      title: 'hey title',
+      messages: [
+        {
+          id: 'message-one',
+          title: 'title one',
+          description: <p>{'we have two messages'}</p>,
+          errorType: 'success',
+        },
+      ],
+    };
+
+    const wrapper = mount(
+      <TestProviders>
+        <CaseCallOut {...props} />
+      </TestProviders>
+    );
+
+    const id = createCalloutId(['message-one']);
+    wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click');
+    wrapper.update();
+    expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled();
   });
 });
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/index.tsx b/x-pack/plugins/security_solution/public/cases/components/callout/index.tsx
index 171c0508b9d92..cefaec6ad0b06 100644
--- a/x-pack/plugins/security_solution/public/cases/components/callout/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/index.tsx
@@ -4,79 +4,99 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { EuiCallOut, EuiButton, EuiDescriptionList, EuiSpacer } from '@elastic/eui';
-import { isEmpty } from 'lodash/fp';
-import React, { memo, useCallback, useState } from 'react';
+import { EuiSpacer } from '@elastic/eui';
+import React, { memo, useCallback, useState, useMemo } from 'react';
 
-import * as i18n from './translations';
+import { useMessagesStorage } from '../../../common/containers/local_storage/use_messages_storage';
+import { CallOut } from './callout';
+import { ErrorMessage } from './types';
+import { createCalloutId } from './helpers';
 
 export * from './helpers';
 
-interface ErrorMessage {
+export interface CaseCallOutProps {
   title: string;
-  description: JSX.Element;
-  errorType?: 'primary' | 'success' | 'warning' | 'danger';
+  messages?: ErrorMessage[];
 }
 
-interface CaseCallOutProps {
-  title: string;
-  message?: string;
-  messages?: ErrorMessage[];
+type GroupByTypeMessages = {
+  [key in NonNullable<ErrorMessage['errorType']>]: {
+    messagesId: string[];
+    messages: ErrorMessage[];
+  };
+};
+
+interface CalloutVisibility {
+  [index: string]: boolean;
 }
 
-const CaseCallOutComponent = ({ title, message, messages }: CaseCallOutProps) => {
-  const [showCallOut, setShowCallOut] = useState(true);
-  const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]);
-  let callOutMessages = messages ?? [];
+const CaseCallOutComponent = ({ title, messages = [] }: CaseCallOutProps) => {
+  const { getMessages, addMessage } = useMessagesStorage();
+
+  const caseMessages = useMemo(() => getMessages('case'), [getMessages]);
+  const dismissedCallouts = useMemo(
+    () =>
+      caseMessages.reduce<CalloutVisibility>(
+        (acc, id) => ({
+          ...acc,
+          [id]: false,
+        }),
+        {}
+      ),
+    [caseMessages]
+  );
 
-  if (message) {
-    callOutMessages = [
-      ...callOutMessages,
-      {
-        title: '',
-        description: <p data-test-subj="callout-message-primary">{message}</p>,
-        errorType: 'primary',
-      },
-    ];
-  }
+  const [calloutVisibility, setCalloutVisibility] = useState(dismissedCallouts);
+  const handleCallOut = useCallback(
+    (id, type) => {
+      setCalloutVisibility((prevState) => ({ ...prevState, [id]: false }));
+      if (type === 'primary') {
+        addMessage('case', id);
+      }
+    },
+    [setCalloutVisibility, addMessage]
+  );
 
-  const groupedErrorMessages = callOutMessages.reduce((acc, currentMessage: ErrorMessage) => {
-    const key = currentMessage.errorType == null ? 'primary' : currentMessage.errorType;
-    return {
-      ...acc,
-      [key]: [...(acc[key] || []), currentMessage],
-    };
-  }, {} as { [key in NonNullable<ErrorMessage['errorType']>]: ErrorMessage[] });
+  const groupedByTypeErrorMessages = useMemo(
+    () =>
+      messages.reduce<GroupByTypeMessages>(
+        (acc: GroupByTypeMessages, currentMessage: ErrorMessage) => {
+          const type = currentMessage.errorType == null ? 'primary' : currentMessage.errorType;
+          return {
+            ...acc,
+            [type]: {
+              messagesId: [...(acc[type]?.messagesId ?? []), currentMessage.id],
+              messages: [...(acc[type]?.messages ?? []), currentMessage],
+            },
+          };
+        },
+        {} as GroupByTypeMessages
+      ),
+    [messages]
+  );
 
-  return showCallOut ? (
+  return (
     <>
-      {(Object.keys(groupedErrorMessages) as Array<keyof ErrorMessage['errorType']>).map((key) => (
-        <React.Fragment key={key}>
-          <EuiCallOut
-            title={title}
-            color={key}
-            iconType="gear"
-            data-test-subj={`case-call-out-${key}`}
-          >
-            {!isEmpty(groupedErrorMessages[key]) && (
-              <EuiDescriptionList
-                data-test-subj={`callout-messages-${key}`}
-                listItems={groupedErrorMessages[key]}
+      {(Object.keys(groupedByTypeErrorMessages) as Array<keyof ErrorMessage['errorType']>).map(
+        (type: NonNullable<ErrorMessage['errorType']>) => {
+          const id = createCalloutId(groupedByTypeErrorMessages[type].messagesId);
+          return (
+            <React.Fragment key={id}>
+              <CallOut
+                id={id}
+                type={type}
+                title={title}
+                messages={groupedByTypeErrorMessages[type].messages}
+                showCallOut={calloutVisibility[id] ?? true}
+                handleDismissCallout={handleCallOut}
               />
-            )}
-            <EuiButton
-              data-test-subj={`callout-dismiss-${key}`}
-              color={key === 'success' ? 'secondary' : key}
-              onClick={handleCallOut}
-            >
-              {i18n.DISMISS_CALLOUT}
-            </EuiButton>
-          </EuiCallOut>
-          <EuiSpacer />
-        </React.Fragment>
-      ))}
+              <EuiSpacer />
+            </React.Fragment>
+          );
+        }
+      )}
     </>
-  ) : null;
+  );
 };
 
 export const CaseCallOut = memo(CaseCallOutComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
index 01956ca942997..2ba3df82102e2 100644
--- a/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/translations.ts
@@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
 export const READ_ONLY_SAVED_OBJECT_TITLE = i18n.translate(
   'xpack.securitySolution.case.readOnlySavedObjectTitle',
   {
-    defaultMessage: 'You have read-only feature privileges',
+    defaultMessage: 'You cannot open new or update existing cases',
   }
 );
 
@@ -17,7 +17,7 @@ export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate(
   'xpack.securitySolution.case.readOnlySavedObjectDescription',
   {
     defaultMessage:
-      'You are only allowed to view cases. If you need to open and update cases, contact your Kibana administrator',
+      'You only have permissions to view cases. If you need to open and update cases, contact your Kibana administrator.',
   }
 );
 
diff --git a/x-pack/plugins/security_solution/public/cases/components/callout/types.ts b/x-pack/plugins/security_solution/public/cases/components/callout/types.ts
new file mode 100644
index 0000000000000..1f07ef1bd9248
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/callout/types.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 ErrorMessage {
+  id: string;
+  title: string;
+  description: JSX.Element;
+  errorType?: 'primary' | 'success' | 'warning' | 'danger';
+}
diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx
index 919231d2f6034..43f2a2a6e12f1 100644
--- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/helpers.tsx
@@ -10,8 +10,10 @@ import React from 'react';
 
 import * as i18n from './translations';
 import { ActionLicense } from '../../containers/types';
+import { ErrorMessage } from '../callout/types';
 
 export const getLicenseError = () => ({
+  id: 'license-error',
   title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE,
   description: (
     <FormattedMessage
@@ -29,6 +31,7 @@ export const getLicenseError = () => ({
 });
 
 export const getKibanaConfigError = () => ({
+  id: 'kibana-config-error',
   title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE,
   description: (
     <FormattedMessage
@@ -45,10 +48,8 @@ export const getKibanaConfigError = () => ({
   ),
 });
 
-export const getActionLicenseError = (
-  actionLicense: ActionLicense | null
-): Array<{ title: string; description: JSX.Element }> => {
-  let errors: Array<{ title: string; description: JSX.Element }> = [];
+export const getActionLicenseError = (actionLicense: ActionLicense | null): ErrorMessage[] => {
+  let errors: ErrorMessage[] = [];
   if (actionLicense != null && !actionLicense.enabledInLicense) {
     errors = [...errors, getLicenseError()];
   }
diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx
index f2de830a71644..d17a2bd215910 100644
--- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx
@@ -10,9 +10,7 @@ import { usePushToService, ReturnUsePushToService, UsePushToService } from '.';
 import { TestProviders } from '../../../common/mock';
 import { usePostPushToService } from '../../containers/use_post_push_to_service';
 import { basicPush, actionLicenses } from '../../containers/mock';
-import * as i18n from './translations';
 import { useGetActionLicense } from '../../containers/use_get_action_license';
-import { getKibanaConfigError, getLicenseError } from './helpers';
 import { connectorsMock } from '../../containers/configure/mock';
 
 jest.mock('react-router-dom', () => {
@@ -110,7 +108,7 @@ describe('usePushToService', () => {
       await waitForNextUpdate();
       const errorsMsg = result.current.pushCallouts?.props.messages;
       expect(errorsMsg).toHaveLength(1);
-      expect(errorsMsg[0].title).toEqual(getLicenseError().title);
+      expect(errorsMsg[0].id).toEqual('license-error');
     });
   });
 
@@ -132,7 +130,7 @@ describe('usePushToService', () => {
       await waitForNextUpdate();
       const errorsMsg = result.current.pushCallouts?.props.messages;
       expect(errorsMsg).toHaveLength(1);
-      expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title);
+      expect(errorsMsg[0].id).toEqual('kibana-config-error');
     });
   });
 
@@ -152,7 +150,7 @@ describe('usePushToService', () => {
       await waitForNextUpdate();
       const errorsMsg = result.current.pushCallouts?.props.messages;
       expect(errorsMsg).toHaveLength(1);
-      expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CONFIG_TITLE);
+      expect(errorsMsg[0].id).toEqual('connector-missing-error');
     });
   });
 
@@ -171,7 +169,7 @@ describe('usePushToService', () => {
       await waitForNextUpdate();
       const errorsMsg = result.current.pushCallouts?.props.messages;
       expect(errorsMsg).toHaveLength(1);
-      expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE);
+      expect(errorsMsg[0].id).toEqual('connector-not-selected-error');
     });
   });
 
@@ -191,7 +189,7 @@ describe('usePushToService', () => {
       await waitForNextUpdate();
       const errorsMsg = result.current.pushCallouts?.props.messages;
       expect(errorsMsg).toHaveLength(1);
-      expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE);
+      expect(errorsMsg[0].id).toEqual('connector-deleted-error');
     });
   });
 
@@ -212,7 +210,7 @@ describe('usePushToService', () => {
       await waitForNextUpdate();
       const errorsMsg = result.current.pushCallouts?.props.messages;
       expect(errorsMsg).toHaveLength(1);
-      expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE);
+      expect(errorsMsg[0].id).toEqual('connector-deleted-error');
     });
   });
 
@@ -231,7 +229,7 @@ describe('usePushToService', () => {
       await waitForNextUpdate();
       const errorsMsg = result.current.pushCallouts?.props.messages;
       expect(errorsMsg).toHaveLength(1);
-      expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE);
+      expect(errorsMsg[0].id).toEqual('closed-case-push-error');
     });
   });
 });
diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx
index 45b515ccacacd..7b4a29098bdde 100644
--- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx
@@ -20,6 +20,7 @@ import { Connector } from '../../../../../case/common/api/cases';
 import { CaseServices } from '../../containers/use_get_case_user_actions';
 import { LinkAnchor } from '../../../common/components/links';
 import { SecurityPageName } from '../../../app/types';
+import { ErrorMessage } from '../callout/types';
 
 export interface UsePushToService {
   caseId: string;
@@ -76,11 +77,7 @@ export const usePushToService = ({
   );
 
   const errorsMsg = useMemo(() => {
-    let errors: Array<{
-      title: string;
-      description: JSX.Element;
-      errorType?: 'primary' | 'success' | 'warning' | 'danger';
-    }> = [];
+    let errors: ErrorMessage[] = [];
     if (actionLicense != null && !actionLicense.enabledInLicense) {
       errors = [...errors, getLicenseError()];
     }
@@ -88,6 +85,7 @@ export const usePushToService = ({
       errors = [
         ...errors,
         {
+          id: 'connector-missing-error',
           title: i18n.PUSH_DISABLE_BY_NO_CONFIG_TITLE,
           description: (
             <FormattedMessage
@@ -112,6 +110,7 @@ export const usePushToService = ({
       errors = [
         ...errors,
         {
+          id: 'connector-not-selected-error',
           title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE,
           description: (
             <FormattedMessage
@@ -125,6 +124,7 @@ export const usePushToService = ({
       errors = [
         ...errors,
         {
+          id: 'connector-deleted-error',
           title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE,
           description: (
             <FormattedMessage
@@ -140,6 +140,7 @@ export const usePushToService = ({
       errors = [
         ...errors,
         {
+          id: 'closed-case-push-error',
           title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE,
           description: (
             <FormattedMessage
diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx
index eb6da9579e4e8..802394c400d24 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx
@@ -11,7 +11,7 @@ import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana';
 import { SpyRoute } from '../../common/utils/route/spy_routes';
 import { AllCases } from '../components/all_cases';
 
-import { savedObjectReadOnly, CaseCallOut } from '../components/callout';
+import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/callout';
 import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions';
 import { SecurityPageName } from '../../app/types';
 
@@ -23,8 +23,8 @@ export const CasesPage = React.memo(() => {
       <WrapperPage>
         {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
           <CaseCallOut
-            title={savedObjectReadOnly.title}
-            message={savedObjectReadOnly.description}
+            title={savedObjectReadOnlyErrorMessage.title}
+            messages={[{ ...savedObjectReadOnlyErrorMessage }]}
           />
         )}
         <AllCases userCanCrud={userPermissions?.crud ?? false} />
diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
index 43c51b32bce0f..c3538f0c18ed5 100644
--- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
+++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx
@@ -15,7 +15,7 @@ import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana';
 import { getCaseUrl } from '../../common/components/link_to';
 import { navTabs } from '../../app/home/home_navigations';
 import { CaseView } from '../components/case_view';
-import { savedObjectReadOnly, CaseCallOut } from '../components/callout';
+import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/callout';
 
 export const CaseDetailsPage = React.memo(() => {
   const history = useHistory();
@@ -33,8 +33,8 @@ export const CaseDetailsPage = React.memo(() => {
       <WrapperPage noPadding>
         {userPermissions != null && !userPermissions?.crud && userPermissions?.read && (
           <CaseCallOut
-            title={savedObjectReadOnly.title}
-            message={savedObjectReadOnly.description}
+            title={savedObjectReadOnlyErrorMessage.title}
+            messages={[{ ...savedObjectReadOnlyErrorMessage }]}
           />
         )}
         <CaseView caseId={caseId} userCanCrud={userPermissions?.crud ?? false} />
diff --git a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx
new file mode 100644
index 0000000000000..d52bc4b1a267d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx
@@ -0,0 +1,85 @@
+/*
+ * 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 { renderHook, act } from '@testing-library/react-hooks';
+import { useKibana } from '../../lib/kibana';
+import { createUseKibanaMock } from '../../mock/kibana_react';
+import { useMessagesStorage, UseMessagesStorage } from './use_messages_storage';
+
+jest.mock('../../lib/kibana');
+const useKibanaMock = useKibana as jest.Mock;
+
+describe('useLocalStorage', () => {
+  beforeEach(() => {
+    const services = { ...createUseKibanaMock()().services };
+    useKibanaMock.mockImplementation(() => ({ services }));
+    services.storage.store.clear();
+  });
+
+  it('should return an empty array when there is no messages', async () => {
+    await act(async () => {
+      const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() =>
+        useMessagesStorage()
+      );
+      await waitForNextUpdate();
+      const { getMessages } = result.current;
+      expect(getMessages('case')).toEqual([]);
+    });
+  });
+
+  it('should add a message', async () => {
+    await act(async () => {
+      const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() =>
+        useMessagesStorage()
+      );
+      await waitForNextUpdate();
+      const { getMessages, addMessage } = result.current;
+      addMessage('case', 'id-1');
+      expect(getMessages('case')).toEqual(['id-1']);
+    });
+  });
+
+  it('should add multiple messages', async () => {
+    await act(async () => {
+      const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() =>
+        useMessagesStorage()
+      );
+      await waitForNextUpdate();
+      const { getMessages, addMessage } = result.current;
+      addMessage('case', 'id-1');
+      addMessage('case', 'id-2');
+      expect(getMessages('case')).toEqual(['id-1', 'id-2']);
+    });
+  });
+
+  it('should remove a message', async () => {
+    await act(async () => {
+      const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() =>
+        useMessagesStorage()
+      );
+      await waitForNextUpdate();
+      const { getMessages, addMessage, removeMessage } = result.current;
+      addMessage('case', 'id-1');
+      addMessage('case', 'id-2');
+      removeMessage('case', 'id-2');
+      expect(getMessages('case')).toEqual(['id-1']);
+    });
+  });
+
+  it('should clear all messages', async () => {
+    await act(async () => {
+      const { result, waitForNextUpdate } = renderHook<string, UseMessagesStorage>(() =>
+        useMessagesStorage()
+      );
+      await waitForNextUpdate();
+      const { getMessages, addMessage, clearAllMessages } = result.current;
+      addMessage('case', 'id-1');
+      addMessage('case', 'id-2');
+      clearAllMessages('case');
+      expect(getMessages('case')).toEqual([]);
+    });
+  });
+});
diff --git a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx
new file mode 100644
index 0000000000000..0c96712ad9c53
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { useCallback } from 'react';
+import { useKibana } from '../../lib/kibana';
+
+export interface UseMessagesStorage {
+  getMessages: (plugin: string) => string[];
+  addMessage: (plugin: string, id: string) => void;
+  removeMessage: (plugin: string, id: string) => void;
+  clearAllMessages: (plugin: string) => void;
+}
+
+export const useMessagesStorage = (): UseMessagesStorage => {
+  const { storage } = useKibana().services;
+
+  const getMessages = useCallback(
+    (plugin: string): string[] => storage.get(`${plugin}-messages`) ?? [],
+    [storage]
+  );
+
+  const addMessage = useCallback(
+    (plugin: string, id: string) => {
+      const pluginStorage = storage.get(`${plugin}-messages`) ?? [];
+      storage.set(`${plugin}-messages`, [...pluginStorage, id]);
+    },
+    [storage]
+  );
+
+  const removeMessage = useCallback(
+    (plugin: string, id: string) => {
+      const pluginStorage = storage.get(`${plugin}-messages`) ?? [];
+      storage.set(`${plugin}-messages`, [...pluginStorage.filter((val: string) => val !== id)]);
+    },
+    [storage]
+  );
+
+  const clearAllMessages = useCallback(
+    (plugin: string): string[] => storage.remove(`${plugin}-messages`),
+    [storage]
+  );
+
+  return {
+    getMessages,
+    addMessage,
+    clearAllMessages,
+    removeMessage,
+  };
+};
diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts
index cc8970d4df5b4..2b639bfdc14f5 100644
--- a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts
@@ -26,6 +26,7 @@ import {
   DEFAULT_INDEX_PATTERN,
 } from '../../../common/constants';
 import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core';
+import { createSecuritySolutionStorageMock } from './mock_local_storage';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export const mockUiSettings: Record<string, any> = {
@@ -74,6 +75,7 @@ export const createUseKibanaMock = () => {
   const core = createKibanaCoreStartMock();
   const plugins = createKibanaPluginsStartMock();
   const useUiSetting = createUseUiSettingMock();
+  const { storage } = createSecuritySolutionStorageMock();
 
   const services = {
     ...core,
@@ -82,6 +84,7 @@ export const createUseKibanaMock = () => {
       ...core.uiSettings,
       get: useUiSetting,
     },
+    storage,
   };
 
   return () => ({ services });
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
index 7c28f88a571d5..c3c11289037a2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
@@ -10,6 +10,6 @@ export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate(
   'xpack.securitySolution.timeline.callOut.unauthorized.message.description',
   {
     defaultMessage:
-      'You require permission to auto-save timelines within the SIEM application, though you may continue to use the timeline to search and filter security events',
+      'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.',
   }
 );
diff --git a/yarn.lock b/yarn.lock
index 53fef40b44c93..0a7899e4ac102 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5375,6 +5375,13 @@
   dependencies:
     "@types/linkify-it" "*"
 
+"@types/md5@^2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.2.0.tgz#cd82e16b95973f94bb03dee40c5b6be4a7fb7fb4"
+  integrity sha512-JN8OVL/wiDlCWTPzplsgMPu0uE9Q6blwp68rYsfk2G8aokRUQ8XD9MEhZwihfAiQvoyE+m31m6i3GFXwYWomKQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/memoize-one@^4.1.0":
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.1.tgz#41dd138a4335b5041f7d8fc038f9d593d88b3369"

From 0bdff152973d766973702ac791dcc23ea1d24312 Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Fri, 26 Jun 2020 14:59:13 -0400
Subject: [PATCH 11/21] [ENDPOINT] Hide the Timeline Flyout while on the
 Management Pages (#69998)

* hide timeline on Management pages
* adjust managment page view styles
* Added additional tests for validating no timeline button on management views
* centralize API Path responses and reuse across some tests
* Fix state being reset incorrectly
---
 .../__snapshots__/page_view.test.tsx.snap     |  48 +++---
 .../common/components/endpoint/page_view.tsx  |   7 +-
 .../utils/timeline/use_show_timeline.tsx      |   2 +-
 .../pages/endpoint_hosts/view/index.test.tsx  |   6 +
 .../policy/store/policy_details/reducer.ts    |  19 ++-
 .../store/policy_list/services/ingest.test.ts |  73 +--------
 .../store/policy_list/test_mock_utils.ts      | 148 +++++++++---------
 .../pages/policy/view/policy_details.test.tsx |  49 +++++-
 .../pages/policy/view/policy_list.test.tsx    |   6 +
 9 files changed, 175 insertions(+), 183 deletions(-)

diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap
index 6d8ea6b346eff..096df5ceab256 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/__snapshots__/page_view.test.tsx.snap
@@ -2,11 +2,11 @@
 
 exports[`PageView component should display body header custom element 1`] = `
 .c0.endpoint--isListView {
-  padding: 0 70px 0 24px;
+  padding: 0 24px;
 }
 
 .c0.endpoint--isListView .endpoint-header {
-  padding: 24px 0;
+  padding: 24px;
   margin-bottom: 0;
 }
 
@@ -22,7 +22,7 @@ exports[`PageView component should display body header custom element 1`] = `
 }
 
 .c0 .endpoint-navTabs {
-  margin-left: 24px;
+  margin-left: 12px;
 }
 
 <PageView
@@ -97,11 +97,11 @@ exports[`PageView component should display body header custom element 1`] = `
 
 exports[`PageView component should display body header wrapped in EuiTitle 1`] = `
 .c0.endpoint--isListView {
-  padding: 0 70px 0 24px;
+  padding: 0 24px;
 }
 
 .c0.endpoint--isListView .endpoint-header {
-  padding: 24px 0;
+  padding: 24px;
   margin-bottom: 0;
 }
 
@@ -117,7 +117,7 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
 }
 
 .c0 .endpoint-navTabs {
-  margin-left: 24px;
+  margin-left: 12px;
 }
 
 <PageView
@@ -195,11 +195,11 @@ exports[`PageView component should display body header wrapped in EuiTitle 1`] =
 
 exports[`PageView component should display header left and right 1`] = `
 .c0.endpoint--isListView {
-  padding: 0 70px 0 24px;
+  padding: 0 24px;
 }
 
 .c0.endpoint--isListView .endpoint-header {
-  padding: 24px 0;
+  padding: 24px;
   margin-bottom: 0;
 }
 
@@ -215,7 +215,7 @@ exports[`PageView component should display header left and right 1`] = `
 }
 
 .c0 .endpoint-navTabs {
-  margin-left: 24px;
+  margin-left: 12px;
 }
 
 <PageView
@@ -308,11 +308,11 @@ exports[`PageView component should display header left and right 1`] = `
 
 exports[`PageView component should display only body if not header props used 1`] = `
 .c0.endpoint--isListView {
-  padding: 0 70px 0 24px;
+  padding: 0 24px;
 }
 
 .c0.endpoint--isListView .endpoint-header {
-  padding: 24px 0;
+  padding: 24px;
   margin-bottom: 0;
 }
 
@@ -328,7 +328,7 @@ exports[`PageView component should display only body if not header props used 1`
 }
 
 .c0 .endpoint-navTabs {
-  margin-left: 24px;
+  margin-left: 12px;
 }
 
 <PageView
@@ -380,11 +380,11 @@ exports[`PageView component should display only body if not header props used 1`
 
 exports[`PageView component should display only header left 1`] = `
 .c0.endpoint--isListView {
-  padding: 0 70px 0 24px;
+  padding: 0 24px;
 }
 
 .c0.endpoint--isListView .endpoint-header {
-  padding: 24px 0;
+  padding: 24px;
   margin-bottom: 0;
 }
 
@@ -400,7 +400,7 @@ exports[`PageView component should display only header left 1`] = `
 }
 
 .c0 .endpoint-navTabs {
-  margin-left: 24px;
+  margin-left: 12px;
 }
 
 <PageView
@@ -482,11 +482,11 @@ exports[`PageView component should display only header left 1`] = `
 
 exports[`PageView component should display only header right but include an empty left side 1`] = `
 .c0.endpoint--isListView {
-  padding: 0 70px 0 24px;
+  padding: 0 24px;
 }
 
 .c0.endpoint--isListView .endpoint-header {
-  padding: 24px 0;
+  padding: 24px;
   margin-bottom: 0;
 }
 
@@ -502,7 +502,7 @@ exports[`PageView component should display only header right but include an empt
 }
 
 .c0 .endpoint-navTabs {
-  margin-left: 24px;
+  margin-left: 12px;
 }
 
 <PageView
@@ -581,11 +581,11 @@ exports[`PageView component should display only header right but include an empt
 
 exports[`PageView component should pass through EuiPage props 1`] = `
 .c0.endpoint--isListView {
-  padding: 0 70px 0 24px;
+  padding: 0 24px;
 }
 
 .c0.endpoint--isListView .endpoint-header {
-  padding: 24px 0;
+  padding: 24px;
   margin-bottom: 0;
 }
 
@@ -601,7 +601,7 @@ exports[`PageView component should pass through EuiPage props 1`] = `
 }
 
 .c0 .endpoint-navTabs {
-  margin-left: 24px;
+  margin-left: 12px;
 }
 
 <PageView
@@ -670,11 +670,11 @@ exports[`PageView component should pass through EuiPage props 1`] = `
 
 exports[`PageView component should use custom element for header left and not wrap in EuiTitle 1`] = `
 .c0.endpoint--isListView {
-  padding: 0 70px 0 24px;
+  padding: 0 24px;
 }
 
 .c0.endpoint--isListView .endpoint-header {
-  padding: 24px 0;
+  padding: 24px;
   margin-bottom: 0;
 }
 
@@ -690,7 +690,7 @@ exports[`PageView component should use custom element for header left and not wr
 }
 
 .c0 .endpoint-navTabs {
-  margin-left: 24px;
+  margin-left: 12px;
 }
 
 <PageView
diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx
index 65c536fe12085..3d2a1d2d6fc9b 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/page_view.tsx
@@ -21,14 +21,13 @@ import {
 import React, { memo, MouseEventHandler, ReactNode, useMemo } from 'react';
 import styled from 'styled-components';
 import { EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
-import { gutterTimeline } from '../../lib/helpers';
 
 const StyledEuiPage = styled(EuiPage)`
   &.endpoint--isListView {
-    padding: 0 ${gutterTimeline} 0 ${(props) => props.theme.eui.euiSizeL};
+    padding: 0 ${(props) => props.theme.eui.euiSizeL};
 
     .endpoint-header {
-      padding: ${(props) => props.theme.eui.euiSizeL} 0;
+      padding: ${(props) => props.theme.eui.euiSizeL};
       margin-bottom: 0;
     }
     .endpoint-page-content {
@@ -44,7 +43,7 @@ const StyledEuiPage = styled(EuiPage)`
     }
   }
   .endpoint-navTabs {
-    margin-left: ${(props) => props.theme.eui.euiSizeL};
+    margin-left: ${(props) => props.theme.eui.euiSizeM};
   }
 `;
 
diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx
index fde3f6f8b222d..a9c6660ba9c68 100644
--- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx
@@ -7,7 +7,7 @@
 import { useState, useEffect } from 'react';
 import { useRouteSpy } from '../route/use_route_spy';
 
-const hideTimelineForRoutes = [`/cases/configure`];
+const hideTimelineForRoutes = [`/cases/configure`, '/management'];
 
 export const useShowTimeline = () => {
   const [{ pageName, pathName }] = useRouteSpy();
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index 224411c5f7ec0..7bc101b891477 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -34,6 +34,12 @@ describe('when on the hosts page', () => {
     render = () => mockedContext.render(<HostList />);
   });
 
+  it('should NOT display timeline', async () => {
+    const renderResult = render();
+    const timelineFlyout = await renderResult.queryByTestId('flyoutOverlay');
+    expect(timelineFlyout).toBeNull();
+  });
+
   it('should show a table', async () => {
     const renderResult = render();
     const table = await renderResult.findByTestId('hostListTable');
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts
index 75e7808ea30b1..b3b74c2ca9dae 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts
@@ -78,13 +78,20 @@ export const policyDetailsReducer: ImmutableReducer<PolicyDetailsState, AppActio
     const isCurrentlyOnDetailsPage = isOnPolicyDetailsPage(newState);
     const wasPreviouslyOnDetailsPage = isOnPolicyDetailsPage(state);
 
-    // Did user just enter the Detail page? if so, then set the loading indicator and return new state
-    if (isCurrentlyOnDetailsPage && !wasPreviouslyOnDetailsPage) {
-      return {
-        ...newState,
-        isLoading: true,
-      };
+    if (isCurrentlyOnDetailsPage) {
+      // Did user just enter the Detail page? if so, then
+      // set the loading indicator and return new state
+      if (!wasPreviouslyOnDetailsPage) {
+        return {
+          ...newState,
+          isLoading: true,
+        };
+      }
+      // Else, user was already on the details page,
+      // just return the updated state with new location data
+      return newState;
     }
+
     return {
       ...initialPolicyDetailsState(),
       location: action.payload,
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts
index 2270c65fb149f..818ca49e1e81a 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.test.ts
@@ -5,12 +5,14 @@
  */
 
 import {
+  INGEST_API_EPM_PACKAGES,
   sendGetDatasource,
   sendGetEndpointSecurityPackage,
   sendGetEndpointSpecificDatasources,
 } from './ingest';
 import { httpServiceMock } from '../../../../../../../../../../src/core/public/mocks';
 import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../../ingest_manager/common';
+import { apiPathMockResponseProviders } from '../test_mock_utils';
 
 describe('ingest service', () => {
   let http: ReturnType<typeof httpServiceMock.createStartContract>;
@@ -59,76 +61,7 @@ describe('ingest service', () => {
 
   describe('sendGetEndpointSecurityPackage()', () => {
     it('should query EPM with category=security', async () => {
-      http.get.mockResolvedValue({
-        response: [
-          {
-            name: 'endpoint',
-            title: 'Elastic Endpoint',
-            version: '0.5.0',
-            description: 'This is the Elastic Endpoint package.',
-            type: 'solution',
-            download: '/epr/endpoint/endpoint-0.5.0.tar.gz',
-            path: '/package/endpoint/0.5.0',
-            icons: [
-              {
-                src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg',
-                size: '16x16',
-                type: 'image/svg+xml',
-              },
-            ],
-            status: 'installed',
-            savedObject: {
-              type: 'epm-packages',
-              id: 'endpoint',
-              attributes: {
-                installed: [
-                  { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' },
-                  { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
-                  { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
-                  { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
-                  { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
-                  { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' },
-                  { id: 'logs-endpoint.alerts', type: 'index-template' },
-                  { id: 'events-endpoint', type: 'index-template' },
-                  { id: 'logs-endpoint.events.file', type: 'index-template' },
-                  { id: 'logs-endpoint.events.library', type: 'index-template' },
-                  { id: 'metrics-endpoint.metadata', type: 'index-template' },
-                  { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' },
-                  { id: 'logs-endpoint.events.network', type: 'index-template' },
-                  { id: 'metrics-endpoint.policy', type: 'index-template' },
-                  { id: 'logs-endpoint.events.process', type: 'index-template' },
-                  { id: 'logs-endpoint.events.registry', type: 'index-template' },
-                  { id: 'logs-endpoint.events.security', type: 'index-template' },
-                  { id: 'metrics-endpoint.telemetry', type: 'index-template' },
-                ],
-                es_index_patterns: {
-                  alerts: 'logs-endpoint.alerts-*',
-                  events: 'events-endpoint-*',
-                  file: 'logs-endpoint.events.file-*',
-                  library: 'logs-endpoint.events.library-*',
-                  metadata: 'metrics-endpoint.metadata-*',
-                  metadata_mirror: 'metrics-endpoint.metadata_mirror-*',
-                  network: 'logs-endpoint.events.network-*',
-                  policy: 'metrics-endpoint.policy-*',
-                  process: 'logs-endpoint.events.process-*',
-                  registry: 'logs-endpoint.events.registry-*',
-                  security: 'logs-endpoint.events.security-*',
-                  telemetry: 'metrics-endpoint.telemetry-*',
-                },
-                name: 'endpoint',
-                version: '0.5.0',
-                internal: false,
-                removable: false,
-              },
-              references: [],
-              updated_at: '2020-06-24T14:41:23.098Z',
-              version: 'Wzc0LDFd',
-              score: 0,
-            },
-          },
-        ],
-        success: true,
-      });
+      http.get.mockReturnValue(apiPathMockResponseProviders[INGEST_API_EPM_PACKAGES]());
       await sendGetEndpointSecurityPackage(http);
       expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/epm/packages', {
         query: { category: 'security' },
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts
index 0f0d1cb1b559d..46f84d296bd4e 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts
@@ -16,6 +16,82 @@ import {
 
 const generator = new EndpointDocGenerator('policy-list');
 
+/**
+ * a list of API paths response mock providers
+ */
+export const apiPathMockResponseProviders = {
+  [INGEST_API_EPM_PACKAGES]: () =>
+    Promise.resolve<GetPackagesResponse>({
+      response: [
+        {
+          name: 'endpoint',
+          title: 'Elastic Endpoint',
+          version: '0.5.0',
+          description: 'This is the Elastic Endpoint package.',
+          type: 'solution',
+          download: '/epr/endpoint/endpoint-0.5.0.tar.gz',
+          path: '/package/endpoint/0.5.0',
+          icons: [
+            {
+              src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg',
+              size: '16x16',
+              type: 'image/svg+xml',
+            },
+          ],
+          status: 'installed' as InstallationStatus,
+          savedObject: {
+            type: 'epm-packages',
+            id: 'endpoint',
+            attributes: {
+              installed: [
+                { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' },
+                { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
+                { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
+                { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
+                { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
+                { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' },
+                { id: 'logs-endpoint.alerts', type: 'index-template' },
+                { id: 'events-endpoint', type: 'index-template' },
+                { id: 'logs-endpoint.events.file', type: 'index-template' },
+                { id: 'logs-endpoint.events.library', type: 'index-template' },
+                { id: 'metrics-endpoint.metadata', type: 'index-template' },
+                { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' },
+                { id: 'logs-endpoint.events.network', type: 'index-template' },
+                { id: 'metrics-endpoint.policy', type: 'index-template' },
+                { id: 'logs-endpoint.events.process', type: 'index-template' },
+                { id: 'logs-endpoint.events.registry', type: 'index-template' },
+                { id: 'logs-endpoint.events.security', type: 'index-template' },
+                { id: 'metrics-endpoint.telemetry', type: 'index-template' },
+              ] as AssetReference[],
+              es_index_patterns: {
+                alerts: 'logs-endpoint.alerts-*',
+                events: 'events-endpoint-*',
+                file: 'logs-endpoint.events.file-*',
+                library: 'logs-endpoint.events.library-*',
+                metadata: 'metrics-endpoint.metadata-*',
+                metadata_mirror: 'metrics-endpoint.metadata_mirror-*',
+                network: 'logs-endpoint.events.network-*',
+                policy: 'metrics-endpoint.policy-*',
+                process: 'logs-endpoint.events.process-*',
+                registry: 'logs-endpoint.events.registry-*',
+                security: 'logs-endpoint.events.security-*',
+                telemetry: 'metrics-endpoint.telemetry-*',
+              },
+              name: 'endpoint',
+              version: '0.5.0',
+              internal: false,
+              removable: false,
+            },
+            references: [],
+            updated_at: '2020-06-24T14:41:23.098Z',
+            version: 'Wzc0LDFd',
+          },
+        },
+      ],
+      success: true,
+    }),
+};
+
 /**
  * It sets the mock implementation on the necessary http methods to support the policy list view
  * @param mockedHttpService
@@ -38,76 +114,8 @@ export const setPolicyListApiMockImplementation = (
         });
       }
 
-      if (path === INGEST_API_EPM_PACKAGES) {
-        return Promise.resolve<GetPackagesResponse>({
-          response: [
-            {
-              name: 'endpoint',
-              title: 'Elastic Endpoint',
-              version: '0.5.0',
-              description: 'This is the Elastic Endpoint package.',
-              type: 'solution',
-              download: '/epr/endpoint/endpoint-0.5.0.tar.gz',
-              path: '/package/endpoint/0.5.0',
-              icons: [
-                {
-                  src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg',
-                  size: '16x16',
-                  type: 'image/svg+xml',
-                },
-              ],
-              status: 'installed' as InstallationStatus,
-              savedObject: {
-                type: 'epm-packages',
-                id: 'endpoint',
-                attributes: {
-                  installed: [
-                    { id: '826759f0-7074-11ea-9bc8-6b38f4d29a16', type: 'dashboard' },
-                    { id: '1cfceda0-728b-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
-                    { id: '1e525190-7074-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
-                    { id: '55387750-729c-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
-                    { id: '92b1edc0-706a-11ea-9bc8-6b38f4d29a16', type: 'visualization' },
-                    { id: 'a3a3bd10-706b-11ea-9bc8-6b38f4d29a16', type: 'map' },
-                    { id: 'logs-endpoint.alerts', type: 'index-template' },
-                    { id: 'events-endpoint', type: 'index-template' },
-                    { id: 'logs-endpoint.events.file', type: 'index-template' },
-                    { id: 'logs-endpoint.events.library', type: 'index-template' },
-                    { id: 'metrics-endpoint.metadata', type: 'index-template' },
-                    { id: 'metrics-endpoint.metadata_mirror', type: 'index-template' },
-                    { id: 'logs-endpoint.events.network', type: 'index-template' },
-                    { id: 'metrics-endpoint.policy', type: 'index-template' },
-                    { id: 'logs-endpoint.events.process', type: 'index-template' },
-                    { id: 'logs-endpoint.events.registry', type: 'index-template' },
-                    { id: 'logs-endpoint.events.security', type: 'index-template' },
-                    { id: 'metrics-endpoint.telemetry', type: 'index-template' },
-                  ] as AssetReference[],
-                  es_index_patterns: {
-                    alerts: 'logs-endpoint.alerts-*',
-                    events: 'events-endpoint-*',
-                    file: 'logs-endpoint.events.file-*',
-                    library: 'logs-endpoint.events.library-*',
-                    metadata: 'metrics-endpoint.metadata-*',
-                    metadata_mirror: 'metrics-endpoint.metadata_mirror-*',
-                    network: 'logs-endpoint.events.network-*',
-                    policy: 'metrics-endpoint.policy-*',
-                    process: 'logs-endpoint.events.process-*',
-                    registry: 'logs-endpoint.events.registry-*',
-                    security: 'logs-endpoint.events.security-*',
-                    telemetry: 'metrics-endpoint.telemetry-*',
-                  },
-                  name: 'endpoint',
-                  version: '0.5.0',
-                  internal: false,
-                  removable: false,
-                },
-                references: [],
-                updated_at: '2020-06-24T14:41:23.098Z',
-                version: 'Wzc0LDFd',
-              },
-            },
-          ],
-          success: true,
-        });
+      if (apiPathMockResponseProviders[path]) {
+        return apiPathMockResponseProviders[path]();
       }
     }
     return Promise.reject(new Error(`MOCK: unknown policy list api: ${path}`));
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
index 315e3d29b6df2..984639f0f599d 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx
@@ -9,8 +9,9 @@ import { mount } from 'enzyme';
 
 import { PolicyDetails } from './policy_details';
 import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
-import { createAppRootMockRenderer } from '../../../../common/mock/endpoint';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
 import { getPolicyDetailPath, getPoliciesPath } from '../../../common/routing';
+import { apiPathMockResponseProviders } from '../store/policy_list/test_mock_utils';
 
 describe('Policy Details', () => {
   type FindReactWrapperResponse = ReturnType<ReturnType<typeof render>['find']>;
@@ -19,29 +20,50 @@ describe('Policy Details', () => {
   const policyListPathUrl = getPoliciesPath();
   const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms));
   const generator = new EndpointDocGenerator();
-  const { history, AppWrapper, coreStart } = createAppRootMockRenderer();
-  const http = coreStart.http;
-  const render = (ui: Parameters<typeof mount>[0]) => mount(ui, { wrappingComponent: AppWrapper });
+  let history: AppContextTestRender['history'];
+  let coreStart: AppContextTestRender['coreStart'];
+  let middlewareSpy: AppContextTestRender['middlewareSpy'];
+  let http: typeof coreStart.http;
+  let render: (ui: Parameters<typeof mount>[0]) => ReturnType<typeof mount>;
   let policyDatasource: ReturnType<typeof generator.generatePolicyDatasource>;
   let policyView: ReturnType<typeof render>;
 
-  beforeEach(() => jest.clearAllMocks());
+  beforeEach(() => {
+    const appContextMockRenderer = createAppRootMockRenderer();
+    const AppWrapper = appContextMockRenderer.AppWrapper;
+
+    ({ history, coreStart, middlewareSpy } = appContextMockRenderer);
+    render = (ui) => mount(ui, { wrappingComponent: AppWrapper });
+    http = coreStart.http;
+  });
 
   afterEach(() => {
     if (policyView) {
       policyView.unmount();
     }
+    jest.clearAllMocks();
   });
 
   describe('when displayed with invalid id', () => {
+    let releaseApiFailure: () => void;
     beforeEach(() => {
-      http.get.mockReturnValue(Promise.reject(new Error('policy not found')));
+      http.get.mockImplementationOnce(async () => {
+        await new Promise((_, reject) => {
+          releaseApiFailure = reject.bind(null, new Error('policy not found'));
+        });
+      });
       history.push(policyDetailsPathUrl);
       policyView = render(<PolicyDetails />);
     });
 
-    it('should show loader followed by error message', () => {
+    it('should NOT display timeline', async () => {
+      expect(policyView.find('flyoutOverlay')).toHaveLength(0);
+    });
+
+    it('should show loader followed by error message', async () => {
       expect(policyView.find('EuiLoadingSpinner').length).toBe(1);
+      releaseApiFailure();
+      await middlewareSpy.waitForAction('serverFailedToReturnPolicyDetailsData');
       policyView.update();
       const callout = policyView.find('EuiCallOut');
       expect(callout).toHaveLength(1);
@@ -76,14 +98,25 @@ describe('Policy Details', () => {
               success: true,
             });
           }
+
+          // Get package data
+          // Used in tests that route back to the list
+          if (apiPathMockResponseProviders[path]) {
+            asyncActions = asyncActions.then(async () => sleep());
+            return apiPathMockResponseProviders[path]();
+          }
         }
 
-        return Promise.reject(new Error('unknown API call!'));
+        return Promise.reject(new Error(`unknown API call (not MOCKED): ${path}`));
       });
       history.push(policyDetailsPathUrl);
       policyView = render(<PolicyDetails />);
     });
 
+    it('should NOT display timeline', async () => {
+      expect(policyView.find('flyoutOverlay')).toHaveLength(0);
+    });
+
     it('should display back to list button and policy title', () => {
       policyView.update();
       const pageHeaderLeft = policyView.find(
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx
index acce5c8f78350..32de3c93ac98f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx
@@ -23,6 +23,12 @@ describe('when on the policies page', () => {
     render = () => mockedContext.render(<PolicyList />);
   });
 
+  it('should NOT display timeline', async () => {
+    const renderResult = render();
+    const timelineFlyout = await renderResult.queryByTestId('flyoutOverlay');
+    expect(timelineFlyout).toBeNull();
+  });
+
   it('should show the empty state', async () => {
     const renderResult = render();
     const table = await renderResult.findByTestId('emptyPolicyTable');

From e4aaed6926390aece0a4d5114fbe9a3e3e543f51 Mon Sep 17 00:00:00 2001
From: Brian Seeders <seeders@gmail.com>
Date: Fri, 26 Jun 2020 15:06:49 -0400
Subject: [PATCH 12/21] skip failing suite (#70104) (#70103)

---
 .../plugins/lens/public/indexpattern_datasource/loader.test.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
index 5e59627d8c335..d8d8ebcf12de4 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
@@ -177,7 +177,8 @@ function mockClient() {
   } as unknown) as Pick<SavedObjectsClientContract, 'find' | 'bulkGet'>;
 }
 
-describe('loader', () => {
+// Failing: See https://github.com/elastic/kibana/issues/70104
+describe.skip('loader', () => {
   describe('loadIndexPatterns', () => {
     it('should not load index patterns that are already loaded', async () => {
       const cache = await loadIndexPatterns({

From 938733e8628387f34d7197883a1bd4fe77ad94cd Mon Sep 17 00:00:00 2001
From: Chris Cowan <chris@chriscowan.us>
Date: Fri, 26 Jun 2020 12:55:36 -0700
Subject: [PATCH 13/21] [Metrics UI] Fix EuiTheme type issue (#69735)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 .../components/autocomplete_field/suggestion_item.tsx  | 10 +++++-----
 .../public/components/logging/log_highlights_menu.tsx  |  2 +-
 .../inventory_view/components/dropdown_button.tsx      | 10 +++++-----
 .../waffle/metric_control/custom_metric_form.tsx       | 10 +++++-----
 .../waffle/metric_control/metrics_edit_mode.tsx        |  4 ++--
 .../components/waffle/metric_control/mode_switcher.tsx |  4 ++--
 .../metrics/inventory_view/components/waffle/node.tsx  |  4 ++--
 .../components/waffle/waffle_sort_controls.tsx         |  6 +++---
 .../components/waffle/waffle_time_controls.tsx         |  8 ++++----
 .../infra/public/pages/metrics/metric_detail/index.tsx |  2 +-
 10 files changed, 30 insertions(+), 30 deletions(-)

diff --git a/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx
index f14494a8abc49..4bcb7a7ec8a02 100644
--- a/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx
+++ b/x-pack/plugins/infra/public/components/autocomplete_field/suggestion_item.tsx
@@ -104,15 +104,15 @@ const getEuiIconType = (suggestionType: QuerySuggestionTypes) => {
 const getEuiIconColor = (theme: any, suggestionType: QuerySuggestionTypes): string => {
   switch (suggestionType) {
     case QuerySuggestionTypes.Field:
-      return theme.eui.euiColorVis7;
+      return theme?.eui.euiColorVis7;
     case QuerySuggestionTypes.Value:
-      return theme.eui.euiColorVis0;
+      return theme?.eui.euiColorVis0;
     case QuerySuggestionTypes.Operator:
-      return theme.eui.euiColorVis1;
+      return theme?.eui.euiColorVis1;
     case QuerySuggestionTypes.Conjunction:
-      return theme.eui.euiColorVis2;
+      return theme?.eui.euiColorVis2;
     case QuerySuggestionTypes.RecentSearch:
     default:
-      return theme.eui.euiColorMediumShade;
+      return theme?.eui.euiColorMediumShade;
   }
 };
diff --git a/x-pack/plugins/infra/public/components/logging/log_highlights_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_highlights_menu.tsx
index 608a22a79c473..7beead461cb2e 100644
--- a/x-pack/plugins/infra/public/components/logging/log_highlights_menu.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_highlights_menu.tsx
@@ -166,7 +166,7 @@ const goToNextHighlightLabel = i18n.translate(
 const ActiveHighlightsIndicator = euiStyled(EuiIcon).attrs(({ theme }) => ({
   type: 'checkInCircleFilled',
   size: 'm',
-  color: theme.eui.euiColorAccent,
+  color: theme?.eui.euiColorAccent,
 }))`
   padding-left: ${(props) => props.theme.eui.paddingSizes.xs};
 `;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx
index f0bc404dc3797..6e3ebee2dcb4b 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx
@@ -11,7 +11,7 @@ import { withTheme, EuiTheme } from '../../../../../../observability/public';
 interface Props {
   label: string;
   onClick: () => void;
-  theme: EuiTheme;
+  theme: EuiTheme | undefined;
   children: ReactNode;
 }
 
@@ -21,18 +21,18 @@ export const DropdownButton = withTheme(({ onClick, label, theme, children }: Pr
       alignItems="center"
       gutterSize="none"
       style={{
-        border: theme.eui.euiFormInputGroupBorder,
-        boxShadow: `0px 3px 2px ${theme.eui.euiTableActionsBorderColor}, 0px 1px 1px ${theme.eui.euiTableActionsBorderColor}`,
+        border: theme?.eui.euiFormInputGroupBorder,
+        boxShadow: `0px 3px 2px ${theme?.eui.euiTableActionsBorderColor}, 0px 1px 1px ${theme?.eui.euiTableActionsBorderColor}`,
       }}
     >
       <EuiFlexItem
         grow={false}
         style={{
           padding: 12,
-          background: theme.eui.euiFormInputGroupLabelBackground,
+          background: theme?.eui.euiFormInputGroupLabelBackground,
           fontSize: '0.75em',
           fontWeight: 600,
-          color: theme.eui.euiTitleColor,
+          color: theme?.eui.euiTitleColor,
         }}
       >
         {label}
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx
index 4e7bdeddd6246..a785cb31c3cf4 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx
@@ -52,7 +52,7 @@ const AGGREGATION_LABELS = {
 };
 
 interface Props {
-  theme: EuiTheme;
+  theme: EuiTheme | undefined;
   metric?: SnapshotCustomMetricInput;
   fields: IFieldType[];
   customMetrics: SnapshotCustomMetricInput[];
@@ -158,8 +158,8 @@ export const CustomMetricForm = withTheme(
           </EuiPopoverTitle>
           <div
             style={{
-              padding: theme.eui.paddingSizes.m,
-              borderBottom: `${theme.eui.euiBorderWidthThin} solid ${theme.eui.euiBorderColor}`,
+              padding: theme?.eui.paddingSizes.m,
+              borderBottom: `${theme?.eui.euiBorderWidthThin} solid ${theme?.eui.euiBorderColor}`,
             }}
           >
             <EuiFormRow
@@ -219,11 +219,11 @@ export const CustomMetricForm = withTheme(
               />
             </EuiFormRow>
           </div>
-          <div style={{ padding: theme.eui.paddingSizes.m, textAlign: 'right' }}>
+          <div style={{ padding: theme?.eui.paddingSizes.m, textAlign: 'right' }}>
             <EuiButtonEmpty
               onClick={onCancel}
               size="s"
-              style={{ paddingRight: theme.eui.paddingSizes.xl }}
+              style={{ paddingRight: theme?.eui.paddingSizes.xl }}
             >
               <FormattedMessage
                 id="xpack.infra.waffle.customMetrics.cancelLabel"
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx
index 89edaacdb8a1d..649dcc4282d67 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx
@@ -14,7 +14,7 @@ import {
 } from '../../../../../../../../../legacy/common/eui_styled_components';
 
 interface Props {
-  theme: EuiTheme;
+  theme: EuiTheme | undefined;
   customMetrics: SnapshotCustomMetricInput[];
   options: Array<{ text: string; value: string }>;
   onEdit: (metric: SnapshotCustomMetricInput) => void;
@@ -28,7 +28,7 @@ export const MetricsEditMode = withTheme(
       <div style={{ width: 256 }}>
         {options.map((option) => (
           <div key={option.value} style={{ padding: '14px 14px 13px 36px' }}>
-            <span style={{ color: theme.eui.euiButtonColorDisabled }}>{option.text}</span>
+            <span style={{ color: theme?.eui.euiButtonColorDisabled }}>{option.text}</span>
           </div>
         ))}
         {customMetrics.map((metric) => (
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx
index acb740f1750c8..d1abcade5d660 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx
@@ -15,7 +15,7 @@ import {
 } from '../../../../../../../../../legacy/common/eui_styled_components';
 
 interface Props {
-  theme: EuiTheme;
+  theme: EuiTheme | undefined;
   onEdit: () => void;
   onAdd: () => void;
   onSave: () => void;
@@ -32,7 +32,7 @@ export const ModeSwitcher = withTheme(
     return (
       <div
         style={{
-          borderTop: `${theme.eui.euiBorderWidthThin} solid ${theme.eui.euiBorderColor}`,
+          borderTop: `${theme?.eui.euiBorderWidthThin} solid ${theme?.eui.euiBorderColor}`,
           padding: 12,
         }}
       >
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx
index 5f526197cbfb9..e7bee82a9f0fe 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx
@@ -152,8 +152,8 @@ const ValueInner = euiStyled.button`
   border: none;
   &:focus {
     outline: none !important;
-    border: ${(params) => params.theme.eui.euiFocusRingSize} solid
-      ${(params) => params.theme.eui.euiFocusRingColor};
+    border: ${(params) => params.theme?.eui.euiFocusRingSize} solid
+      ${(params) => params.theme?.eui.euiFocusRingColor};
     box-shadow: none;
   }
 `;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx
index b5e6aacd0e6f4..a45ac0cee72d9 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_sort_controls.tsx
@@ -107,7 +107,7 @@ export const WaffleSortControls = ({ sort, onChange }: Props) => {
 };
 
 interface SwitchContainerProps {
-  theme: EuiTheme;
+  theme: EuiTheme | undefined;
   children: ReactNode;
 }
 
@@ -115,8 +115,8 @@ const SwitchContainer = withTheme(({ children, theme }: SwitchContainerProps) =>
   return (
     <div
       style={{
-        padding: theme.eui.paddingSizes.m,
-        borderTop: `1px solid ${theme.eui.euiBorderColor}`,
+        padding: theme?.eui.paddingSizes.m,
+        borderTop: `1px solid ${theme?.eui.euiBorderColor}`,
       }}
     >
       {children}
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx
index fac1e086101e9..da044b1cf99ee 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx
@@ -12,7 +12,7 @@ import { withTheme, EuiTheme } from '../../../../../../../observability/public';
 import { useWaffleTimeContext } from '../../hooks/use_waffle_time';
 
 interface Props {
-  theme: EuiTheme;
+  theme: EuiTheme | undefined;
 }
 
 export const WaffleTimeControls = withTheme(({ theme }: Props) => {
@@ -56,9 +56,9 @@ export const WaffleTimeControls = withTheme(({ theme }: Props) => {
       <EuiFlexItem
         grow={false}
         style={{
-          border: theme.eui.euiFormInputGroupBorder,
-          boxShadow: `0px 3px 2px ${theme.eui.euiTableActionsBorderColor}, 0px 1px 1px ${theme.eui.euiTableActionsBorderColor}`,
-          marginRight: theme.eui.paddingSizes.m,
+          border: theme?.eui.euiFormInputGroupBorder,
+          boxShadow: `0px 3px 2px ${theme?.eui.euiTableActionsBorderColor}, 0px 1px 1px ${theme?.eui.euiTableActionsBorderColor}`,
+          marginRight: theme?.eui.paddingSizes.m,
         }}
         data-test-subj="waffleDatePicker"
       >
diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx
index 4ae96f733382f..60c8041fb5ef0 100644
--- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx
@@ -27,7 +27,7 @@ const DetailPageContent = euiStyled(PageContent)`
 `;
 
 interface Props {
-  theme: EuiTheme;
+  theme: EuiTheme | undefined;
   match: {
     params: {
       type: string;

From 59925daff5b8bdedbe1a45fc316b771c2e506d21 Mon Sep 17 00:00:00 2001
From: Andrea Del Rio <delrio.andre@gmail.com>
Date: Fri, 26 Jun 2020 13:21:51 -0700
Subject: [PATCH 14/21] [Discover] Improve styling of graphs in sidebar
 (#69440)

---
 .../sidebar/discover_field_bucket.scss        |  4 ++
 .../sidebar/discover_field_bucket.tsx         | 41 +++++++++++++++----
 .../sidebar/discover_field_details.tsx        |  3 +-
 .../components/sidebar/discover_sidebar.scss  |  8 ----
 .../sidebar/string_progress_bar.tsx           | 29 +++----------
 5 files changed, 43 insertions(+), 42 deletions(-)
 create mode 100644 src/plugins/discover/public/application/components/sidebar/discover_field_bucket.scss

diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.scss b/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.scss
new file mode 100644
index 0000000000000..90b645f70084e
--- /dev/null
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.scss
@@ -0,0 +1,4 @@
+.dscFieldDetails__barContainer {
+  // Constrains value to the flex item, and allows for truncation when necessary
+  min-width: 0;
+}
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx
index 398a945e0f876..281fc9a392d7d 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field_bucket.tsx
@@ -17,11 +17,12 @@
  * under the License.
  */
 import React from 'react';
-import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
+import { EuiText, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
 import { StringFieldProgressBar } from './string_progress_bar';
 import { Bucket } from './types';
 import { IndexPatternField } from '../../../../../data/public';
+import './discover_field_bucket.scss';
 
 interface Props {
   bucket: Bucket;
@@ -47,18 +48,40 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
 
   return (
     <>
-      <EuiFlexGroup gutterSize="xs" responsive={false}>
-        <EuiFlexItem className="eui-textTruncate">
-          <EuiText size="xs" className="eui-textTruncate">
-            {bucket.display === '' ? emptyTxt : bucket.display}
-          </EuiText>
+      <EuiFlexGroup justifyContent="spaceBetween" responsive={false} gutterSize="s">
+        <EuiFlexItem className="dscFieldDetails__barContainer" grow={1}>
+          <EuiFlexGroup justifyContent="spaceBetween" gutterSize="xs" responsive={false}>
+            <EuiFlexItem grow={1} className="eui-textTruncate">
+              <EuiText
+                title={
+                  bucket.display === ''
+                    ? emptyTxt
+                    : `${bucket.display}: ${bucket.count} (${bucket.percent}%)`
+                }
+                size="xs"
+                className="eui-textTruncate"
+              >
+                {bucket.display === '' ? emptyTxt : bucket.display}
+              </EuiText>
+            </EuiFlexItem>
+            <EuiFlexItem grow={false} className="eui-textTruncate">
+              <EuiText color="secondary" size="xs" className="eui-textTruncate">
+                {bucket.percent}%
+              </EuiText>
+            </EuiFlexItem>
+          </EuiFlexGroup>
+          <StringFieldProgressBar
+            value={bucket.value}
+            percent={bucket.percent}
+            count={bucket.count}
+          />
         </EuiFlexItem>
         {field.filterable && (
           <EuiFlexItem grow={false}>
             <div>
               <EuiButtonIcon
                 iconSize="s"
-                iconType="magnifyWithPlus"
+                iconType="plusInCircle"
                 onClick={() => onAddFilter(field, bucket.value, '+')}
                 aria-label={addLabel}
                 data-test-subj={`plus-${field.name}-${bucket.value}`}
@@ -73,7 +96,7 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
               />
               <EuiButtonIcon
                 iconSize="s"
-                iconType="magnifyWithMinus"
+                iconType="minusInCircle"
                 onClick={() => onAddFilter(field, bucket.value, '-')}
                 aria-label={removeLabel}
                 data-test-subj={`minus-${field.name}-${bucket.value}`}
@@ -90,7 +113,7 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
           </EuiFlexItem>
         )}
       </EuiFlexGroup>
-      <StringFieldProgressBar percent={bucket.percent} count={bucket.count} />
+      <EuiSpacer size="s" />
     </>
   );
 }
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx
index b56f7ba8a852f..dd95a45f71626 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import React from 'react';
-import { EuiLink, EuiSpacer, EuiIconTip, EuiText } from '@elastic/eui';
+import { EuiLink, EuiIconTip, EuiText } from '@elastic/eui';
 import { FormattedMessage } from '@kbn/i18n/react';
 import { DiscoverFieldBucket } from './discover_field_bucket';
 import { getWarnings } from './lib/get_warnings';
@@ -78,7 +78,6 @@ export function DiscoverFieldDetails({
 
       {details.visualizeUrl && (
         <>
-          <EuiSpacer size={'s'} />
           <EuiLink
             onClick={() => {
               getServices().core.application.navigateToApp(details.visualizeUrl.app, {
diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss
index 9f7700c7f395c..ae7e915f09773 100644
--- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss
+++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss
@@ -121,14 +121,6 @@
   }
 }
 
-/*
-  Fixes EUI known issue https://github.com/elastic/eui/issues/1749
-*/
-.dscProgressBarTooltip__anchor {
-  display: block;
-}
-
-
 .dscFieldSearch {
   padding: $euiSizeS;
 }
diff --git a/src/plugins/discover/public/application/components/sidebar/string_progress_bar.tsx b/src/plugins/discover/public/application/components/sidebar/string_progress_bar.tsx
index 7ea41aa4bf270..c8693727b0725 100644
--- a/src/plugins/discover/public/application/components/sidebar/string_progress_bar.tsx
+++ b/src/plugins/discover/public/application/components/sidebar/string_progress_bar.tsx
@@ -17,35 +17,18 @@
  * under the License.
  */
 import React from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiText, EuiToolTip } from '@elastic/eui';
+import { EuiProgress } from '@elastic/eui';
 
 interface Props {
   percent: number;
   count: number;
+  value: string;
 }
 
-export function StringFieldProgressBar(props: Props) {
+export function StringFieldProgressBar({ value, percent, count }: Props) {
+  const ariaLabel = `${value}: ${count} (${percent}%)`;
+
   return (
-    <EuiToolTip
-      anchorClassName="dscProgressBarTooltip__anchor"
-      content={props.count}
-      delay="regular"
-      position="right"
-    >
-      <EuiFlexGroup alignItems="center" responsive={false}>
-        <EuiFlexItem>
-          <EuiProgress
-            value={props.percent}
-            max={100}
-            color="secondary"
-            aria-hidden={true}
-            size="l"
-          />
-        </EuiFlexItem>
-        <EuiFlexItem grow={false}>
-          <EuiText size="xs">{props.percent}%</EuiText>
-        </EuiFlexItem>
-      </EuiFlexGroup>
-    </EuiToolTip>
+    <EuiProgress value={percent} max={100} color="secondary" aria-label={ariaLabel} size="s" />
   );
 }

From 295ac7ef121ee16e875e6f83c8abada85ca39483 Mon Sep 17 00:00:00 2001
From: Andrew Goldstein <andrew-goldstein@users.noreply.github.com>
Date: Fri, 26 Jun 2020 15:36:51 -0600
Subject: [PATCH 15/21] [Security] `Investigate in Resolver` Timeline
 Integration (#70111)

## [Security] `Investigate in Resolver` Timeline Integration

This PR adds a new `Investigate in Resolver` action to the Timeline, and all timeline-based views, including:

- Timeline
- Alert list (i.e. Signals)
- Hosts > Events
- Hosts > External alerts
- Network > External alerts

![investigate-in-resolver-action](https://user-images.githubusercontent.com/4459398/85886173-c40d1c80-b7a2-11ea-8011-0221fef95d51.png)

### Resolver Overlay

When the `Investigate in Resolver` action is clicked, Resolver is displayed in an overlay over the events. The screenshot below has placeholder text where Resolver will be rendered:

![resolver-overlay](https://user-images.githubusercontent.com/4459398/85886309-10f0f300-b7a3-11ea-95cb-0117207e4890.png)

The Resolver overlay is closed by clicking the `< Back to events` button shown in the screenshot above.

The state of the timeline is restored when the overlay is closed. The scroll position (within the events), any expanded events, etc, will appear exactly as they were before the Resolver overlay was displayed.

### Case Integration

Users may link directly to a Timeline Resolver view from cases via the `Attach to new case` and `Attach to existing case...` actions show in the screenshot below:

![case-integration](https://user-images.githubusercontent.com/4459398/85886773-e3587980-b7a3-11ea-87b6-b098ea14bc5f.png)

![investigate-in-resolver](https://user-images.githubusercontent.com/4459398/85885618-daff3f00-b7a1-11ea-9356-2e8a1291f213.gif)

When users click the link in a case, Timeline will automatically open to the Resolver view in the link.

### URL State

Users can directly share Resolver views (in saved Timelines) with other users by copying the Kibana URL to the clipboard when Resolver is open.

When another user pastes the URL in their browser, Timeline will automatically open and display the Resolver view in the URL.

### Enabling the `Investigate in Resolver` action

In this PR, the `Investigate in Resolver` action is only enabled for events where all of the following are true:

- `agent.type` is `endpoint`
- `process.entity_id` exists

### Context passed to Resolver

The only context passed to `Resolver` is the `_id` of the event (when the user clicks `Investigate in Resolver`)

### What's next?

- @oatkiller will replace the placeholder text shown in the screenshots above with the actual call to Resolver in a separate PR
- I will follow-up this PR with additional tests
- The action text `Investigate in Resolver` may be changed in a future PR
- Hide the `Add to case` action in timeline-based views (it's currently visible, but disabled)
---
 .../alerts_table/default_config.tsx           |   24 +-
 .../components/alerts_table/index.test.tsx    |   53 +-
 .../alerts/components/alerts_table/index.tsx  |    7 +-
 .../components/alerts_viewer/alerts_table.tsx |   10 +-
 .../events_viewer/events_viewer.tsx           |   14 +-
 .../navigation/breadcrumbs/index.test.ts      |    1 +
 .../components/navigation/index.test.tsx      |    4 +-
 .../navigation/tab_navigation/index.test.tsx  |    2 +
 .../common/components/url_state/helpers.ts    |    3 +-
 .../url_state/initialize_redux_by_url.tsx     |    1 +
 .../public/graphql/introspection.json         |   35 +
 .../security_solution/public/graphql/types.ts |   18 +
 .../fields_browser/categories_pane.tsx        |    6 +-
 .../fields_browser/field_browser.tsx          |    6 +-
 .../components/fields_browser/index.tsx       |    1 +
 .../components/flyout/header/index.tsx        |    6 +-
 .../components/graph_overlay/index.tsx        |  150 ++
 .../components/graph_overlay/translations.ts  |   14 +
 .../components/open_timeline/helpers.ts       |    3 +
 .../__snapshots__/timeline.test.tsx.snap      | 1778 +++++++++--------
 .../timeline/body/actions/index.tsx           |   40 +-
 .../__snapshots__/index.test.tsx.snap         |    4 +-
 .../timeline/body/column_headers/index.tsx    |   35 +-
 .../components/timeline/body/constants.ts     |    6 +-
 .../body/events/event_column_view.tsx         |   17 +-
 .../components/timeline/body/helpers.ts       |   36 +-
 .../components/timeline/body/index.test.tsx   |    6 +
 .../components/timeline/body/index.tsx        |   19 +-
 .../timeline/body/stateful_body.tsx           |   13 +-
 .../components/timeline/body/translations.ts  |    7 +
 .../components/timeline/header/index.tsx      |   41 +-
 .../timelines/components/timeline/helpers.tsx |    2 +
 .../components/timeline/index.test.tsx        |    1 +
 .../timelines/components/timeline/index.tsx   |    5 +
 .../insert_timeline_popover/index.test.tsx    |    6 +-
 .../insert_timeline_popover/index.tsx         |   12 +-
 .../use_insert_timeline.tsx                   |   21 +-
 .../timeline/properties/helpers.test.tsx      |    1 +
 .../timeline/properties/helpers.tsx           |   49 +-
 .../timeline/properties/index.test.tsx        |   12 +-
 .../components/timeline/properties/index.tsx  |   14 +-
 .../timeline/properties/properties_right.tsx  |    3 +
 .../timeline/properties/translations.ts       |   16 +-
 .../timeline/selectable_timeline/index.tsx    |    9 +-
 .../timelines/components/timeline/styles.tsx  |   23 +-
 .../components/timeline/timeline.test.tsx     |    6 +-
 .../components/timeline/timeline.tsx          |   11 +
 .../timelines/containers/index.gql_query.ts   |    4 +
 .../timelines/store/timeline/actions.ts       |    4 +
 .../timelines/store/timeline/helpers.ts       |   20 +
 .../public/timelines/store/timeline/model.ts  |    4 +
 .../timelines/store/timeline/reducer.test.ts  |    2 +-
 .../timelines/store/timeline/reducer.ts       |    6 +
 .../public/timelines/store/timeline/types.ts  |    1 +
 .../server/graphql/ecs/schema.gql.ts          |    6 +
 .../security_solution/server/graphql/types.ts |   35 +
 .../server/lib/ecs_fields/index.ts            |    6 +
 57 files changed, 1615 insertions(+), 1024 deletions(-)
 create mode 100644 x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx
 create mode 100644 x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts

diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx
index 2029c5169c2cd..6d82897aaf010 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx
+++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/default_config.tsx
@@ -8,6 +8,7 @@
 
 import React from 'react';
 import ApolloClient from 'apollo-client';
+import { Dispatch } from 'redux';
 
 import { EuiText } from '@elastic/eui';
 import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
@@ -17,10 +18,12 @@ import {
   TimelineRowActionOnClick,
 } from '../../../timelines/components/timeline/body/actions';
 import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
+import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
 import {
   DEFAULT_COLUMN_MIN_WIDTH,
   DEFAULT_DATE_COLUMN_MIN_WIDTH,
 } from '../../../timelines/components/timeline/body/constants';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../timelines/components/timeline/helpers';
 import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model';
 import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
 
@@ -174,23 +177,27 @@ export const getAlertActions = ({
   apolloClient,
   canUserCRUD,
   createTimeline,
+  dispatch,
   hasIndexWrite,
   onAlertStatusUpdateFailure,
   onAlertStatusUpdateSuccess,
   setEventsDeleted,
   setEventsLoading,
   status,
+  timelineId,
   updateTimelineIsLoading,
 }: {
   apolloClient?: ApolloClient<{}>;
   canUserCRUD: boolean;
   createTimeline: CreateTimeline;
+  dispatch: Dispatch;
   hasIndexWrite: boolean;
   onAlertStatusUpdateFailure: (status: Status, error: Error) => void;
   onAlertStatusUpdateSuccess: (count: number, status: Status) => void;
   setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
   setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void;
   status: Status;
+  timelineId: string;
   updateTimelineIsLoading: UpdateTimelineLoading;
 }): TimelineRowAction[] => {
   const openAlertActionComponent: TimelineRowAction = {
@@ -199,7 +206,7 @@ export const getAlertActions = ({
     dataTestSubj: 'open-alert-status',
     displayType: 'contextMenu',
     id: FILTER_OPEN,
-    isActionDisabled: !canUserCRUD || !hasIndexWrite,
+    isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
     onClick: ({ eventId }: TimelineRowActionOnClick) =>
       updateAlertStatusAction({
         alertIds: [eventId],
@@ -210,7 +217,7 @@ export const getAlertActions = ({
         status,
         selectedStatus: FILTER_OPEN,
       }),
-    width: 26,
+    width: DEFAULT_ICON_BUTTON_WIDTH,
   };
 
   const closeAlertActionComponent: TimelineRowAction = {
@@ -219,7 +226,7 @@ export const getAlertActions = ({
     dataTestSubj: 'close-alert-status',
     displayType: 'contextMenu',
     id: FILTER_CLOSED,
-    isActionDisabled: !canUserCRUD || !hasIndexWrite,
+    isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
     onClick: ({ eventId }: TimelineRowActionOnClick) =>
       updateAlertStatusAction({
         alertIds: [eventId],
@@ -230,7 +237,7 @@ export const getAlertActions = ({
         status,
         selectedStatus: FILTER_CLOSED,
       }),
-    width: 26,
+    width: DEFAULT_ICON_BUTTON_WIDTH,
   };
 
   const inProgressAlertActionComponent: TimelineRowAction = {
@@ -239,7 +246,7 @@ export const getAlertActions = ({
     dataTestSubj: 'in-progress-alert-status',
     displayType: 'contextMenu',
     id: FILTER_IN_PROGRESS,
-    isActionDisabled: !canUserCRUD || !hasIndexWrite,
+    isActionDisabled: () => !canUserCRUD || !hasIndexWrite,
     onClick: ({ eventId }: TimelineRowActionOnClick) =>
       updateAlertStatusAction({
         alertIds: [eventId],
@@ -250,10 +257,13 @@ export const getAlertActions = ({
         status,
         selectedStatus: FILTER_IN_PROGRESS,
       }),
-    width: 26,
+    width: DEFAULT_ICON_BUTTON_WIDTH,
   };
 
   return [
+    {
+      ...getInvestigateInResolverAction({ dispatch, timelineId }),
+    },
     {
       ariaLabel: 'Send alert to timeline',
       content: i18n.ACTION_INVESTIGATE_IN_TIMELINE,
@@ -268,7 +278,7 @@ export const getAlertActions = ({
           ecsData,
           updateTimelineIsLoading,
         }),
-      width: 26,
+      width: DEFAULT_ICON_BUTTON_WIDTH,
     },
     // Context menu items
     ...(FILTER_OPEN !== status ? [openAlertActionComponent] : []),
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx
index f843bf6881846..9ff368aff2bf6 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.test.tsx
@@ -7,37 +7,40 @@
 import React from 'react';
 import { shallow } from 'enzyme';
 
+import { TestProviders } from '../../../common/mock/test_providers';
 import { TimelineId } from '../../../../common/types/timeline';
 import { AlertsTableComponent } from './index';
 
 describe('AlertsTableComponent', () => {
   it('renders correctly', () => {
     const wrapper = shallow(
-      <AlertsTableComponent
-        timelineId={TimelineId.test}
-        canUserCRUD
-        hasIndexWrite
-        from={0}
-        loading
-        signalsIndex="index"
-        to={1}
-        globalQuery={{
-          query: 'query',
-          language: 'language',
-        }}
-        globalFilters={[]}
-        deletedEventIds={[]}
-        loadingEventIds={[]}
-        selectedEventIds={{}}
-        isSelectAllChecked={false}
-        clearSelected={jest.fn()}
-        setEventsLoading={jest.fn()}
-        clearEventsLoading={jest.fn()}
-        setEventsDeleted={jest.fn()}
-        clearEventsDeleted={jest.fn()}
-        updateTimelineIsLoading={jest.fn()}
-        updateTimeline={jest.fn()}
-      />
+      <TestProviders>
+        <AlertsTableComponent
+          timelineId={TimelineId.test}
+          canUserCRUD
+          hasIndexWrite
+          from={0}
+          loading
+          signalsIndex="index"
+          to={1}
+          globalQuery={{
+            query: 'query',
+            language: 'language',
+          }}
+          globalFilters={[]}
+          deletedEventIds={[]}
+          loadingEventIds={[]}
+          selectedEventIds={{}}
+          isSelectAllChecked={false}
+          clearSelected={jest.fn()}
+          setEventsLoading={jest.fn()}
+          clearEventsLoading={jest.fn()}
+          setEventsDeleted={jest.fn()}
+          clearEventsDeleted={jest.fn()}
+          updateTimelineIsLoading={jest.fn()}
+          updateTimeline={jest.fn()}
+        />
+      </TestProviders>
     );
 
     expect(wrapper.find('[title="Alerts"]')).toBeTruthy();
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx
index ba6102312fef6..ec088c111e3bb 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/index.tsx
@@ -7,7 +7,7 @@
 import { EuiPanel, EuiLoadingContent } from '@elastic/eui';
 import { isEmpty } from 'lodash/fp';
 import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { connect, ConnectedProps } from 'react-redux';
+import { connect, ConnectedProps, useDispatch } from 'react-redux';
 import { Dispatch } from 'redux';
 
 import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
@@ -84,6 +84,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
   updateTimeline,
   updateTimelineIsLoading,
 }) => {
+  const dispatch = useDispatch();
   const [selectAll, setSelectAll] = useState(false);
   const apolloClient = useApolloClient();
 
@@ -292,11 +293,13 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
       getAlertActions({
         apolloClient,
         canUserCRUD,
+        dispatch,
         hasIndexWrite,
         createTimeline: createTimelineCallback,
         setEventsLoading: setEventsLoadingCallback,
         setEventsDeleted: setEventsDeletedCallback,
         status: filterGroup,
+        timelineId,
         updateTimelineIsLoading,
         onAlertStatusUpdateSuccess,
         onAlertStatusUpdateFailure,
@@ -305,10 +308,12 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
       apolloClient,
       canUserCRUD,
       createTimelineCallback,
+      dispatch,
       hasIndexWrite,
       filterGroup,
       setEventsLoadingCallback,
       setEventsDeletedCallback,
+      timelineId,
       updateTimelineIsLoading,
       onAlertStatusUpdateSuccess,
       onAlertStatusUpdateFailure,
diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
index 251e0278b11ba..6d5471404ab4d 100644
--- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx
@@ -5,13 +5,16 @@
  */
 
 import React, { useEffect, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
 
 import { Filter } from '../../../../../../../src/plugins/data/public';
 import { TimelineIdLiteral } from '../../../../common/types/timeline';
 import { StatefulEventsViewer } from '../events_viewer';
 import { alertsDefaultModel } from './default_headers';
 import { useManageTimeline } from '../../../timelines/components/manage_timeline';
+import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
 import * as i18n from './translations';
+
 export interface OwnProps {
   end: number;
   id: string;
@@ -64,8 +67,9 @@ const AlertsTableComponent: React.FC<Props> = ({
   startDate,
   pageFilters = [],
 }) => {
+  const dispatch = useDispatch();
   const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
-  const { initializeTimeline } = useManageTimeline();
+  const { initializeTimeline, setTimelineRowActions } = useManageTimeline();
 
   useEffect(() => {
     initializeTimeline({
@@ -75,6 +79,10 @@ const AlertsTableComponent: React.FC<Props> = ({
       title: i18n.ALERTS_TABLE_TITLE,
       unit: i18n.UNIT,
     });
+    setTimelineRowActions({
+      id: timelineId,
+      timelineRowActions: [getInvestigateInResolverAction({ dispatch, timelineId })],
+    });
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
   return (
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
index 6b4baac0ff26c..9e38b14c4334a 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx
@@ -7,6 +7,7 @@
 import { EuiPanel } from '@elastic/eui';
 import { getOr, isEmpty, union } from 'lodash/fp';
 import React, { useEffect, useMemo, useState } from 'react';
+import { useDispatch } from 'react-redux';
 import styled from 'styled-components';
 import deepEqual from 'fast-deep-equal';
 
@@ -34,6 +35,7 @@ import {
 } from '../../../../../../../src/plugins/data/public';
 import { inputsModel } from '../../store';
 import { useManageTimeline } from '../../../timelines/components/manage_timeline';
+import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers';
 
 const DEFAULT_EVENTS_VIEWER_HEIGHT = 500;
 
@@ -91,6 +93,7 @@ const EventsViewerComponent: React.FC<Props> = ({
   toggleColumn,
   utilityBar,
 }) => {
+  const dispatch = useDispatch();
   const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
   const kibana = useKibana();
   const { filterManager } = useKibana().services.data.query;
@@ -100,7 +103,16 @@ const EventsViewerComponent: React.FC<Props> = ({
     getManageTimelineById,
     setIsTimelineLoading,
     setTimelineFilterManager,
+    setTimelineRowActions,
   } = useManageTimeline();
+
+  useEffect(() => {
+    setTimelineRowActions({
+      id,
+      timelineRowActions: [getInvestigateInResolverAction({ dispatch, timelineId: id })],
+    });
+  }, [setTimelineRowActions, id, dispatch]);
+
   useEffect(() => {
     setIsTimelineLoading({ id, isLoading: isQueryLoading });
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -179,9 +191,7 @@ const EventsViewerComponent: React.FC<Props> = ({
                   <HeaderSection id={id} subtitle={utilityBar ? undefined : subtitle} title={title}>
                     {headerFilterGroup}
                   </HeaderSection>
-
                   {utilityBar?.(refetch, totalCountMinusDeleted)}
-
                   <EventsContainerLoading data-test-subj={`events-container-loading-${loading}`}>
                     <TimelineRefetch
                       id={id}
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
index 2c30f9a2e4ac3..ade76f8e24338 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts
@@ -74,6 +74,7 @@ const getMockObject = (
   timeline: {
     id: '',
     isOpen: false,
+    graphEventId: '',
   },
   timerange: {
     global: {
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
index cab4ef8ead63f..c2c94e192d9f7 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx
@@ -81,6 +81,7 @@ describe('SIEM Navigation', () => {
       [CONSTANTS.timeline]: {
         id: '',
         isOpen: false,
+        graphEventId: '',
       },
     },
   };
@@ -160,6 +161,7 @@ describe('SIEM Navigation', () => {
         timeline: {
           id: '',
           isOpen: false,
+          graphEventId: '',
         },
         timerange: {
           global: {
@@ -266,7 +268,7 @@ describe('SIEM Navigation', () => {
         search: '',
         state: undefined,
         tabName: 'authentications',
-        timeline: { id: '', isOpen: false },
+        timeline: { id: '', isOpen: false, graphEventId: '' },
         timerange: {
           global: {
             linkTo: ['timeline'],
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx
index 977c7808b6c86..f345346d620cb 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx
@@ -71,6 +71,7 @@ describe('Tab Navigation', () => {
       [CONSTANTS.timeline]: {
         id: '',
         isOpen: false,
+        graphEventId: '',
       },
     };
     test('it mounts with correct tab highlighted', () => {
@@ -128,6 +129,7 @@ describe('Tab Navigation', () => {
       [CONSTANTS.timeline]: {
         id: '',
         isOpen: false,
+        graphEventId: '',
       },
     };
     test('it mounts with correct tab highlighted', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
index c270a99d3c51e..7f4267bc5e2b3 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts
@@ -126,8 +126,9 @@ export const makeMapStateToProps = () => {
         ? {
             id: flyoutTimeline.savedObjectId != null ? flyoutTimeline.savedObjectId : '',
             isOpen: flyoutTimeline.show,
+            graphEventId: flyoutTimeline.graphEventId ?? '',
           }
-        : { id: '', isOpen: false };
+        : { id: '', isOpen: false, graphEventId: '' };
 
     let searchAttr: {
       [CONSTANTS.appQuery]?: Query;
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
index efd6221bbfbd0..ab03e2199474c 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx
@@ -81,6 +81,7 @@ export const dispatchSetInitialStateFromUrl = (
         queryTimelineById({
           apolloClient,
           duplicate: false,
+          graphEventId: timeline.graphEventId,
           timelineId: timeline.id,
           openTimeline: timeline.isOpen,
           updateIsLoading: updateTimelineIsLoading,
diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json
index 3c8c7c21d72a0..48547212bb6c0 100644
--- a/x-pack/plugins/security_solution/public/graphql/introspection.json
+++ b/x-pack/plugins/security_solution/public/graphql/introspection.json
@@ -3570,6 +3570,14 @@
             "isDeprecated": false,
             "deprecationReason": null
           },
+          {
+            "name": "agent",
+            "description": "",
+            "args": [],
+            "type": { "kind": "OBJECT", "name": "AgentEcsField", "ofType": null },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
           {
             "name": "auditd",
             "description": "",
@@ -3760,6 +3768,25 @@
         "enumValues": null,
         "possibleTypes": null
       },
+      {
+        "kind": "OBJECT",
+        "name": "AgentEcsField",
+        "description": "",
+        "fields": [
+          {
+            "name": "type",
+            "description": "",
+            "args": [],
+            "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null },
+            "isDeprecated": false,
+            "deprecationReason": null
+          }
+        ],
+        "inputFields": null,
+        "interfaces": [],
+        "enumValues": null,
+        "possibleTypes": null
+      },
       {
         "kind": "OBJECT",
         "name": "AuditdEcsFields",
@@ -5728,6 +5755,14 @@
             "isDeprecated": false,
             "deprecationReason": null
           },
+          {
+            "name": "entity_id",
+            "description": "",
+            "args": [],
+            "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
           {
             "name": "executable",
             "description": "",
diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts
index dc4a8ae78bf46..b5088fe51b446 100644
--- a/x-pack/plugins/security_solution/public/graphql/types.ts
+++ b/x-pack/plugins/security_solution/public/graphql/types.ts
@@ -763,6 +763,8 @@ export interface Ecs {
 
   _index?: Maybe<string>;
 
+  agent?: Maybe<AgentEcsField>;
+
   auditd?: Maybe<AuditdEcsFields>;
 
   destination?: Maybe<DestinationEcsFields>;
@@ -810,6 +812,10 @@ export interface Ecs {
   system?: Maybe<SystemEcsField>;
 }
 
+export interface AgentEcsField {
+  type?: Maybe<string[]>;
+}
+
 export interface AuditdEcsFields {
   result?: Maybe<string[]>;
 
@@ -1265,6 +1271,8 @@ export interface ProcessEcsFields {
 
   args?: Maybe<string[]>;
 
+  entity_id?: Maybe<string[]>;
+
   executable?: Maybe<string[]>;
 
   title?: Maybe<string[]>;
@@ -4605,6 +4613,8 @@ export namespace GetTimelineQuery {
 
     event: Maybe<Event>;
 
+    agent: Maybe<Agent>;
+
     auditd: Maybe<Auditd>;
 
     file: Maybe<File>;
@@ -4730,6 +4740,12 @@ export namespace GetTimelineQuery {
     type: Maybe<string[]>;
   };
 
+  export type Agent = {
+    __typename?: 'AgentEcsField';
+
+    type: Maybe<string[]>;
+  };
+
   export type Auditd = {
     __typename?: 'AuditdEcsFields';
 
@@ -5155,6 +5171,8 @@ export namespace GetTimelineQuery {
 
     args: Maybe<string[]>;
 
+    entity_id: Maybe<string[]>;
+
     executable: Maybe<string[]>;
 
     title: Maybe<string[]>;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
index 480070fda9594..7addfaaf7c5fc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/categories_pane.tsx
@@ -32,6 +32,10 @@ const Title = styled(EuiTitle)`
   padding-left: 5px;
 `;
 
+const H5 = styled.h5`
+  text-align: left;
+`;
+
 Title.displayName = 'Title';
 
 type Props = Pick<FieldBrowserProps, 'browserFields' | 'timelineId' | 'onUpdateColumns'> & {
@@ -64,7 +68,7 @@ export const CategoriesPane = React.memo<Props>(
   }) => (
     <>
       <Title size="xxs">
-        <h5 data-test-subj="categories-pane-title">{i18n.CATEGORIES}</h5>
+        <H5 data-test-subj="categories-pane-title">{i18n.CATEGORIES}</H5>
       </Title>
 
       <CategoryNames
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
index 8f538e03835f8..07c4893e4550b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx
@@ -29,11 +29,11 @@ const FieldsBrowserContainer = styled.div<{ width: number }>`
   border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid
     ${({ theme }) => theme.eui.euiColorMediumShade};
   border-radius: ${({ theme }) => theme.eui.euiBorderRadius};
-  left: 0;
+  left: 8px;
   padding: ${({ theme }) => theme.eui.paddingSizes.s} ${({ theme }) => theme.eui.paddingSizes.s}
-    ${({ theme }) => theme.eui.paddingSizes.m};
+    ${({ theme }) => theme.eui.paddingSizes.s};
   position: absolute;
-  top: calc(100% + ${({ theme }) => theme.eui.euiSize});
+  top: calc(100% + 4px);
   width: ${({ width }) => width}px;
   z-index: 9990;
 `;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
index a3e93ff3c90eb..a3937107936b6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx
@@ -26,6 +26,7 @@ export const INPUT_TIMEOUT = 250;
 
 const FieldsBrowserButtonContainer = styled.div`
   position: relative;
+  width: 24px;
 `;
 
 FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index 8ad32d6e2cad0..9fe48cd2f0190 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -33,6 +33,7 @@ const StatefulFlyoutHeader = React.memo<Props>(
     associateNote,
     createTimeline,
     description,
+    graphEventId,
     isDataInTimeline,
     isDatepickerLocked,
     isFavorite,
@@ -58,6 +59,7 @@ const StatefulFlyoutHeader = React.memo<Props>(
         createTimeline={createTimeline}
         description={description}
         getNotesByIds={getNotesByIds}
+        graphEventId={graphEventId}
         isDataInTimeline={isDataInTimeline}
         isDatepickerLocked={isDatepickerLocked}
         isFavorite={isFavorite}
@@ -92,6 +94,7 @@ const makeMapStateToProps = () => {
     const {
       dataProviders,
       description = '',
+      graphEventId,
       isFavorite = false,
       kqlQuery,
       title = '',
@@ -103,13 +106,14 @@ const makeMapStateToProps = () => {
 
     return {
       description,
-      notesById: getNotesByIds(state),
+      graphEventId,
       history,
       isDataInTimeline:
         !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)),
       isFavorite,
       isDatepickerLocked: globalInput.linkTo.includes('timeline'),
       noteIds,
+      notesById: getNotesByIds(state),
       status,
       title,
     };
diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx
new file mode 100644
index 0000000000000..fe38dd79176a5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx
@@ -0,0 +1,150 @@
+/*
+ * 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 {
+  EuiButtonEmpty,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiHorizontalRule,
+  EuiTitle,
+} from '@elastic/eui';
+import { noop } from 'lodash/fp';
+import React, { useCallback, useState } from 'react';
+import { connect, ConnectedProps, useDispatch, useSelector } from 'react-redux';
+import styled from 'styled-components';
+
+import { SecurityPageName } from '../../../app/types';
+import { AllCasesModal } from '../../../cases/components/all_cases_modal';
+import { getCaseDetailsUrl } from '../../../common/components/link_to';
+import { APP_ID } from '../../../../common/constants';
+import { useKibana } from '../../../common/lib/kibana';
+import { State } from '../../../common/store';
+import { timelineSelectors } from '../../store/timeline';
+import { timelineDefaults } from '../../store/timeline/defaults';
+import { TimelineModel } from '../../store/timeline/model';
+import { NewCase, ExistingCase } from '../timeline/properties/helpers';
+import { UNTITLED_TIMELINE } from '../timeline/properties/translations';
+import {
+  setInsertTimeline,
+  updateTimelineGraphEventId,
+} from '../../../timelines/store/timeline/actions';
+
+import * as i18n from './translations';
+
+const OverlayContainer = styled.div<{ bodyHeight?: number }>`
+  height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')};
+  width: 100%;
+`;
+
+interface OwnProps {
+  bodyHeight?: number;
+  graphEventId?: string;
+  timelineId: string;
+}
+
+const GraphOverlayComponent = ({
+  bodyHeight,
+  graphEventId,
+  status,
+  timelineId,
+  title,
+}: OwnProps & PropsFromRedux) => {
+  const dispatch = useDispatch();
+  const { navigateToApp } = useKibana().services.application;
+  const onCloseOverlay = useCallback(() => {
+    dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' }));
+  }, [dispatch, timelineId]);
+  const [showCaseModal, setShowCaseModal] = useState<boolean>(false);
+  const onOpenCaseModal = useCallback(() => setShowCaseModal(true), []);
+  const onCloseCaseModal = useCallback(() => setShowCaseModal(false), [setShowCaseModal]);
+  const currentTimeline = useSelector((state: State) =>
+    timelineSelectors.selectTimeline(state, timelineId)
+  );
+  const onRowClick = useCallback(
+    (id: string) => {
+      onCloseCaseModal();
+
+      dispatch(
+        setInsertTimeline({
+          graphEventId,
+          timelineId,
+          timelineSavedObjectId: currentTimeline.savedObjectId,
+          timelineTitle: title.length > 0 ? title : UNTITLED_TIMELINE,
+        })
+      );
+
+      navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
+        path: getCaseDetailsUrl({ id }),
+      });
+    },
+    [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title]
+  );
+
+  return (
+    <OverlayContainer bodyHeight={bodyHeight}>
+      <EuiHorizontalRule margin="none" />
+      <EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
+        <EuiFlexItem grow={false}>
+          <EuiButtonEmpty onClick={onCloseOverlay} size="xs">
+            {i18n.BACK_TO_EVENTS}
+          </EuiButtonEmpty>
+        </EuiFlexItem>
+        <EuiFlexItem grow={false}>
+          <EuiFlexGroup gutterSize="none">
+            <EuiFlexItem grow={false}>
+              <NewCase
+                compact={true}
+                graphEventId={graphEventId}
+                onClosePopover={noop}
+                timelineId={timelineId}
+                timelineTitle={title}
+                timelineStatus={status}
+              />
+            </EuiFlexItem>
+            <EuiFlexItem grow={false}>
+              <ExistingCase
+                compact={true}
+                onClosePopover={noop}
+                onOpenCaseModal={onOpenCaseModal}
+                timelineStatus={status}
+              />
+            </EuiFlexItem>
+          </EuiFlexGroup>
+        </EuiFlexItem>
+      </EuiFlexGroup>
+
+      <EuiHorizontalRule margin="none" />
+      <EuiTitle>
+        <>{`Resolver graph for event _id ${graphEventId}`}</>
+      </EuiTitle>
+      <AllCasesModal
+        onCloseCaseModal={onCloseCaseModal}
+        showCaseModal={showCaseModal}
+        onRowClick={onRowClick}
+      />
+    </OverlayContainer>
+  );
+};
+
+const makeMapStateToProps = () => {
+  const getTimeline = timelineSelectors.getTimelineByIdSelector();
+  const mapStateToProps = (state: State, { timelineId }: OwnProps) => {
+    const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults;
+    const { status, title = '' } = timeline;
+
+    return {
+      status,
+      title,
+    };
+  };
+  return mapStateToProps;
+};
+
+const connector = connect(makeMapStateToProps);
+
+type PropsFromRedux = ConnectedProps<typeof connector>;
+
+export const GraphOverlay = connector(GraphOverlayComponent);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts
new file mode 100644
index 0000000000000..c7cd9253de038
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/translations.ts
@@ -0,0 +1,14 @@
+/*
+ * 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 BACK_TO_EVENTS = i18n.translate(
+  'xpack.securitySolution.timeline.graphOverlay.backToEventsButton',
+  {
+    defaultMessage: '< Back to events',
+  }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
index c8a47798f169c..520215cde4862 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
@@ -190,6 +190,7 @@ export const formatTimelineResultToModel = (
 export interface QueryTimelineById<TCache> {
   apolloClient: ApolloClient<TCache> | ApolloClient<{}> | undefined;
   duplicate?: boolean;
+  graphEventId?: string;
   timelineId: string;
   onOpenTimeline?: (timeline: TimelineModel) => void;
   openTimeline?: boolean;
@@ -206,6 +207,7 @@ export interface QueryTimelineById<TCache> {
 export const queryTimelineById = <TCache>({
   apolloClient,
   duplicate = false,
+  graphEventId = '',
   timelineId,
   onOpenTimeline,
   openTimeline = true,
@@ -238,6 +240,7 @@ export const queryTimelineById = <TCache>({
             notes,
             timeline: {
               ...timeline,
+              graphEventId,
               show: openTimeline,
             },
             to: getOr(to, 'dateRange.end', timeline),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
index 4e6cce618880b..9278225271930 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
@@ -1,882 +1,942 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Timeline rendering renders correctly against snapshot 1`] = `
-<styled.div
-  data-test-subj="timeline"
->
-  <Styled(EuiFlyoutHeader)
-    data-test-subj="eui-flyout-header"
-    hasBorder={false}
-  >
-    <FlyoutHeaderWithCloseButton
-      onClose={[MockFunction]}
-      timelineId="foo"
-      usersViewing={
-        Array [
-          "elastic",
-        ]
-      }
-    />
-    <TimelineHeaderContainer
-      data-test-subj="timelineHeader"
-    >
-      <Memo(TimelineHeaderComponent)
-        browserFields={
-          Object {
-            "agent": Object {
-              "fields": Object {
-                "agent.ephemeral_id": Object {
-                  "aggregatable": true,
-                  "category": "agent",
-                  "description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.",
-                  "example": "8a4f500f",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "agent.ephemeral_id",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "agent.hostname": Object {
-                  "aggregatable": true,
-                  "category": "agent",
-                  "description": null,
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "agent.hostname",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "agent.id": Object {
-                  "aggregatable": true,
-                  "category": "agent",
-                  "description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.",
-                  "example": "8a4f500d",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "agent.id",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "agent.name": Object {
-                  "aggregatable": true,
-                  "category": "agent",
-                  "description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.",
-                  "example": "foo",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "agent.name",
-                  "searchable": true,
-                  "type": "string",
-                },
-              },
+<I18nProvider>
+  <Component>
+    <ApolloProvider
+      client={
+        ApolloClient {
+          "cache": InMemoryCache {
+            "addTypename": true,
+            "cacheKeyRoot": KeyTrie {
+              "weakness": true,
             },
-            "auditd": Object {
-              "fields": Object {
-                "auditd.data.a0": Object {
-                  "aggregatable": true,
-                  "category": "auditd",
-                  "description": null,
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                  ],
-                  "name": "auditd.data.a0",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "auditd.data.a1": Object {
-                  "aggregatable": true,
-                  "category": "auditd",
-                  "description": null,
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                  ],
-                  "name": "auditd.data.a1",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "auditd.data.a2": Object {
-                  "aggregatable": true,
-                  "category": "auditd",
-                  "description": null,
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                  ],
-                  "name": "auditd.data.a2",
-                  "searchable": true,
-                  "type": "string",
-                },
-              },
+            "config": Object {
+              "addTypename": true,
+              "dataIdFromObject": [Function],
+              "fragmentMatcher": HeuristicFragmentMatcher {},
+              "freezeResults": false,
+              "resultCaching": true,
             },
-            "base": Object {
-              "fields": Object {
-                "@timestamp": Object {
-                  "aggregatable": true,
-                  "category": "base",
-                  "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.",
-                  "example": "2016-05-23T08:05:34.853Z",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "@timestamp",
-                  "searchable": true,
-                  "type": "date",
-                },
-              },
+            "data": DepTrackingCache {
+              "data": Object {},
+              "depend": [Function],
             },
-            "client": Object {
-              "fields": Object {
-                "client.address": Object {
-                  "aggregatable": true,
-                  "category": "client",
-                  "description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket.  You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.",
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "client.address",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "client.bytes": Object {
-                  "aggregatable": true,
-                  "category": "client",
-                  "description": "Bytes sent from the client to the server.",
-                  "example": "184",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "client.bytes",
-                  "searchable": true,
-                  "type": "number",
-                },
-                "client.domain": Object {
-                  "aggregatable": true,
-                  "category": "client",
-                  "description": "Client domain.",
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "client.domain",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "client.geo.country_iso_code": Object {
-                  "aggregatable": true,
-                  "category": "client",
-                  "description": "Country ISO code.",
-                  "example": "CA",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "client.geo.country_iso_code",
-                  "searchable": true,
-                  "type": "string",
-                },
-              },
+            "maybeBroadcastWatch": [Function],
+            "optimisticData": DepTrackingCache {
+              "data": Object {},
+              "depend": [Function],
             },
-            "cloud": Object {
-              "fields": Object {
-                "cloud.account.id": Object {
-                  "aggregatable": true,
-                  "category": "cloud",
-                  "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.",
-                  "example": "666777888999",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "cloud.account.id",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "cloud.availability_zone": Object {
-                  "aggregatable": true,
-                  "category": "cloud",
-                  "description": "Availability zone in which this host is running.",
-                  "example": "us-east-1c",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "cloud.availability_zone",
-                  "searchable": true,
-                  "type": "string",
-                },
-              },
+            "silenceBroadcast": false,
+            "storeReader": StoreReader {
+              "executeSelectionSet": [Function],
+              "executeStoreQuery": [Function],
+              "executeSubSelectedArray": [Function],
+              "freezeResults": false,
             },
-            "container": Object {
-              "fields": Object {
-                "container.id": Object {
-                  "aggregatable": true,
-                  "category": "container",
-                  "description": "Unique container id.",
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "container.id",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "container.image.name": Object {
-                  "aggregatable": true,
-                  "category": "container",
-                  "description": "Name of the image the container was built on.",
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "container.image.name",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "container.image.tag": Object {
-                  "aggregatable": true,
-                  "category": "container",
-                  "description": "Container image tag.",
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "container.image.tag",
-                  "searchable": true,
-                  "type": "string",
-                },
-              },
-            },
-            "destination": Object {
-              "fields": Object {
-                "destination.address": Object {
-                  "aggregatable": true,
-                  "category": "destination",
-                  "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket.  You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.",
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "destination.address",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "destination.bytes": Object {
-                  "aggregatable": true,
-                  "category": "destination",
-                  "description": "Bytes sent from the destination to the source.",
-                  "example": "184",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "destination.bytes",
-                  "searchable": true,
-                  "type": "number",
-                },
-                "destination.domain": Object {
-                  "aggregatable": true,
-                  "category": "destination",
-                  "description": "Destination domain.",
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "destination.domain",
-                  "searchable": true,
-                  "type": "string",
-                },
-                "destination.ip": Object {
-                  "aggregatable": true,
-                  "category": "destination",
-                  "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.",
-                  "example": "",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "destination.ip",
-                  "searchable": true,
-                  "type": "ip",
-                },
-                "destination.port": Object {
-                  "aggregatable": true,
-                  "category": "destination",
-                  "description": "Port of the destination.",
-                  "example": "",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "destination.port",
-                  "searchable": true,
-                  "type": "long",
-                },
-              },
-            },
-            "event": Object {
-              "fields": Object {
-                "event.end": Object {
-                  "aggregatable": true,
-                  "category": "event",
-                  "description": "event.end contains the date when the event ended or when the activity was last observed.",
-                  "example": null,
-                  "format": "",
-                  "indexes": Array [
-                    "apm-*-transaction*",
-                    "auditbeat-*",
-                    "endgame-*",
-                    "filebeat-*",
-                    "packetbeat-*",
-                    "winlogbeat-*",
-                  ],
-                  "name": "event.end",
-                  "searchable": true,
-                  "type": "date",
-                },
-              },
-            },
-            "source": Object {
-              "fields": Object {
-                "source.ip": Object {
-                  "aggregatable": true,
-                  "category": "source",
-                  "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.",
-                  "example": "",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "source.ip",
-                  "searchable": true,
-                  "type": "ip",
-                },
-                "source.port": Object {
-                  "aggregatable": true,
-                  "category": "source",
-                  "description": "Port of the source.",
-                  "example": "",
-                  "format": "",
-                  "indexes": Array [
-                    "auditbeat",
-                    "filebeat",
-                    "packetbeat",
-                  ],
-                  "name": "source.port",
-                  "searchable": true,
-                  "type": "long",
-                },
-              },
+            "storeWriter": StoreWriter {},
+            "typenameDocumentCache": Map {},
+            "watches": Set {},
+          },
+          "defaultOptions": Object {},
+          "disableNetworkFetches": false,
+          "link": ApolloLink {
+            "request": [Function],
+          },
+          "mutate": [Function],
+          "query": [Function],
+          "queryDeduplication": true,
+          "reFetchObservableQueries": [Function],
+          "resetStore": [Function],
+          "resetStoreCallbacks": Array [],
+          "ssrMode": false,
+          "store": DataStore {
+            "cache": InMemoryCache {
+              "addTypename": true,
+              "cacheKeyRoot": KeyTrie {
+                "weakness": true,
+              },
+              "config": Object {
+                "addTypename": true,
+                "dataIdFromObject": [Function],
+                "fragmentMatcher": HeuristicFragmentMatcher {},
+                "freezeResults": false,
+                "resultCaching": true,
+              },
+              "data": DepTrackingCache {
+                "data": Object {},
+                "depend": [Function],
+              },
+              "maybeBroadcastWatch": [Function],
+              "optimisticData": DepTrackingCache {
+                "data": Object {},
+                "depend": [Function],
+              },
+              "silenceBroadcast": false,
+              "storeReader": StoreReader {
+                "executeSelectionSet": [Function],
+                "executeStoreQuery": [Function],
+                "executeSubSelectedArray": [Function],
+                "freezeResults": false,
+              },
+              "storeWriter": StoreWriter {},
+              "typenameDocumentCache": Map {},
+              "watches": Set {},
             },
+          },
+          "version": "2.3.8",
+          "watchQuery": [Function],
+        }
+      }
+    >
+      <Provider
+        store={
+          Object {
+            "dispatch": [Function],
+            "getState": [Function],
+            "replaceReducer": [Function],
+            "subscribe": [Function],
+            Symbol(observable): [Function],
           }
         }
-        dataProviders={
-          Array [
-            Object {
-              "and": Array [
+      >
+        <ThemeProvider
+          theme={[Function]}
+        >
+          <DragDropContext
+            onDragEnd={[MockFunction]}
+          >
+            <TimelineComponent
+              browserFields={
                 Object {
-                  "and": Array [],
-                  "enabled": true,
-                  "excluded": false,
-                  "id": "id-Provider 2",
-                  "kqlQuery": "",
-                  "name": "Provider 2",
-                  "queryMatch": Object {
-                    "field": "name",
-                    "operator": ":",
-                    "value": "Provider 2",
+                  "agent": Object {
+                    "fields": Object {
+                      "agent.ephemeral_id": Object {
+                        "aggregatable": true,
+                        "category": "agent",
+                        "description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.",
+                        "example": "8a4f500f",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "agent.ephemeral_id",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "agent.hostname": Object {
+                        "aggregatable": true,
+                        "category": "agent",
+                        "description": null,
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "agent.hostname",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "agent.id": Object {
+                        "aggregatable": true,
+                        "category": "agent",
+                        "description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.",
+                        "example": "8a4f500d",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "agent.id",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "agent.name": Object {
+                        "aggregatable": true,
+                        "category": "agent",
+                        "description": "Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.",
+                        "example": "foo",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "agent.name",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                    },
                   },
-                },
-                Object {
-                  "and": Array [],
-                  "enabled": true,
-                  "excluded": false,
-                  "id": "id-Provider 3",
-                  "kqlQuery": "",
-                  "name": "Provider 3",
-                  "queryMatch": Object {
-                    "field": "name",
-                    "operator": ":",
-                    "value": "Provider 3",
+                  "auditd": Object {
+                    "fields": Object {
+                      "auditd.data.a0": Object {
+                        "aggregatable": true,
+                        "category": "auditd",
+                        "description": null,
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                        ],
+                        "name": "auditd.data.a0",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "auditd.data.a1": Object {
+                        "aggregatable": true,
+                        "category": "auditd",
+                        "description": null,
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                        ],
+                        "name": "auditd.data.a1",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "auditd.data.a2": Object {
+                        "aggregatable": true,
+                        "category": "auditd",
+                        "description": null,
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                        ],
+                        "name": "auditd.data.a2",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                    },
                   },
-                },
-              ],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 1",
-              "kqlQuery": "",
-              "name": "Provider 1",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 1",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 2",
-              "kqlQuery": "",
-              "name": "Provider 2",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 2",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 3",
-              "kqlQuery": "",
-              "name": "Provider 3",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 3",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 4",
-              "kqlQuery": "",
-              "name": "Provider 4",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 4",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 5",
-              "kqlQuery": "",
-              "name": "Provider 5",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 5",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 6",
-              "kqlQuery": "",
-              "name": "Provider 6",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 6",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 7",
-              "kqlQuery": "",
-              "name": "Provider 7",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 7",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 8",
-              "kqlQuery": "",
-              "name": "Provider 8",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 8",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 9",
-              "kqlQuery": "",
-              "name": "Provider 9",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 9",
-              },
-            },
-            Object {
-              "and": Array [],
-              "enabled": true,
-              "excluded": false,
-              "id": "id-Provider 10",
-              "kqlQuery": "",
-              "name": "Provider 10",
-              "queryMatch": Object {
-                "field": "name",
-                "operator": ":",
-                "value": "Provider 10",
-              },
-            },
-          ]
-        }
-        filterManager={
-          FilterManager {
-            "fetch$": Subject {
-              "_isScalar": false,
-              "closed": false,
-              "hasError": false,
-              "isStopped": false,
-              "observers": Array [],
-              "thrownError": null,
-            },
-            "filters": Array [],
-            "uiSettings": Object {
-              "get": [MockFunction] {
-                "calls": Array [
-                  Array [
-                    "query:allowLeadingWildcards",
-                  ],
-                  Array [
-                    "query:queryString:options",
-                  ],
-                  Array [
-                    "courier:ignoreFilterIfFieldNotInIndex",
-                  ],
-                  Array [
-                    "dateFormat:tz",
-                  ],
-                ],
-                "results": Array [
+                  "base": Object {
+                    "fields": Object {
+                      "@timestamp": Object {
+                        "aggregatable": true,
+                        "category": "base",
+                        "description": "Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.",
+                        "example": "2016-05-23T08:05:34.853Z",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "@timestamp",
+                        "searchable": true,
+                        "type": "date",
+                      },
+                    },
+                  },
+                  "client": Object {
+                    "fields": Object {
+                      "client.address": Object {
+                        "aggregatable": true,
+                        "category": "client",
+                        "description": "Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket.  You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.",
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "client.address",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "client.bytes": Object {
+                        "aggregatable": true,
+                        "category": "client",
+                        "description": "Bytes sent from the client to the server.",
+                        "example": "184",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "client.bytes",
+                        "searchable": true,
+                        "type": "number",
+                      },
+                      "client.domain": Object {
+                        "aggregatable": true,
+                        "category": "client",
+                        "description": "Client domain.",
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "client.domain",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "client.geo.country_iso_code": Object {
+                        "aggregatable": true,
+                        "category": "client",
+                        "description": "Country ISO code.",
+                        "example": "CA",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "client.geo.country_iso_code",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                    },
+                  },
+                  "cloud": Object {
+                    "fields": Object {
+                      "cloud.account.id": Object {
+                        "aggregatable": true,
+                        "category": "cloud",
+                        "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.",
+                        "example": "666777888999",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "cloud.account.id",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "cloud.availability_zone": Object {
+                        "aggregatable": true,
+                        "category": "cloud",
+                        "description": "Availability zone in which this host is running.",
+                        "example": "us-east-1c",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "cloud.availability_zone",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                    },
+                  },
+                  "container": Object {
+                    "fields": Object {
+                      "container.id": Object {
+                        "aggregatable": true,
+                        "category": "container",
+                        "description": "Unique container id.",
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "container.id",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "container.image.name": Object {
+                        "aggregatable": true,
+                        "category": "container",
+                        "description": "Name of the image the container was built on.",
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "container.image.name",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "container.image.tag": Object {
+                        "aggregatable": true,
+                        "category": "container",
+                        "description": "Container image tag.",
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "container.image.tag",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                    },
+                  },
+                  "destination": Object {
+                    "fields": Object {
+                      "destination.address": Object {
+                        "aggregatable": true,
+                        "category": "destination",
+                        "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket.  You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.",
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "destination.address",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "destination.bytes": Object {
+                        "aggregatable": true,
+                        "category": "destination",
+                        "description": "Bytes sent from the destination to the source.",
+                        "example": "184",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "destination.bytes",
+                        "searchable": true,
+                        "type": "number",
+                      },
+                      "destination.domain": Object {
+                        "aggregatable": true,
+                        "category": "destination",
+                        "description": "Destination domain.",
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "destination.domain",
+                        "searchable": true,
+                        "type": "string",
+                      },
+                      "destination.ip": Object {
+                        "aggregatable": true,
+                        "category": "destination",
+                        "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.",
+                        "example": "",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "destination.ip",
+                        "searchable": true,
+                        "type": "ip",
+                      },
+                      "destination.port": Object {
+                        "aggregatable": true,
+                        "category": "destination",
+                        "description": "Port of the destination.",
+                        "example": "",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "destination.port",
+                        "searchable": true,
+                        "type": "long",
+                      },
+                    },
+                  },
+                  "event": Object {
+                    "fields": Object {
+                      "event.end": Object {
+                        "aggregatable": true,
+                        "category": "event",
+                        "description": "event.end contains the date when the event ended or when the activity was last observed.",
+                        "example": null,
+                        "format": "",
+                        "indexes": Array [
+                          "apm-*-transaction*",
+                          "auditbeat-*",
+                          "endgame-*",
+                          "filebeat-*",
+                          "packetbeat-*",
+                          "winlogbeat-*",
+                        ],
+                        "name": "event.end",
+                        "searchable": true,
+                        "type": "date",
+                      },
+                    },
+                  },
+                  "source": Object {
+                    "fields": Object {
+                      "source.ip": Object {
+                        "aggregatable": true,
+                        "category": "source",
+                        "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.",
+                        "example": "",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "source.ip",
+                        "searchable": true,
+                        "type": "ip",
+                      },
+                      "source.port": Object {
+                        "aggregatable": true,
+                        "category": "source",
+                        "description": "Port of the source.",
+                        "example": "",
+                        "format": "",
+                        "indexes": Array [
+                          "auditbeat",
+                          "filebeat",
+                          "packetbeat",
+                        ],
+                        "name": "source.port",
+                        "searchable": true,
+                        "type": "long",
+                      },
+                    },
+                  },
+                }
+              }
+              columns={
+                Array [
                   Object {
-                    "type": "return",
-                    "value": undefined,
+                    "aggregatable": true,
+                    "category": "base",
+                    "columnHeaderType": "not-filtered",
+                    "description": "Date/time when the event originated.
+For log events this is the date/time when the event was generated, and not when it was read.
+Required field for all events.",
+                    "example": "2016-05-23T08:05:34.853Z",
+                    "id": "@timestamp",
+                    "type": "date",
+                    "width": 190,
                   },
                   Object {
-                    "type": "return",
-                    "value": undefined,
+                    "aggregatable": true,
+                    "category": "event",
+                    "columnHeaderType": "not-filtered",
+                    "description": "Severity describes the severity of the event. What the different severity values mean can very different between use cases. It's up to the implementer to make sure severities are consistent across events.",
+                    "example": "7",
+                    "id": "event.severity",
+                    "type": "long",
+                    "width": 180,
                   },
                   Object {
-                    "type": "return",
-                    "value": undefined,
+                    "aggregatable": true,
+                    "category": "event",
+                    "columnHeaderType": "not-filtered",
+                    "description": "Event category.
+This contains high-level information about the contents of the event. It is more generic than \`event.action\`, in the sense that typically a category contains multiple actions. Warning: In future versions of ECS, we plan to provide a list of acceptable values for this field, please use with caution.",
+                    "example": "user-management",
+                    "id": "event.category",
+                    "type": "keyword",
+                    "width": 180,
                   },
                   Object {
-                    "type": "return",
-                    "value": undefined,
+                    "aggregatable": true,
+                    "category": "event",
+                    "columnHeaderType": "not-filtered",
+                    "description": "The action captured by the event.
+This describes the information in the event. It is more specific than \`event.category\`. Examples are \`group-add\`, \`process-started\`, \`file-created\`. The value is normally defined by the implementer.",
+                    "example": "user-password-change",
+                    "id": "event.action",
+                    "type": "keyword",
+                    "width": 180,
                   },
-                ],
-              },
-            },
-            "updated$": Subject {
-              "_isScalar": false,
-              "closed": false,
-              "hasError": false,
-              "isStopped": false,
-              "observers": Array [],
-              "thrownError": null,
-            },
-          }
-        }
-        id="foo"
-        indexPattern={
-          Object {
-            "fields": Array [
-              Object {
-                "aggregatable": true,
-                "name": "@timestamp",
-                "searchable": true,
-                "type": "date",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "@version",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.ephemeral_id",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.hostname",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.id",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.test1",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.test2",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.test3",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.test4",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.test5",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.test6",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.test7",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "agent.test8",
-                "searchable": true,
-                "type": "string",
-              },
-              Object {
-                "aggregatable": true,
-                "name": "host.name",
-                "searchable": true,
-                "type": "string",
-              },
-            ],
-            "title": "filebeat-*,auditbeat-*,packetbeat-*",
-          }
-        }
-        onDataProviderEdited={[MockFunction]}
-        onDataProviderRemoved={[MockFunction]}
-        onToggleDataProviderEnabled={[MockFunction]}
-        onToggleDataProviderExcluded={[MockFunction]}
-        show={true}
-        showCallOutUnauthorizedMsg={false}
-      />
-    </TimelineHeaderContainer>
-  </Styled(EuiFlyoutHeader)>
-  <Connect(Component)
-    id="foo"
-    indexPattern={
-      Object {
-        "fields": Array [
-          Object {
-            "aggregatable": true,
-            "name": "@timestamp",
-            "searchable": true,
-            "type": "date",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "@version",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.ephemeral_id",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.hostname",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.id",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.test1",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.test2",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.test3",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.test4",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.test5",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.test6",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.test7",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "agent.test8",
-            "searchable": true,
-            "type": "string",
-          },
-          Object {
-            "aggregatable": true,
-            "name": "host.name",
-            "searchable": true,
-            "type": "string",
-          },
-        ],
-        "title": "filebeat-*,auditbeat-*,packetbeat-*",
-      }
-    }
-    inputId="timeline"
-  />
-  <Connect(EnhancedType)
-    eventType="raw"
-    fields={
-      Array [
-        "@timestamp",
-        "event.severity",
-        "event.category",
-        "event.action",
-        "host.name",
-        "source.ip",
-        "destination.ip",
-        "destination.bytes",
-        "user.name",
-        "_id",
-        "message",
-      ]
-    }
-    filterQuery="{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 1\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}}]}}]}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 2\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 3\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 4\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 5\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 6\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 7\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 8\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 9\\"}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"match_phrase\\":{\\"name\\":\\"Provider 10\\"}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"gte\\":1521830963132}}}],\\"minimum_should_match\\":1}},{\\"bool\\":{\\"should\\":[{\\"range\\":{\\"@timestamp\\":{\\"lte\\":1521862432253}}}],\\"minimum_should_match\\":1}}]}}]}}],\\"should\\":[],\\"must_not\\":[]}}"
-    id="foo"
-    indexToAdd={Array []}
-    limit={5}
-    sortField={
-      Object {
-        "direction": "desc",
-        "sortFieldId": "@timestamp",
-      }
-    }
-    sourceId="default"
-  >
-    <Component />
-  </Connect(EnhancedType)>
-</styled.div>
+                  Object {
+                    "aggregatable": true,
+                    "category": "host",
+                    "columnHeaderType": "not-filtered",
+                    "description": "Name of the host.
+It can contain what \`hostname\` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.",
+                    "example": "",
+                    "id": "host.name",
+                    "type": "keyword",
+                    "width": 180,
+                  },
+                  Object {
+                    "aggregatable": true,
+                    "category": "source",
+                    "columnHeaderType": "not-filtered",
+                    "description": "IP address of the source.
+Can be one or multiple IPv4 or IPv6 addresses.",
+                    "example": "",
+                    "id": "source.ip",
+                    "type": "ip",
+                    "width": 180,
+                  },
+                  Object {
+                    "aggregatable": true,
+                    "category": "destination",
+                    "columnHeaderType": "not-filtered",
+                    "description": "IP address of the destination.
+Can be one or multiple IPv4 or IPv6 addresses.",
+                    "example": "",
+                    "id": "destination.ip",
+                    "type": "ip",
+                    "width": 180,
+                  },
+                  Object {
+                    "aggregatable": true,
+                    "category": "destination",
+                    "columnHeaderType": "not-filtered",
+                    "description": "Bytes sent from the source to the destination",
+                    "example": "123",
+                    "format": "bytes",
+                    "id": "destination.bytes",
+                    "type": "number",
+                    "width": 180,
+                  },
+                  Object {
+                    "aggregatable": true,
+                    "category": "user",
+                    "columnHeaderType": "not-filtered",
+                    "description": "Short name or login of the user.",
+                    "example": "albert",
+                    "id": "user.name",
+                    "type": "keyword",
+                    "width": 180,
+                  },
+                  Object {
+                    "aggregatable": true,
+                    "category": "base",
+                    "columnHeaderType": "not-filtered",
+                    "description": "Each document has an _id that uniquely identifies it",
+                    "example": "Y-6TfmcB0WOhS6qyMv3s",
+                    "id": "_id",
+                    "type": "keyword",
+                    "width": 180,
+                  },
+                  Object {
+                    "aggregatable": false,
+                    "category": "base",
+                    "columnHeaderType": "not-filtered",
+                    "description": "For log events the message field contains the log message.
+In other use cases the message field can be used to concatenate different values which are then freely searchable. If multiple messages exist, they can be combined into one message.",
+                    "example": "Hello World",
+                    "id": "message",
+                    "type": "text",
+                    "width": 180,
+                  },
+                ]
+              }
+              dataProviders={
+                Array [
+                  Object {
+                    "and": Array [
+                      Object {
+                        "and": Array [],
+                        "enabled": true,
+                        "excluded": false,
+                        "id": "id-Provider 2",
+                        "kqlQuery": "",
+                        "name": "Provider 2",
+                        "queryMatch": Object {
+                          "field": "name",
+                          "operator": ":",
+                          "value": "Provider 2",
+                        },
+                      },
+                      Object {
+                        "and": Array [],
+                        "enabled": true,
+                        "excluded": false,
+                        "id": "id-Provider 3",
+                        "kqlQuery": "",
+                        "name": "Provider 3",
+                        "queryMatch": Object {
+                          "field": "name",
+                          "operator": ":",
+                          "value": "Provider 3",
+                        },
+                      },
+                    ],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 1",
+                    "kqlQuery": "",
+                    "name": "Provider 1",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 1",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 2",
+                    "kqlQuery": "",
+                    "name": "Provider 2",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 2",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 3",
+                    "kqlQuery": "",
+                    "name": "Provider 3",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 3",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 4",
+                    "kqlQuery": "",
+                    "name": "Provider 4",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 4",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 5",
+                    "kqlQuery": "",
+                    "name": "Provider 5",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 5",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 6",
+                    "kqlQuery": "",
+                    "name": "Provider 6",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 6",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 7",
+                    "kqlQuery": "",
+                    "name": "Provider 7",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 7",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 8",
+                    "kqlQuery": "",
+                    "name": "Provider 8",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 8",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 9",
+                    "kqlQuery": "",
+                    "name": "Provider 9",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 9",
+                    },
+                  },
+                  Object {
+                    "and": Array [],
+                    "enabled": true,
+                    "excluded": false,
+                    "id": "id-Provider 10",
+                    "kqlQuery": "",
+                    "name": "Provider 10",
+                    "queryMatch": Object {
+                      "field": "name",
+                      "operator": ":",
+                      "value": "Provider 10",
+                    },
+                  },
+                ]
+              }
+              end={1521862432253}
+              eventType="raw"
+              filters={Array []}
+              id="foo"
+              indexPattern={
+                Object {
+                  "fields": Array [
+                    Object {
+                      "aggregatable": true,
+                      "name": "@timestamp",
+                      "searchable": true,
+                      "type": "date",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "@version",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.ephemeral_id",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.hostname",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.id",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.test1",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.test2",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.test3",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.test4",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.test5",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.test6",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.test7",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "agent.test8",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                    Object {
+                      "aggregatable": true,
+                      "name": "host.name",
+                      "searchable": true,
+                      "type": "string",
+                    },
+                  ],
+                  "title": "filebeat-*,auditbeat-*,packetbeat-*",
+                }
+              }
+              indexToAdd={Array []}
+              isLive={false}
+              itemsPerPage={5}
+              itemsPerPageOptions={
+                Array [
+                  5,
+                  10,
+                  20,
+                ]
+              }
+              kqlMode="search"
+              kqlQueryExpression=""
+              loadingIndexName={false}
+              onChangeItemsPerPage={[MockFunction]}
+              onClose={[MockFunction]}
+              onDataProviderEdited={[MockFunction]}
+              onDataProviderRemoved={[MockFunction]}
+              onToggleDataProviderEnabled={[MockFunction]}
+              onToggleDataProviderExcluded={[MockFunction]}
+              show={true}
+              showCallOutUnauthorizedMsg={false}
+              sort={
+                Object {
+                  "columnId": "@timestamp",
+                  "sortDirection": "desc",
+                }
+              }
+              start={1521830963132}
+              toggleColumn={[MockFunction]}
+              usersViewing={
+                Array [
+                  "elastic",
+                ]
+              }
+            />
+          </DragDropContext>
+        </ThemeProvider>
+      </Provider>
+    </ApolloProvider>
+  </Component>
+</I18nProvider>
 `;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
index ef744ab562e71..b478070b31578 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
@@ -15,6 +15,7 @@ import { eventHasNotes, getPinTooltip } from '../helpers';
 import * as i18n from '../translations';
 import { OnRowSelected } from '../../events';
 import { Ecs } from '../../../../../graphql/types';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
 
 export interface TimelineRowActionOnClick {
   eventId: string;
@@ -27,7 +28,7 @@ export interface TimelineRowAction {
   displayType: 'icon' | 'contextMenu';
   iconType?: string;
   id: string;
-  isActionDisabled?: boolean;
+  isActionDisabled?: (ecsData?: Ecs) => boolean;
   onClick: ({ eventId, ecsData }: TimelineRowActionOnClick) => void;
   content: string | JSX.Element;
   width?: number;
@@ -83,24 +84,9 @@ export const Actions = React.memo<Props>(
       actionsColumnWidth={actionsColumnWidth}
       data-test-subj="event-actions-container"
     >
-      <EventsTd>
-        <EventsTdContent textAlign="center">
-          {loading && <EventsLoading />}
-
-          {!loading && (
-            <EuiButtonIcon
-              aria-label={expanded ? i18n.COLLAPSE : i18n.EXPAND}
-              data-test-subj="expand-event"
-              iconType={expanded ? 'arrowDown' : 'arrowRight'}
-              id={eventId}
-              onClick={onEventToggled}
-            />
-          )}
-        </EventsTdContent>
-      </EventsTd>
       {showCheckboxes && (
         <EventsTd data-test-subj="select-event-container">
-          <EventsTdContent textAlign="center">
+          <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
             {loadingEventIds.includes(eventId) ? (
               <EuiLoadingSpinner size="m" data-test-subj="event-loader" />
             ) : (
@@ -120,12 +106,28 @@ export const Actions = React.memo<Props>(
         </EventsTd>
       )}
 
+      <EventsTd>
+        <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
+          {loading && <EventsLoading />}
+
+          {!loading && (
+            <EuiButtonIcon
+              aria-label={expanded ? i18n.COLLAPSE : i18n.EXPAND}
+              data-test-subj="expand-event"
+              iconType={expanded ? 'arrowDown' : 'arrowRight'}
+              id={eventId}
+              onClick={onEventToggled}
+            />
+          )}
+        </EventsTdContent>
+      </EventsTd>
+
       <>{additionalActions}</>
 
       {!isEventViewer && (
         <>
           <EventsTd>
-            <EventsTdContent textAlign="center">
+            <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
               <EuiToolTip
                 data-test-subj="timeline-action-pin-tool-tip"
                 content={getPinTooltip({
@@ -144,7 +146,7 @@ export const Actions = React.memo<Props>(
           </EventsTd>
 
           <EventsTd>
-            <EventsTdContent textAlign="center">
+            <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
               <NotesButton
                 animate={false}
                 associateNote={associateNote}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap
index 03e4f4b5f0f2b..9508e3f18a348 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap
@@ -6,13 +6,13 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
 >
   <styled.div>
     <styled.div
-      actionsColumnWidth={115}
+      actionsColumnWidth={76}
       data-test-subj="actions-container"
-      justifyContent="space-between"
     >
       <styled.div>
         <styled.div
           textAlign="center"
+          width={24}
         >
           <Connect(Component)
             browserFields={
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
index 2bb78c0dcb0ad..aa0b8d770f60c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx
@@ -28,6 +28,7 @@ import {
   OnSelectAll,
   OnUpdateColumns,
 } from '../../events';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
 import {
   EventsTh,
   EventsThContent,
@@ -168,11 +169,26 @@ export const ColumnHeadersComponent = ({
       <EventsTrHeader>
         <EventsThGroupActions
           actionsColumnWidth={actionsColumnWidth}
-          justifyContent={showSelectAllCheckbox ? 'flexStart' : 'space-between'}
           data-test-subj="actions-container"
         >
+          {showSelectAllCheckbox && (
+            <EventsTh>
+              <EventsThContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
+                <EuiCheckbox
+                  data-test-subj="select-all-events"
+                  id={'select-all-events'}
+                  checked={isSelectAllChecked}
+                  onChange={handleSelectAllChange}
+                />
+              </EventsThContent>
+            </EventsTh>
+          )}
+
           <EventsTh>
-            <EventsThContent textAlign={showSelectAllCheckbox ? 'left' : 'center'}>
+            <EventsThContent
+              textAlign={showSelectAllCheckbox ? 'left' : 'center'}
+              width={DEFAULT_ICON_BUTTON_WIDTH}
+            >
               <StatefulFieldsBrowser
                 browserFields={browserFields}
                 columnHeaders={columnHeaders}
@@ -186,25 +202,14 @@ export const ColumnHeadersComponent = ({
               />
             </EventsThContent>
           </EventsTh>
+
           {showEventsSelect && (
             <EventsTh>
-              <EventsThContent textAlign="center">
+              <EventsThContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
                 <EventsSelect checkState="unchecked" timelineId={timelineId} />
               </EventsThContent>
             </EventsTh>
           )}
-          {showSelectAllCheckbox && (
-            <EventsTh>
-              <EventsThContent textAlign="center">
-                <EuiCheckbox
-                  data-test-subj="select-all-events"
-                  id={'select-all-events'}
-                  checked={isSelectAllChecked}
-                  onChange={handleSelectAllChange}
-                />
-              </EventsThContent>
-            </EventsTh>
-          )}
         </EventsThGroupActions>
 
         <Droppable
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts
index 2aeb033c50d6f..5f3fb4fa5113c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts
@@ -5,14 +5,14 @@
  */
 
 /** The (fixed) width of the Actions column */
-export const DEFAULT_ACTIONS_COLUMN_WIDTH = 115; // px;
+export const DEFAULT_ACTIONS_COLUMN_WIDTH = 76; // px;
 /**
  * The (fixed) width of the Actions column when the timeline body is used as
  * an events viewer, which has fewer actions than a regular events viewer
  */
-export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 32; // px;
+export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 26; // px;
 /** Additional column width to include when checkboxes are shown **/
-export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 32; // px;
+export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px;
 /** The default minimum width of a column (when a width for the column type is not specified) */
 export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px
 /** The default minimum width of a column of type `date` */
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
index 767164967a3f4..03a964bbd444a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx
@@ -16,6 +16,7 @@ import {
 } from '@elastic/eui';
 import styled from 'styled-components';
 import { TimelineNonEcsData, Ecs } from '../../../../../graphql/types';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
 import { Note } from '../../../../../common/lib/note';
 import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
 import { AssociateNote, UpdateNote } from '../../../notes/helpers';
@@ -131,7 +132,7 @@ export const EventColumnView = React.memo<Props>(
               ...acc,
               icon: [
                 ...acc.icon,
-                <EventsTdContent key={action.id} textAlign="center">
+                <EventsTdContent key={action.id} textAlign="center" width={action.width}>
                   <EuiToolTip
                     data-test-subj={`${action.dataTestSubj}-tool-tip`}
                     content={action.content}
@@ -140,7 +141,9 @@ export const EventColumnView = React.memo<Props>(
                       aria-label={action.ariaLabel}
                       data-test-subj={`${action.dataTestSubj}-button`}
                       iconType={action.iconType}
-                      isDisabled={action.isActionDisabled ?? false}
+                      isDisabled={
+                        action.isActionDisabled != null ? action.isActionDisabled(ecsData) : false
+                      }
                       onClick={() => action.onClick({ eventId: id, ecsData })}
                     />
                   </EuiToolTip>
@@ -155,7 +158,9 @@ export const EventColumnView = React.memo<Props>(
               <EuiContextMenuItem
                 aria-label={action.ariaLabel}
                 data-test-subj={action.dataTestSubj}
-                disabled={action.isActionDisabled ?? false}
+                disabled={
+                  action.isActionDisabled != null ? action.isActionDisabled(ecsData) : false
+                }
                 icon={action.iconType}
                 key={action.id}
                 onClick={() => onClickCb(() => action.onClick({ eventId: id, ecsData }))}
@@ -170,7 +175,11 @@ export const EventColumnView = React.memo<Props>(
       return grouped.contextMenu.length > 0
         ? [
             ...grouped.icon,
-            <EventsTdContent key="actions-context-menu" textAlign="center">
+            <EventsTdContent
+              key="actions-context-menu"
+              textAlign="center"
+              width={DEFAULT_ICON_BUTTON_WIDTH}
+            >
               <EuiPopover
                 id="singlePanel"
                 button={button}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
index 4c0dc28b56d3f..bdc8c66ec3aa6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
@@ -3,12 +3,17 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-import { isEmpty, noop } from 'lodash/fp';
+import { get, isEmpty, noop } from 'lodash/fp';
+import { Dispatch } from 'redux';
 
 import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
+import { updateTimelineGraphEventId } from '../../../store/timeline/actions';
 import { EventType } from '../../../../timelines/store/timeline/model';
 import { OnPinEvent, OnUnPinEvent } from '../events';
 
+import { TimelineRowAction, TimelineRowActionOnClick } from './actions';
+
 import * as i18n from './translations';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -87,3 +92,32 @@ export const getEventType = (event: Ecs): Omit<EventType, 'all'> => {
   }
   return 'raw';
 };
+
+export const showGraphView = (graphEventId?: string) =>
+  graphEventId != null && graphEventId.length > 0;
+
+export const isInvestigateInResolverActionEnabled = (ecsData?: Ecs) => {
+  return (
+    get(['agent', 'type', 0], ecsData) === 'endpoint' &&
+    get(['process', 'entity_id'], ecsData)?.length > 0
+  );
+};
+
+export const getInvestigateInResolverAction = ({
+  dispatch,
+  timelineId,
+}: {
+  dispatch: Dispatch;
+  timelineId: string;
+}): TimelineRowAction => ({
+  ariaLabel: i18n.ACTION_INVESTIGATE_IN_RESOLVER,
+  content: i18n.ACTION_INVESTIGATE_IN_RESOLVER,
+  dataTestSubj: 'investigate-in-resolver',
+  displayType: 'icon',
+  iconType: 'node',
+  id: 'investigateInResolver',
+  isActionDisabled: (ecsData?: Ecs) => !isInvestigateInResolverActionEnabled(ecsData),
+  onClick: ({ eventId }: TimelineRowActionOnClick) =>
+    dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })),
+  width: DEFAULT_ICON_BUTTON_WIDTH,
+});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
index 775c26e82d27b..9b96e0c49c73d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
@@ -70,6 +70,7 @@ describe('Body', () => {
             pinnedEventIds={{}}
             rowRenderers={rowRenderers}
             selectedEventIds={{}}
+            show={true}
             sort={mockSort}
             showCheckboxes={false}
             toggleColumn={jest.fn()}
@@ -108,6 +109,7 @@ describe('Body', () => {
             pinnedEventIds={{}}
             rowRenderers={rowRenderers}
             selectedEventIds={{}}
+            show={true}
             sort={mockSort}
             showCheckboxes={false}
             toggleColumn={jest.fn()}
@@ -146,6 +148,7 @@ describe('Body', () => {
             pinnedEventIds={{}}
             rowRenderers={rowRenderers}
             selectedEventIds={{}}
+            show={true}
             sort={mockSort}
             showCheckboxes={false}
             toggleColumn={jest.fn()}
@@ -186,6 +189,7 @@ describe('Body', () => {
             pinnedEventIds={{}}
             rowRenderers={rowRenderers}
             selectedEventIds={{}}
+            show={true}
             sort={mockSort}
             showCheckboxes={false}
             toggleColumn={jest.fn()}
@@ -271,6 +275,7 @@ describe('Body', () => {
             pinnedEventIds={{}}
             rowRenderers={rowRenderers}
             selectedEventIds={{}}
+            show={true}
             sort={mockSort}
             showCheckboxes={false}
             toggleColumn={jest.fn()}
@@ -316,6 +321,7 @@ describe('Body', () => {
           pinnedEventIds={{}}
           rowRenderers={rowRenderers}
           selectedEventIds={{}}
+          show={true}
           sort={mockSort}
           showCheckboxes={false}
           toggleColumn={jest.fn()}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
index da8835d5903e1..46895c86de084 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx
@@ -26,10 +26,13 @@ import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles';
 import { ColumnHeaders } from './column_headers';
 import { getActionsColumnWidth } from './column_headers/helpers';
 import { Events } from './events';
+import { showGraphView } from './helpers';
 import { ColumnRenderer } from './renderers/column_renderer';
 import { RowRenderer } from './renderers/row_renderer';
 import { Sort } from './sort';
 import { useManageTimeline } from '../../manage_timeline';
+import { GraphOverlay } from '../../graph_overlay';
+import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
 
 export interface BodyProps {
   addNoteToEvent: AddNoteToEvent;
@@ -38,6 +41,7 @@ export interface BodyProps {
   columnRenderers: ColumnRenderer[];
   data: TimelineItem[];
   getNotesByIds: (noteIds: string[]) => Note[];
+  graphEventId?: string;
   height?: number;
   id: string;
   isEventViewer?: boolean;
@@ -56,6 +60,7 @@ export interface BodyProps {
   pinnedEventIds: Readonly<Record<string, boolean>>;
   rowRenderers: RowRenderer[];
   selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
+  show: boolean;
   showCheckboxes: boolean;
   sort: Sort;
   toggleColumn: (column: ColumnHeaderOptions) => void;
@@ -72,6 +77,7 @@ export const Body = React.memo<BodyProps>(
     data,
     eventIdToNoteIds,
     getNotesByIds,
+    graphEventId,
     height,
     id,
     isEventViewer = false,
@@ -89,6 +95,7 @@ export const Body = React.memo<BodyProps>(
     pinnedEventIds,
     rowRenderers,
     selectedEventIds,
+    show,
     showCheckboxes,
     sort,
     toggleColumn,
@@ -108,7 +115,7 @@ export const Body = React.memo<BodyProps>(
           if (v.displayType === 'icon') {
             return acc + (v.width ?? 0);
           }
-          const addWidth = hasContextMenu ? 0 : 26;
+          const addWidth = hasContextMenu ? 0 : DEFAULT_ICON_BUTTON_WIDTH;
           hasContextMenu = true;
           return acc + addWidth;
         }, 0) ?? 0
@@ -127,7 +134,15 @@ export const Body = React.memo<BodyProps>(
 
     return (
       <>
-        <TimelineBody data-test-subj="timeline-body" bodyHeight={height} ref={containerElementRef}>
+        {showGraphView(graphEventId) && (
+          <GraphOverlay bodyHeight={height} graphEventId={graphEventId} timelineId={id} />
+        )}
+        <TimelineBody
+          data-test-subj="timeline-body"
+          bodyHeight={height}
+          ref={containerElementRef}
+          visible={show && !showGraphView(graphEventId)}
+        >
           <EventsTable data-test-subj="events-table" columnWidths={columnWidths}>
             <ColumnHeaders
               actionsColumnWidth={actionsColumnWidth}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
index 2d5e64fb09ffc..0587da9f61508 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/stateful_body.tsx
@@ -10,11 +10,13 @@ import React, { useCallback, useEffect, useMemo } from 'react';
 import { connect, ConnectedProps } from 'react-redux';
 import deepEqual from 'fast-deep-equal';
 
+import { ACTIVE_TIMELINE_REDUX_ID } from '../../../../common/components/top_n';
 import { BrowserFields } from '../../../../common/containers/source';
 import { TimelineItem } from '../../../../graphql/types';
 import { Note } from '../../../../common/lib/note';
 import { appSelectors, State } from '../../../../common/store';
 import { appActions } from '../../../../common/store/actions';
+import { useManageTimeline } from '../../manage_timeline';
 import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model';
 import { timelineDefaults } from '../../../store/timeline/defaults';
 import { timelineActions, timelineSelectors } from '../../../store/timeline';
@@ -35,7 +37,6 @@ import { Body } from './index';
 import { columnRenderers, rowRenderers } from './renderers';
 import { Sort } from './sort';
 import { plainRowRenderer } from './renderers/plain_row_renderer';
-import { useManageTimeline } from '../../manage_timeline';
 
 interface OwnProps {
   browserFields: BrowserFields;
@@ -71,8 +72,10 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
     selectedEventIds,
     setSelected,
     clearSelected,
+    show,
     showCheckboxes,
     showRowRenderers,
+    graphEventId,
     sort,
     toggleColumn,
     unPinEvent,
@@ -180,6 +183,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
         data={data}
         eventIdToNoteIds={eventIdToNoteIds}
         getNotesByIds={getNotesByIds}
+        graphEventId={graphEventId}
         height={height}
         id={id}
         isEventViewer={isEventViewer}
@@ -197,6 +201,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
         pinnedEventIds={pinnedEventIds}
         rowRenderers={showRowRenderers ? rowRenderers : [plainRowRenderer]}
         selectedEventIds={selectedEventIds}
+        show={id === ACTIVE_TIMELINE_REDUX_ID ? show : true}
         showCheckboxes={showCheckboxes}
         sort={sort}
         toggleColumn={toggleColumn}
@@ -209,6 +214,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
     deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) &&
     deepEqual(prevProps.data, nextProps.data) &&
     prevProps.eventIdToNoteIds === nextProps.eventIdToNoteIds &&
+    prevProps.graphEventId === nextProps.graphEventId &&
     deepEqual(prevProps.notesById, nextProps.notesById) &&
     prevProps.height === nextProps.height &&
     prevProps.id === nextProps.id &&
@@ -216,6 +222,7 @@ const StatefulBodyComponent = React.memo<StatefulBodyComponentProps>(
     prevProps.isSelectAllChecked === nextProps.isSelectAllChecked &&
     prevProps.loadingEventIds === nextProps.loadingEventIds &&
     prevProps.pinnedEventIds === nextProps.pinnedEventIds &&
+    prevProps.show === nextProps.show &&
     prevProps.selectedEventIds === nextProps.selectedEventIds &&
     prevProps.showCheckboxes === nextProps.showCheckboxes &&
     prevProps.showRowRenderers === nextProps.showRowRenderers &&
@@ -238,10 +245,12 @@ const makeMapStateToProps = () => {
       columns,
       eventIdToNoteIds,
       eventType,
+      graphEventId,
       isSelectAllChecked,
       loadingEventIds,
       pinnedEventIds,
       selectedEventIds,
+      show,
       showCheckboxes,
       showRowRenderers,
     } = timeline;
@@ -250,12 +259,14 @@ const makeMapStateToProps = () => {
       columnHeaders: memoizedColumnHeaders(columns, browserFields),
       eventIdToNoteIds,
       eventType,
+      graphEventId,
       isSelectAllChecked,
       loadingEventIds,
       notesById: getNotesByIds(state),
       id,
       pinnedEventIds,
       selectedEventIds,
+      show,
       showCheckboxes,
       showRowRenderers,
     };
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
index 98f544f30ae8b..63b92d6b316cc 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
@@ -51,3 +51,10 @@ export const COLLAPSE = i18n.translate(
     defaultMessage: 'Collapse',
   }
 );
+
+export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate(
+  'xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip',
+  {
+    defaultMessage: 'Investigate in Resolver',
+  }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
index fb47eb331fdbb..e8f1e73719234 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
@@ -9,6 +9,7 @@ import React from 'react';
 import { FilterManager, IIndexPattern } from 'src/plugins/data/public';
 import deepEqual from 'fast-deep-equal';
 
+import { showGraphView } from '../body/helpers';
 import { DataProviders } from '../data_providers';
 import { DataProvider } from '../data_providers/data_provider';
 import {
@@ -26,6 +27,7 @@ interface Props {
   browserFields: BrowserFields;
   dataProviders: DataProvider[];
   filterManager: FilterManager;
+  graphEventId?: string;
   id: string;
   indexPattern: IIndexPattern;
   onDataProviderEdited: OnDataProviderEdited;
@@ -42,6 +44,7 @@ const TimelineHeaderComponent: React.FC<Props> = ({
   indexPattern,
   dataProviders,
   filterManager,
+  graphEventId,
   onDataProviderEdited,
   onDataProviderRemoved,
   onToggleDataProviderEnabled,
@@ -59,24 +62,27 @@ const TimelineHeaderComponent: React.FC<Props> = ({
         size="s"
       />
     )}
-    {show && (
-      <DataProviders
-        browserFields={browserFields}
-        id={id}
-        dataProviders={dataProviders}
-        onDataProviderEdited={onDataProviderEdited}
-        onDataProviderRemoved={onDataProviderRemoved}
-        onToggleDataProviderEnabled={onToggleDataProviderEnabled}
-        onToggleDataProviderExcluded={onToggleDataProviderExcluded}
-      />
-    )}
 
-    <StatefulSearchOrFilter
-      browserFields={browserFields}
-      filterManager={filterManager}
-      indexPattern={indexPattern}
-      timelineId={id}
-    />
+    {show && !showGraphView(graphEventId) && (
+      <>
+        <DataProviders
+          browserFields={browserFields}
+          id={id}
+          dataProviders={dataProviders}
+          onDataProviderEdited={onDataProviderEdited}
+          onDataProviderRemoved={onDataProviderRemoved}
+          onToggleDataProviderEnabled={onToggleDataProviderEnabled}
+          onToggleDataProviderExcluded={onToggleDataProviderExcluded}
+        />
+
+        <StatefulSearchOrFilter
+          browserFields={browserFields}
+          filterManager={filterManager}
+          indexPattern={indexPattern}
+          timelineId={id}
+        />
+      </>
+    )}
   </>
 );
 
@@ -88,6 +94,7 @@ export const TimelineHeader = React.memo(
     deepEqual(prevProps.indexPattern, nextProps.indexPattern) &&
     deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
     prevProps.filterManager === nextProps.filterManager &&
+    prevProps.graphEventId === nextProps.graphEventId &&
     prevProps.onDataProviderEdited === nextProps.onDataProviderEdited &&
     prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved &&
     prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled &&
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
index b5481e9d4eee2..a3fc692c3a8a8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx
@@ -153,3 +153,5 @@ export const combineQueries = ({
  * the `Timeline` and the `Events Viewer` widget
  */
 export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view';
+
+export const DEFAULT_ICON_BUTTON_WIDTH = 24;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
index 5ccc8911d1974..83ac1a421958b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
@@ -72,6 +72,7 @@ describe('StatefulTimeline', () => {
       eventType: 'raw',
       end: endDate,
       filters: [],
+      graphEventId: undefined,
       id: 'foo',
       isLive: false,
       isTimelineExists: false,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
index df76eb350ace7..a66c01d0b5d0b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
@@ -41,6 +41,7 @@ const StatefulTimelineComponent = React.memo<Props>(
     eventType,
     end,
     filters,
+    graphEventId,
     id,
     isLive,
     isTimelineExists,
@@ -168,6 +169,7 @@ const StatefulTimelineComponent = React.memo<Props>(
         end={end}
         eventType={eventType}
         filters={filters}
+        graphEventId={graphEventId}
         id={id}
         indexPattern={indexPattern}
         indexToAdd={indexToAdd}
@@ -196,6 +198,7 @@ const StatefulTimelineComponent = React.memo<Props>(
     return (
       prevProps.eventType === nextProps.eventType &&
       prevProps.end === nextProps.end &&
+      prevProps.graphEventId === nextProps.graphEventId &&
       prevProps.id === nextProps.id &&
       prevProps.isLive === nextProps.isLive &&
       prevProps.itemsPerPage === nextProps.itemsPerPage &&
@@ -229,6 +232,7 @@ const makeMapStateToProps = () => {
       dataProviders,
       eventType,
       filters,
+      graphEventId,
       itemsPerPage,
       itemsPerPageOptions,
       kqlMode,
@@ -245,6 +249,7 @@ const makeMapStateToProps = () => {
       eventType,
       end: input.timerange.to,
       filters: timelineFilter,
+      graphEventId,
       id,
       isLive: input.policy.kind === 'interval',
       isTimelineExists: getTimeline(state, id) != null,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx
index 2ffbae1f7eb5c..5e6f35e8397e4 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx
@@ -50,7 +50,11 @@ describe('Insert timeline popover ', () => {
       payload: { id: 'timeline-id', show: false },
       type: 'x-pack/security_solution/local/timeline/SHOW_TIMELINE',
     });
-    expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759');
+    expect(onTimelineChange).toBeCalledWith(
+      'Timeline title',
+      '34578-3497-5893-47589-34759',
+      undefined
+    );
     expect(mockDispatch.mock.calls[1][0]).toEqual({
       payload: null,
       type: 'x-pack/security_solution/local/timeline/SET_INSERT_TIMELINE',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
index de199d9a1cc2e..83417cdb51b69 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
@@ -19,7 +19,11 @@ import { setInsertTimeline } from '../../../store/timeline/actions';
 interface InsertTimelinePopoverProps {
   isDisabled: boolean;
   hideUntitled?: boolean;
-  onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
+  onTimelineChange: (
+    timelineTitle: string,
+    timelineId: string | null,
+    graphEventId?: string
+  ) => void;
 }
 
 type Props = InsertTimelinePopoverProps;
@@ -38,7 +42,11 @@ export const InsertTimelinePopoverComponent: React.FC<Props> = ({
   useEffect(() => {
     if (insertTimeline != null) {
       dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false }));
-      onTimelineChange(insertTimeline.timelineTitle, insertTimeline.timelineSavedObjectId);
+      onTimelineChange(
+        insertTimeline.timelineTitle,
+        insertTimeline.timelineSavedObjectId,
+        insertTimeline.graphEventId
+      );
       dispatch(setInsertTimeline(null));
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
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 c3def9c4cbb29..c3bcd1c0ebe51 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,6 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { isEmpty } from 'lodash/fp';
 import { useCallback, useState } from 'react';
 import { useBasePath } from '../../../../common/lib/kibana';
 import { CursorPosition } from '../../../../common/components/markdown_editor';
@@ -16,8 +17,10 @@ export const useInsertTimeline = <T extends FormData>(form: FormHook<T>, fieldNa
     end: 0,
   });
   const handleOnTimelineChange = useCallback(
-    (title: string, id: string | null) => {
-      const builtLink = `${basePath}/app/security/timelines?timeline=(id:'${id}',isOpen:!t)`;
+    (title: string, id: string | null, graphEventId?: string) => {
+      const builtLink = `${basePath}/app/security/timelines?timeline=(id:'${id}'${
+        !isEmpty(graphEventId) ? `,graphEventId:'${graphEventId}'` : ''
+      },isOpen:!t)`;
       const currentValue = form.getFormData()[fieldName];
       const newValue: string = [
         currentValue.slice(0, cursorPosition.start),
@@ -28,16 +31,12 @@ export const useInsertTimeline = <T extends FormData>(form: FormHook<T>, fieldNa
       ].join('');
       form.setFieldValue(fieldName, newValue);
     },
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [form]
-  );
-  const handleCursorChange = useCallback(
-    (cp: CursorPosition) => {
-      setCursorPosition(cp);
-    },
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [cursorPosition]
+    [basePath, cursorPosition, fieldName, form]
   );
+  const handleCursorChange = useCallback((cp: CursorPosition) => {
+    setCursorPosition(cp);
+  }, []);
+
   return {
     cursorPosition,
     handleCursorChange,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
index d8c9d2ed02cc6..aec09a95b4b19 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx
@@ -17,6 +17,7 @@ jest.mock('../../../../common/lib/kibana', () => {
     useKibana: jest.fn().mockReturnValue({
       services: {
         application: {
+          navigateToApp: jest.fn(),
           capabilities: {
             siem: {
               crud: true,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
index f2e7d26c9e851..528af23191ee9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
@@ -20,7 +20,6 @@ import {
 import React, { useCallback } from 'react';
 import uuid from 'uuid';
 import styled from 'styled-components';
-import { useHistory } from 'react-router-dom';
 import { useDispatch, useSelector } from 'react-redux';
 
 import { APP_ID } from '../../../../../common/constants';
@@ -28,11 +27,10 @@ import {
   TimelineTypeLiteral,
   TimelineStatus,
   TimelineType,
+  TimelineId,
 } from '../../../../../common/types/timeline';
-import { navTabs } from '../../../../app/home/home_navigations';
 import { SecurityPageName } from '../../../../app/types';
 import { timelineSelectors } from '../../../../timelines/store/timeline';
-import { useGetUrlSearch } from '../../../../common/components/navigation/use_get_url_search';
 import { getCreateCaseUrl } from '../../../../common/components/link_to';
 import { State } from '../../../../common/store';
 import { useKibana } from '../../../../common/lib/kibana';
@@ -44,7 +42,7 @@ import { AssociateNote, UpdateNote } from '../../notes/helpers';
 import { NOTES_PANEL_WIDTH } from './notes_size';
 import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles';
 import * as i18n from './translations';
-import { setInsertTimeline } from '../../../store/timeline/actions';
+import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions';
 import { useCreateTimelineButton } from './use_create_timeline';
 
 export const historyToolTip = 'The chronological history of actions related to this timeline';
@@ -139,6 +137,8 @@ export const Name = React.memo<NameProps>(({ timelineId, title, updateTitle }) =
 Name.displayName = 'Name';
 
 interface NewCaseProps {
+  compact?: boolean;
+  graphEventId?: string;
   onClosePopover: () => void;
   timelineId: string;
   timelineStatus: TimelineStatus;
@@ -146,44 +146,50 @@ interface NewCaseProps {
 }
 
 export const NewCase = React.memo<NewCaseProps>(
-  ({ onClosePopover, timelineId, timelineStatus, timelineTitle }) => {
-    const history = useHistory();
-    const urlSearch = useGetUrlSearch(navTabs.case);
+  ({ compact, graphEventId, onClosePopover, timelineId, timelineStatus, timelineTitle }) => {
     const dispatch = useDispatch();
     const { savedObjectId } = useSelector((state: State) =>
       timelineSelectors.selectTimeline(state, timelineId)
     );
     const { navigateToApp } = useKibana().services.application;
+    const buttonText = compact ? i18n.ATTACH_TO_NEW_CASE : i18n.ATTACH_TIMELINE_TO_NEW_CASE;
 
     const handleClick = useCallback(() => {
       onClosePopover();
       dispatch(
         setInsertTimeline({
+          graphEventId,
           timelineId,
           timelineSavedObjectId: savedObjectId,
           timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE,
         })
       );
+      dispatch(showTimeline({ id: TimelineId.active, show: false }));
+
       navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
-        path: getCreateCaseUrl(urlSearch),
-      });
-      history.push({
-        pathname: `/${SecurityPageName.case}/create`,
+        path: getCreateCaseUrl(),
       });
-
-      // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [dispatch, navigateToApp, onClosePopover, history, timelineId, timelineTitle, urlSearch]);
+    }, [
+      dispatch,
+      graphEventId,
+      navigateToApp,
+      onClosePopover,
+      savedObjectId,
+      timelineId,
+      timelineTitle,
+    ]);
 
     return (
       <EuiButtonEmpty
         data-test-subj="attach-timeline-case"
-        color="text"
+        color={compact ? undefined : 'text'}
         iconSide="left"
         iconType="paperClip"
         disabled={timelineStatus === TimelineStatus.draft}
         onClick={handleClick}
+        size={compact ? 'xs' : undefined}
       >
-        {i18n.ATTACH_TIMELINE_TO_NEW_CASE}
+        {buttonText}
       </EuiButtonEmpty>
     );
   }
@@ -191,28 +197,33 @@ export const NewCase = React.memo<NewCaseProps>(
 NewCase.displayName = 'NewCase';
 
 interface ExistingCaseProps {
+  compact?: boolean;
   onClosePopover: () => void;
   onOpenCaseModal: () => void;
   timelineStatus: TimelineStatus;
 }
 export const ExistingCase = React.memo<ExistingCaseProps>(
-  ({ onClosePopover, onOpenCaseModal, timelineStatus }) => {
+  ({ compact, onClosePopover, onOpenCaseModal, timelineStatus }) => {
     const handleClick = useCallback(() => {
       onClosePopover();
       onOpenCaseModal();
     }, [onOpenCaseModal, onClosePopover]);
+    const buttonText = compact
+      ? i18n.ATTACH_TO_EXISTING_CASE
+      : i18n.ATTACH_TIMELINE_TO_EXISTING_CASE;
 
     return (
       <>
         <EuiButtonEmpty
           data-test-subj="attach-timeline-existing-case"
-          color="text"
+          color={compact ? undefined : 'text'}
           iconSide="left"
           iconType="paperClip"
           disabled={timelineStatus === TimelineStatus.draft}
           onClick={handleClick}
+          size={compact ? 'xs' : undefined}
         >
-          {i18n.ATTACH_TIMELINE_TO_EXISTING_CASE}
+          {buttonText}
         </EuiButtonEmpty>
       </>
     );
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
index 3078700a29d76..1b76db409484f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
@@ -17,7 +17,6 @@ import {
 import { createStore, State } from '../../../../common/store';
 import { useThrottledResizeObserver } from '../../../../common/components/utils';
 import { Properties, showDescriptionThreshold, showNotesThreshold } from '.';
-import { SecurityPageName } from '../../../../app/types';
 import { setInsertTimeline } from '../../../store/timeline/actions';
 export { nextTick } from '../../../../../../../test_utils';
 
@@ -25,12 +24,13 @@ import { act } from 'react-dom/test-utils';
 
 jest.mock('../../../../common/components/link_to');
 
+const mockNavigateToApp = jest.fn();
 jest.mock('../../../../common/lib/kibana', () => {
   const original = jest.requireActual('../../../../common/lib/kibana');
 
   return {
     ...original,
-    useKibana: jest.fn().mockReturnValue({
+    useKibana: () => ({
       services: {
         application: {
           capabilities: {
@@ -38,7 +38,7 @@ jest.mock('../../../../common/lib/kibana', () => {
               crud: true,
             },
           },
-          navigateToApp: jest.fn(),
+          navigateToApp: mockNavigateToApp,
         },
       },
     }),
@@ -63,7 +63,6 @@ jest.mock('react-redux', () => {
     useSelector: jest.fn().mockReturnValue({ savedObjectId: '1', urlState: {} }),
   };
 });
-const mockHistoryPush = jest.fn();
 
 jest.mock('react-router-dom', () => {
   const original = jest.requireActual('react-router-dom');
@@ -71,7 +70,7 @@ jest.mock('react-router-dom', () => {
   return {
     ...original,
     useHistory: () => ({
-      push: mockHistoryPush,
+      push: jest.fn(),
     }),
   };
 });
@@ -342,8 +341,7 @@ describe('Properties', () => {
     );
     wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click');
     wrapper.find('[data-test-subj="attach-timeline-case"]').first().simulate('click');
-
-    expect(mockHistoryPush).toBeCalledWith({ pathname: `/${SecurityPageName.case}/create` });
+    expect(mockNavigateToApp).toBeCalledWith('securitySolution:case', { path: '/create' });
     expect(mockDispatch).toBeCalledWith(
       setInsertTimeline({
         timelineId: defaultProps.timelineId,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
index 602a7c8191c7a..8029d166a688a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
@@ -46,6 +46,7 @@ interface Props {
   createTimeline: CreateTimeline;
   description: string;
   getNotesByIds: (noteIds: string[]) => Note[];
+  graphEventId?: string;
   isDataInTimeline: boolean;
   isDatepickerLocked: boolean;
   isFavorite: boolean;
@@ -79,6 +80,7 @@ export const Properties = React.memo<Props>(
     createTimeline,
     description,
     getNotesByIds,
+    graphEventId,
     isDataInTimeline,
     isDatepickerLocked,
     isFavorite,
@@ -120,18 +122,21 @@ export const Properties = React.memo<Props>(
     const onRowClick = useCallback(
       (id: string) => {
         onCloseCaseModal();
-        navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
-          path: getCaseDetailsUrl({ id }),
-        });
+
         dispatch(
           setInsertTimeline({
+            graphEventId,
             timelineId,
             timelineSavedObjectId: currentTimeline.savedObjectId,
             timelineTitle: title.length > 0 ? title : i18n.UNTITLED_TIMELINE,
           })
         );
+
+        navigateToApp(`${APP_ID}:${SecurityPageName.case}`, {
+          path: getCaseDetailsUrl({ id }),
+        });
       },
-      [navigateToApp, onCloseCaseModal, currentTimeline, dispatch, timelineId, title]
+      [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title]
     );
 
     const datePickerWidth = useMemo(
@@ -174,6 +179,7 @@ export const Properties = React.memo<Props>(
           associateNote={associateNote}
           description={description}
           getNotesByIds={getNotesByIds}
+          graphEventId={graphEventId}
           isDataInTimeline={isDataInTimeline}
           noteIds={noteIds}
           onButtonClick={onButtonClick}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
index 7d176d57b5d81..e20a3db80d881 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
@@ -68,6 +68,7 @@ interface PropertiesRightComponentProps {
   associateNote: AssociateNote;
   description: string;
   getNotesByIds: (noteIds: string[]) => Note[];
+  graphEventId?: string;
   isDataInTimeline: boolean;
   noteIds: string[];
   onButtonClick: () => void;
@@ -94,6 +95,7 @@ const PropertiesRightComponent: React.FC<PropertiesRightComponentProps> = ({
   associateNote,
   description,
   getNotesByIds,
+  graphEventId,
   isDataInTimeline,
   noteIds,
   onButtonClick,
@@ -166,6 +168,7 @@ const PropertiesRightComponent: React.FC<PropertiesRightComponentProps> = ({
 
               <EuiFlexItem grow={false}>
                 <NewCase
+                  graphEventId={graphEventId}
                   onClosePopover={onClosePopover}
                   timelineId={timelineId}
                   timelineTitle={title}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
index 1621afc91cdbf..2568f41275401 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
@@ -123,10 +123,24 @@ export const ATTACH_TIMELINE_TO_NEW_CASE = i18n.translate(
   }
 );
 
+export const ATTACH_TO_NEW_CASE = i18n.translate(
+  'xpack.securitySolution.timeline.properties.attachToNewCaseButtonLabel',
+  {
+    defaultMessage: 'Attach to new case',
+  }
+);
+
 export const ATTACH_TIMELINE_TO_EXISTING_CASE = i18n.translate(
   'xpack.securitySolution.timeline.properties.existingCaseButtonLabel',
   {
-    defaultMessage: 'Attach timeline to existing case',
+    defaultMessage: 'Attach timeline to existing case...',
+  }
+);
+
+export const ATTACH_TO_EXISTING_CASE = i18n.translate(
+  'xpack.securitySolution.timeline.properties.attachToExistingCaseButtonLabel',
+  {
+    defaultMessage: 'Attach to existing case...',
   }
 );
 
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
index 76c3a647a9439..56c7c3dcfeb76 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
@@ -88,7 +88,11 @@ export interface SelectableTimelineProps {
     searchTimelineValue,
   }: GetSelectableOptions) => EuiSelectableOption[];
   onClosePopover: () => void;
-  onTimelineChange: (timelineTitle: string, timelineId: string | null) => void;
+  onTimelineChange: (
+    timelineTitle: string,
+    timelineId: string | null,
+    graphEventId?: string
+  ) => void;
   timelineType: TimelineTypeLiteral;
 }
 
@@ -202,7 +206,8 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
           isEmpty(selectedTimeline[0].title)
             ? i18nTimeline.UNTITLED_TIMELINE
             : selectedTimeline[0].title,
-          selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id
+          selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id,
+          selectedTimeline[0].graphEventId ?? ''
         );
       }
       onClosePopover();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
index aad80cbdfe337..55bcbbecda269 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
@@ -24,11 +24,12 @@ export const TimelineBodyGlobalStyle = createGlobalStyle`
 
 export const TimelineBody = styled.div.attrs(({ className = '' }) => ({
   className: `siemTimeline__body ${className}`,
-}))<{ bodyHeight?: number }>`
+}))<{ bodyHeight?: number; visible: boolean }>`
   height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')};
   overflow: auto;
   scrollbar-width: thin;
   flex: 1;
+  visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
 
   &::-webkit-scrollbar {
     height: ${({ theme }) => theme.eui.euiScrollBar};
@@ -89,10 +90,9 @@ export const EventsTrHeader = styled.div.attrs(({ className }) => ({
 
 export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({
   className: `siemEventsTable__thGroupActions ${className}`,
-}))<{ actionsColumnWidth: number; justifyContent: string }>`
+}))<{ actionsColumnWidth: number }>`
   display: flex;
   flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`};
-  justify-content: ${({ justifyContent }) => justifyContent};
   min-width: 0;
 `;
 
@@ -139,14 +139,17 @@ export const EventsTh = styled.div.attrs(({ className = '' }) => ({
 
 export const EventsThContent = styled.div.attrs(({ className = '' }) => ({
   className: `siemEventsTable__thContent ${className}`,
-}))<{ textAlign?: string }>`
+}))<{ textAlign?: string; width?: number }>`
   font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
   font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
   line-height: ${({ theme }) => theme.eui.euiLineHeight};
   min-width: 0;
   padding: ${({ theme }) => theme.eui.paddingSizes.xs};
   text-align: ${({ textAlign }) => textAlign};
-  width: 100%; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
+  width: ${({ width }) =>
+    width != null
+      ? `${width}px`
+      : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
 `;
 
 /* EVENTS BODY */
@@ -202,7 +205,6 @@ export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({
   className: `siemEventsTable__tdGroupActions ${className}`,
 }))<{ actionsColumnWidth: number }>`
   display: flex;
-  justify-content: space-between;
   flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`};
   min-width: 0;
 `;
@@ -234,14 +236,17 @@ export const EventsTd = styled.div.attrs<WidthProp>(({ className = '', width })
 `;
 
 export const EventsTdContent = styled.div.attrs(({ className }) => ({
-  className: `siemEventsTable__tdContent ${className}`,
-}))<{ textAlign?: string }>`
+  className: `siemEventsTable__tdContent ${className != null ? className : ''}`,
+}))<{ textAlign?: string; width?: number }>`
   font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
   line-height: ${({ theme }) => theme.eui.euiLineHeight};
   min-width: 0;
   padding: ${({ theme }) => theme.eui.paddingSizes.xs};
   text-align: ${({ textAlign }) => textAlign};
-  width: 100%; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
+  width: ${({ width }) =>
+    width != null
+      ? `${width}px`
+      : '100%'}; /* Using width: 100% instead of flex: 1 and max-width: 100% for IE11 */
 `;
 
 /**
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
index 96703941f616e..79ec58711e06c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
@@ -103,7 +103,11 @@ describe('Timeline', () => {
 
   describe('rendering', () => {
     test('renders correctly against snapshot', () => {
-      const wrapper = shallow(<TimelineComponent {...props} />);
+      const wrapper = shallow(
+        <TestProviders>
+          <TimelineComponent {...props} />
+        </TestProviders>
+      );
 
       expect(wrapper).toMatchSnapshot();
     });
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
index 884d693ca6ade..85e3d5d9478b6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
@@ -7,6 +7,7 @@
 import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter } from '@elastic/eui';
 import { getOr, isEmpty } from 'lodash/fp';
 import React, { useState, useMemo, useEffect } from 'react';
+import { useDispatch } from 'react-redux';
 import styled from 'styled-components';
 
 import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button';
@@ -16,6 +17,7 @@ import { Direction } from '../../../graphql/types';
 import { useKibana } from '../../../common/lib/kibana';
 import { ColumnHeaderOptions, KqlMode, EventType } from '../../../timelines/store/timeline/model';
 import { defaultHeaders } from './body/column_headers/default_headers';
+import { getInvestigateInResolverAction } from './body/helpers';
 import { Sort } from './body/sort';
 import { StatefulBody } from './body/stateful_body';
 import { DataProvider } from './data_providers/data_provider';
@@ -88,6 +90,7 @@ export interface Props {
   end: number;
   eventType?: EventType;
   filters: Filter[];
+  graphEventId?: string;
   id: string;
   indexPattern: IIndexPattern;
   indexToAdd: string[];
@@ -119,6 +122,7 @@ export const TimelineComponent: React.FC<Props> = ({
   end,
   eventType,
   filters,
+  graphEventId,
   id,
   indexPattern,
   indexToAdd,
@@ -141,6 +145,7 @@ export const TimelineComponent: React.FC<Props> = ({
   toggleColumn,
   usersViewing,
 }) => {
+  const dispatch = useDispatch();
   const kibana = useKibana();
   const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings));
   const combinedQueries = combineQueries({
@@ -168,9 +173,14 @@ export const TimelineComponent: React.FC<Props> = ({
     initializeTimeline,
     setIsTimelineLoading,
     setTimelineFilterManager,
+    setTimelineRowActions,
   } = useManageTimeline();
   useEffect(() => {
     initializeTimeline({ id, indexToAdd });
+    setTimelineRowActions({
+      id,
+      timelineRowActions: [getInvestigateInResolverAction({ dispatch, timelineId: id })],
+    });
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
   useEffect(() => {
@@ -197,6 +207,7 @@ export const TimelineComponent: React.FC<Props> = ({
             indexPattern={indexPattern}
             dataProviders={dataProviders}
             filterManager={filterManager}
+            graphEventId={graphEventId}
             onDataProviderEdited={onDataProviderEdited}
             onDataProviderRemoved={onDataProviderRemoved}
             onToggleDataProviderEnabled={onToggleDataProviderEnabled}
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts
index 53d0b98570bcb..e2a268e750b4a 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/index.gql_query.ts
@@ -89,6 +89,9 @@ export const timelineQuery = gql`
                 timezone
                 type
               }
+              agent {
+                type
+              }
               auditd {
                 result
                 session
@@ -285,6 +288,7 @@ export const timelineQuery = gql`
                 name
                 ppid
                 args
+                entity_id
                 executable
                 title
                 working_directory
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
index c5df017604b0c..55e6849fdb6c4 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
@@ -87,6 +87,10 @@ export const removeProvider = actionCreator<{
 
 export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE');
 
+export const updateTimelineGraphEventId = actionCreator<{ id: string; graphEventId: string }>(
+  'UPDATE_TIMELINE_GRAPH_EVENT_ID'
+);
+
 export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT');
 
 export const updateTimeline = actionCreator<{
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
index 15f956fa79d3c..c0615d36f7a2e 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
@@ -228,6 +228,26 @@ export const updateTimelineShowTimeline = ({
   };
 };
 
+export const updateGraphEventId = ({
+  id,
+  graphEventId,
+  timelineById,
+}: {
+  id: string;
+  graphEventId: string;
+  timelineById: TimelineById;
+}): TimelineById => {
+  const timeline = timelineById[id];
+
+  return {
+    ...timelineById,
+    [id]: {
+      ...timeline,
+      graphEventId,
+    },
+  };
+};
+
 interface ApplyDeltaToCurrentWidthParams {
   id: string;
   delta: number;
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
index caad70226365a..e8ea3c8d16e3a 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
@@ -55,6 +55,8 @@ export interface TimelineModel {
   /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */
   eventIdToNoteIds: Record<string, string[]>;
   filters?: Filter[];
+  /** When non-empty, display a graph view for this event */
+  graphEventId?: string;
   /** The chronological history of actions related to this timeline */
   historyIds: string[];
   /** The chronological history of actions related to this timeline */
@@ -129,6 +131,7 @@ export type SubsetTimelineModel = Readonly<
     | 'description'
     | 'eventType'
     | 'eventIdToNoteIds'
+    | 'graphEventId'
     | 'highlightedDropAndProviderId'
     | 'historyIds'
     | 'isFavorite'
@@ -165,4 +168,5 @@ export type SubsetTimelineModel = Readonly<
 export interface TimelineUrl {
   id: string;
   isOpen: boolean;
+  graphEventId?: string;
 }
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
index 3bdb16be79939..6e7a36079a0c3 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts
@@ -1788,6 +1788,7 @@ describe('Timeline', () => {
           isLoading: false,
           id: 'foo',
           savedObjectId: null,
+          showRowRenderers: true,
           kqlMode: 'filter',
           kqlQuery: { filterQuery: null, filterQueryDraft: null },
           loadingEventIds: [],
@@ -1802,7 +1803,6 @@ describe('Timeline', () => {
           },
           selectedEventIds: {},
           show: true,
-          showRowRenderers: true,
           showCheckboxes: false,
           sort: {
             columnId: '@timestamp',
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
index 5e314f1597451..30b7f73c839d1 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
@@ -53,6 +53,7 @@ import {
   updateRange,
   updateSort,
   updateTimeline,
+  updateTimelineGraphEventId,
   updateTitle,
   upsertColumn,
 } from './actions';
@@ -94,6 +95,7 @@ import {
   updateTimelineTitle,
   upsertTimelineColumn,
   updateSavedQuery,
+  updateGraphEventId,
   updateFilters,
   updateTimelineEventType,
 } from './helpers';
@@ -194,6 +196,10 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
     ...state,
     timelineById: updateTimelineShowTimeline({ id, show, timelineById: state.timelineById }),
   }))
+  .case(updateTimelineGraphEventId, (state, { id, graphEventId }) => ({
+    ...state,
+    timelineById: updateGraphEventId({ id, graphEventId, timelineById: state.timelineById }),
+  }))
   .case(applyDeltaToColumnWidth, (state, { id, columnId, delta }) => ({
     ...state,
     timelineById: applyDeltaToTimelineColumnWidth({
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
index 5262c72a6140c..65798648f92c6 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
@@ -23,6 +23,7 @@ export interface TimelineById {
 }
 
 export interface InsertTimeline {
+  graphEventId?: string;
   timelineId: string;
   timelineSavedObjectId: string | null;
   timelineTitle: string;
diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts
index 9bf55cfe1ed2a..52011e1416717 100644
--- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts
+++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts
@@ -60,6 +60,10 @@ export const ecsSchema = gql`
     sequence: ToStringArray
   }
 
+  type AgentEcsField {
+    type: ToStringArray
+  }
+
   type AuditdData {
     acct: ToStringArray
     terminal: ToStringArray
@@ -110,6 +114,7 @@ export const ecsSchema = gql`
     name: ToStringArray
     ppid: ToNumberArray
     args: ToStringArray
+    entity_id: ToStringArray
     executable: ToStringArray
     title: ToStringArray
     thread: Thread
@@ -425,6 +430,7 @@ export const ecsSchema = gql`
   type ECS {
     _id: String!
     _index: String
+    agent: AgentEcsField
     auditd: AuditdEcsFields
     destination: DestinationEcsFields
     dns: DnsEcsFields
diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts
index 4a063647a183d..40666b6193928 100644
--- a/x-pack/plugins/security_solution/server/graphql/types.ts
+++ b/x-pack/plugins/security_solution/server/graphql/types.ts
@@ -765,6 +765,8 @@ export interface Ecs {
 
   _index?: Maybe<string>;
 
+  agent?: Maybe<AgentEcsField>;
+
   auditd?: Maybe<AuditdEcsFields>;
 
   destination?: Maybe<DestinationEcsFields>;
@@ -812,6 +814,10 @@ export interface Ecs {
   system?: Maybe<SystemEcsField>;
 }
 
+export interface AgentEcsField {
+  type?: Maybe<string[] | string>;
+}
+
 export interface AuditdEcsFields {
   result?: Maybe<string[] | string>;
 
@@ -1267,6 +1273,8 @@ export interface ProcessEcsFields {
 
   args?: Maybe<string[] | string>;
 
+  entity_id?: Maybe<string[] | string>;
+
   executable?: Maybe<string[] | string>;
 
   title?: Maybe<string[] | string>;
@@ -4083,6 +4091,8 @@ export namespace EcsResolvers {
 
     _index?: _IndexResolver<Maybe<string>, TypeParent, TContext>;
 
+    agent?: AgentResolver<Maybe<AgentEcsField>, TypeParent, TContext>;
+
     auditd?: AuditdResolver<Maybe<AuditdEcsFields>, TypeParent, TContext>;
 
     destination?: DestinationResolver<Maybe<DestinationEcsFields>, TypeParent, TContext>;
@@ -4140,6 +4150,11 @@ export namespace EcsResolvers {
     Parent,
     TContext
   >;
+  export type AgentResolver<
+    R = Maybe<AgentEcsField>,
+    Parent = Ecs,
+    TContext = SiemContext
+  > = Resolver<R, Parent, TContext>;
   export type AuditdResolver<
     R = Maybe<AuditdEcsFields>,
     Parent = Ecs,
@@ -4257,6 +4272,18 @@ export namespace EcsResolvers {
   > = Resolver<R, Parent, TContext>;
 }
 
+export namespace AgentEcsFieldResolvers {
+  export interface Resolvers<TContext = SiemContext, TypeParent = AgentEcsField> {
+    type?: TypeResolver<Maybe<string[] | string>, TypeParent, TContext>;
+  }
+
+  export type TypeResolver<
+    R = Maybe<string[] | string>,
+    Parent = AgentEcsField,
+    TContext = SiemContext
+  > = Resolver<R, Parent, TContext>;
+}
+
 export namespace AuditdEcsFieldsResolvers {
   export interface Resolvers<TContext = SiemContext, TypeParent = AuditdEcsFields> {
     result?: ResultResolver<Maybe<string[] | string>, TypeParent, TContext>;
@@ -5761,6 +5788,8 @@ export namespace ProcessEcsFieldsResolvers {
 
     args?: ArgsResolver<Maybe<string[] | string>, TypeParent, TContext>;
 
+    entity_id?: EntityIdResolver<Maybe<string[] | string>, TypeParent, TContext>;
+
     executable?: ExecutableResolver<Maybe<string[] | string>, TypeParent, TContext>;
 
     title?: TitleResolver<Maybe<string[] | string>, TypeParent, TContext>;
@@ -5795,6 +5824,11 @@ export namespace ProcessEcsFieldsResolvers {
     Parent = ProcessEcsFields,
     TContext = SiemContext
   > = Resolver<R, Parent, TContext>;
+  export type EntityIdResolver<
+    R = Maybe<string[] | string>,
+    Parent = ProcessEcsFields,
+    TContext = SiemContext
+  > = Resolver<R, Parent, TContext>;
   export type ExecutableResolver<
     R = Maybe<string[] | string>,
     Parent = ProcessEcsFields,
@@ -9110,6 +9144,7 @@ export type IResolvers<TContext = SiemContext> = {
   TimelineItem?: TimelineItemResolvers.Resolvers<TContext>;
   TimelineNonEcsData?: TimelineNonEcsDataResolvers.Resolvers<TContext>;
   Ecs?: EcsResolvers.Resolvers<TContext>;
+  AgentEcsField?: AgentEcsFieldResolvers.Resolvers<TContext>;
   AuditdEcsFields?: AuditdEcsFieldsResolvers.Resolvers<TContext>;
   AuditdData?: AuditdDataResolvers.Resolvers<TContext>;
   Summary?: SummaryResolvers.Resolvers<TContext>;
diff --git a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts
index f2662c79d3393..ff474c4a841f6 100644
--- a/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts
+++ b/x-pack/plugins/security_solution/server/lib/ecs_fields/index.ts
@@ -76,12 +76,17 @@ export const processFieldsMap: Readonly<Record<string, string>> = {
   'process.name': 'process.name',
   'process.ppid': 'process.ppid',
   'process.args': 'process.args',
+  'process.entity_id': 'process.entity_id',
   'process.executable': 'process.executable',
   'process.title': 'process.title',
   'process.thread': 'process.thread',
   'process.working_directory': 'process.working_directory',
 };
 
+export const agentFieldsMap: Readonly<Record<string, string>> = {
+  'agent.type': 'agent.type',
+};
+
 export const userFieldsMap: Readonly<Record<string, string>> = {
   'user.domain': 'user.domain',
   'user.id': 'user.id',
@@ -327,6 +332,7 @@ export const eventFieldsMap: Readonly<Record<string, string>> = {
   timestamp: '@timestamp',
   '@timestamp': '@timestamp',
   message: 'message',
+  ...{ ...agentFieldsMap },
   ...{ ...auditdMap },
   ...{ ...destinationFieldsMap },
   ...{ ...dnsFieldsMap },

From 5c8df21ca0dc034d8f19f6a7936a9360f6e14e46 Mon Sep 17 00:00:00 2001
From: Michael Olorunnisola <michael.olorunnisola@elastic.co>
Date: Fri, 26 Jun 2020 17:38:02 -0400
Subject: [PATCH 16/21] Hide unused resolver buttons (#70112)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 .../public/resolver/store/actions.ts          | 10 ------
 .../public/resolver/view/index.tsx            |  4 +--
 .../resolver/view/process_event_dot.tsx       | 33 ++++++-------------
 .../public/resolver/view/submenu.tsx          |  5 ---
 4 files changed, 12 insertions(+), 40 deletions(-)

diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts
index c633d791e8bf2..ae302d0e60911 100644
--- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts
+++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts
@@ -141,15 +141,6 @@ interface UserSelectedRelatedEventCategory {
   };
 }
 
-/**
- * This action should dispatch to indicate that the user chose to focus
- * on examining alerts related to a particular ResolverEvent
- */
-interface UserSelectedRelatedAlerts {
-  readonly type: 'userSelectedRelatedAlerts';
-  readonly payload: ResolverEvent;
-}
-
 export type ResolverAction =
   | CameraAction
   | DataAction
@@ -160,7 +151,6 @@ export type ResolverAction =
   | UserSelectedResolverNode
   | UserRequestedRelatedEventData
   | UserSelectedRelatedEventCategory
-  | UserSelectedRelatedAlerts
   | AppDetectedNewIdFromQueryParams
   | AppDisplayedDifferentPanel
   | AppDetectedMissingEventData;
diff --git a/x-pack/plugins/security_solution/public/resolver/view/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/index.tsx
index 9b7114b56495c..5c188fdc71156 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/index.tsx
@@ -136,7 +136,7 @@ export const Resolver = React.memo(function Resolver({
               projectionMatrix={projectionMatrix}
             />
           ))}
-          {[...processNodePositions].map(([processEvent, position], index) => {
+          {[...processNodePositions].map(([processEvent, position]) => {
             const adjacentNodeMap = processToAdjacencyMap.get(processEvent);
             const processEntityId = entityId(processEvent);
             if (!adjacentNodeMap) {
@@ -145,7 +145,7 @@ export const Resolver = React.memo(function Resolver({
             }
             return (
               <ProcessEventDot
-                key={index}
+                key={processEntityId}
                 position={position}
                 projectionMatrix={projectionMatrix}
                 event={processEvent}
diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
index e7c9960f78052..a2249e1920bc4 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx
@@ -414,13 +414,6 @@ const ProcessEventDotComponents = React.memo(
       });
     }, [dispatch, selfId]);
 
-    const handleRelatedAlertsRequest = useCallback(() => {
-      dispatch({
-        type: 'userSelectedRelatedAlerts',
-        payload: event,
-      });
-    }, [dispatch, event]);
-
     const history = useHistory();
     const urlSearch = history.location.search;
 
@@ -637,22 +630,16 @@ const ProcessEventDotComponents = React.memo(
             }}
           >
             <EuiFlexItem grow={false} className="related-dropdown">
-              <NodeSubMenu
-                count={grandTotal}
-                buttonBorderColor={labelButtonFill}
-                buttonFill={colorMap.resolverBackground}
-                menuAction={handleRelatedEventRequest}
-                menuTitle={subMenuAssets.relatedEvents.title}
-                optionsWithActions={relatedEventStatusOrOptions}
-              />
-            </EuiFlexItem>
-            <EuiFlexItem grow={false}>
-              <NodeSubMenu
-                buttonBorderColor={labelButtonFill}
-                buttonFill={colorMap.resolverBackground}
-                menuTitle={subMenuAssets.relatedAlerts.title}
-                menuAction={handleRelatedAlertsRequest}
-              />
+              {grandTotal > 0 && (
+                <NodeSubMenu
+                  count={grandTotal}
+                  buttonBorderColor={labelButtonFill}
+                  buttonFill={colorMap.resolverBackground}
+                  menuAction={handleRelatedEventRequest}
+                  menuTitle={subMenuAssets.relatedEvents.title}
+                  optionsWithActions={relatedEventStatusOrOptions}
+                />
+              )}
             </EuiFlexItem>
           </EuiFlexGroup>
         </StyledActionsContainer>
diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
index 8f972dd737af6..d3bb6123ce04d 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
@@ -31,11 +31,6 @@ export const subMenuAssets = {
   menuError: i18n.translate('xpack.securitySolution.endpoint.resolver.relatedRetrievalError', {
     defaultMessage: 'There was an error retrieving related events.',
   }),
-  relatedAlerts: {
-    title: i18n.translate('xpack.securitySolution.endpoint.resolver.relatedAlerts', {
-      defaultMessage: 'Related Alerts',
-    }),
-  },
   relatedEvents: {
     title: i18n.translate('xpack.securitySolution.endpoint.resolver.relatedEvents', {
       defaultMessage: 'Events',

From 5236335d63575e5c5c988e7f2bbd3b14270567cd Mon Sep 17 00:00:00 2001
From: Kevin Logan <56395104+kevinlog@users.noreply.github.com>
Date: Fri, 26 Jun 2020 18:08:07 -0400
Subject: [PATCH 17/21] [Endpoint] Add Endpoint empty states for onboarding
 (#69626)

---
 .../hooks/use_intra_app_state.tsx             |   6 +-
 .../agent_config/components/actions_menu.tsx  | 184 ++++++------
 .../agent_config/details_page/index.tsx       |  27 +-
 .../types/intra_app_route_state.ts            |  12 +-
 .../components/management_empty_state.tsx     | 277 ++++++++++++++++++
 .../pages/endpoint_hosts/store/action.ts      |  42 ++-
 .../pages/endpoint_hosts/store/index.test.ts  |   4 +
 .../pages/endpoint_hosts/store/middleware.ts  |  77 ++++-
 .../pages/endpoint_hosts/store/reducer.ts     |  40 +++
 .../pages/endpoint_hosts/store/selectors.ts   |  13 +
 .../management/pages/endpoint_hosts/types.ts  |  10 +
 .../pages/endpoint_hosts/view/index.test.tsx  |  52 +++-
 .../pages/endpoint_hosts/view/index.tsx       | 150 ++++++++--
 .../pages/policy/view/policy_list.tsx         | 118 +-------
 14 files changed, 783 insertions(+), 229 deletions(-)
 create mode 100644 x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx
index 565c5b364893c..7bccd3a4b1f58 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx
@@ -28,10 +28,11 @@ export const IntraAppStateProvider = memo<{
 }>(({ kibanaScopedHistory, children }) => {
   const internalAppToAppState = useMemo<IntraAppState>(() => {
     return {
-      forRoute: kibanaScopedHistory.location.hash.substr(1),
+      forRoute: new URL(`${kibanaScopedHistory.location.hash.substr(1)}`, 'http://localhost')
+        .pathname,
       routeState: kibanaScopedHistory.location.state as AnyIntraAppRouteState,
     };
-  }, [kibanaScopedHistory.location.hash, kibanaScopedHistory.location.state]);
+  }, [kibanaScopedHistory.location.state, kibanaScopedHistory.location.hash]);
   return (
     <IntraAppStateContext.Provider value={internalAppToAppState}>
       {children}
@@ -57,6 +58,7 @@ export function useIntraAppState<S = AnyIntraAppRouteState>():
     // once so that it does not impact navigation to the page from within the
     // ingest app. side affect is that the browser back button would not work
     // consistently either.
+
     if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) {
       wasHandled.add(intraAppState);
       return intraAppState.routeState as S;
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx
index 39fe090e5008c..86d191d4ff904 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx
@@ -3,7 +3,7 @@
  * 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, useState } from 'react';
+import React, { memo, useState, useMemo } from 'react';
 import { FormattedMessage } from '@kbn/i18n/react';
 import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
 import { AgentConfig } from '../../../types';
@@ -17,86 +17,106 @@ export const AgentConfigActionMenu = memo<{
   config: AgentConfig;
   onCopySuccess?: (newAgentConfig: AgentConfig) => void;
   fullButton?: boolean;
-}>(({ config, onCopySuccess, fullButton = false }) => {
-  const hasWriteCapabilities = useCapabilities().write;
-  const [isYamlFlyoutOpen, setIsYamlFlyoutOpen] = useState<boolean>(false);
-  const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(false);
+  enrollmentFlyoutOpenByDefault?: boolean;
+  onCancelEnrollment?: () => void;
+}>(
+  ({
+    config,
+    onCopySuccess,
+    fullButton = false,
+    enrollmentFlyoutOpenByDefault = false,
+    onCancelEnrollment,
+  }) => {
+    const hasWriteCapabilities = useCapabilities().write;
+    const [isYamlFlyoutOpen, setIsYamlFlyoutOpen] = useState<boolean>(false);
+    const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(
+      enrollmentFlyoutOpenByDefault
+    );
 
-  return (
-    <AgentConfigCopyProvider>
-      {(copyAgentConfigPrompt) => {
-        return (
-          <>
-            {isYamlFlyoutOpen ? (
-              <EuiPortal>
-                <ConfigYamlFlyout configId={config.id} onClose={() => setIsYamlFlyoutOpen(false)} />
-              </EuiPortal>
-            ) : null}
-            {isEnrollmentFlyoutOpen && (
-              <EuiPortal>
-                <AgentEnrollmentFlyout
-                  agentConfigs={[config]}
-                  onClose={() => setIsEnrollmentFlyoutOpen(false)}
-                />
-              </EuiPortal>
-            )}
-            <ContextMenuActions
-              button={
-                fullButton
-                  ? {
-                      props: {
-                        iconType: 'arrowDown',
-                        iconSide: 'right',
-                      },
-                      children: (
-                        <FormattedMessage
-                          id="xpack.ingestManager.agentConfigActionMenu.buttonText"
-                          defaultMessage="Actions"
-                        />
-                      ),
-                    }
-                  : undefined
-              }
-              items={[
-                <EuiContextMenuItem
-                  disabled={!hasWriteCapabilities}
-                  icon="plusInCircle"
-                  onClick={() => setIsEnrollmentFlyoutOpen(true)}
-                  key="enrollAgents"
-                >
-                  <FormattedMessage
-                    id="xpack.ingestManager.agentConfigActionMenu.enrollAgentActionText"
-                    defaultMessage="Enroll agent"
-                  />
-                </EuiContextMenuItem>,
-                <EuiContextMenuItem
-                  icon="inspect"
-                  onClick={() => setIsYamlFlyoutOpen(!isYamlFlyoutOpen)}
-                  key="viewConfig"
-                >
-                  <FormattedMessage
-                    id="xpack.ingestManager.agentConfigActionMenu.viewConfigText"
-                    defaultMessage="View config"
-                  />
-                </EuiContextMenuItem>,
-                <EuiContextMenuItem
-                  disabled={!hasWriteCapabilities}
-                  icon="copy"
-                  onClick={() => {
-                    copyAgentConfigPrompt(config, onCopySuccess);
-                  }}
-                  key="copyConfig"
-                >
-                  <FormattedMessage
-                    id="xpack.ingestManager.agentConfigActionMenu.copyConfigActionText"
-                    defaultMessage="Copy config"
+    const onClose = useMemo(() => {
+      if (onCancelEnrollment) {
+        return onCancelEnrollment;
+      } else {
+        return () => setIsEnrollmentFlyoutOpen(false);
+      }
+    }, [onCancelEnrollment, setIsEnrollmentFlyoutOpen]);
+
+    return (
+      <AgentConfigCopyProvider>
+        {(copyAgentConfigPrompt) => {
+          return (
+            <>
+              {isYamlFlyoutOpen ? (
+                <EuiPortal>
+                  <ConfigYamlFlyout
+                    configId={config.id}
+                    onClose={() => setIsYamlFlyoutOpen(false)}
                   />
-                </EuiContextMenuItem>,
-              ]}
-            />
-          </>
-        );
-      }}
-    </AgentConfigCopyProvider>
-  );
-});
+                </EuiPortal>
+              ) : null}
+              {isEnrollmentFlyoutOpen && (
+                <EuiPortal>
+                  <AgentEnrollmentFlyout agentConfigs={[config]} onClose={onClose} />
+                </EuiPortal>
+              )}
+              <ContextMenuActions
+                button={
+                  fullButton
+                    ? {
+                        props: {
+                          iconType: 'arrowDown',
+                          iconSide: 'right',
+                        },
+                        children: (
+                          <FormattedMessage
+                            id="xpack.ingestManager.agentConfigActionMenu.buttonText"
+                            defaultMessage="Actions"
+                          />
+                        ),
+                      }
+                    : undefined
+                }
+                items={[
+                  <EuiContextMenuItem
+                    disabled={!hasWriteCapabilities}
+                    icon="plusInCircle"
+                    onClick={() => setIsEnrollmentFlyoutOpen(true)}
+                    key="enrollAgents"
+                  >
+                    <FormattedMessage
+                      id="xpack.ingestManager.agentConfigActionMenu.enrollAgentActionText"
+                      defaultMessage="Enroll agent"
+                    />
+                  </EuiContextMenuItem>,
+                  <EuiContextMenuItem
+                    icon="inspect"
+                    onClick={() => setIsYamlFlyoutOpen(!isYamlFlyoutOpen)}
+                    key="viewConfig"
+                  >
+                    <FormattedMessage
+                      id="xpack.ingestManager.agentConfigActionMenu.viewConfigText"
+                      defaultMessage="View config"
+                    />
+                  </EuiContextMenuItem>,
+                  <EuiContextMenuItem
+                    disabled={!hasWriteCapabilities}
+                    icon="copy"
+                    onClick={() => {
+                      copyAgentConfigPrompt(config, onCopySuccess);
+                    }}
+                    key="copyConfig"
+                  >
+                    <FormattedMessage
+                      id="xpack.ingestManager.agentConfigActionMenu.copyConfigActionText"
+                      defaultMessage="Copy config"
+                    />
+                  </EuiContextMenuItem>,
+                ]}
+              />
+            </>
+          );
+        }}
+      </AgentConfigCopyProvider>
+    );
+  }
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx
index 410c0fcb2d140..eaa161d57bbe4 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx
@@ -3,8 +3,8 @@
  * 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, { useMemo, useState } from 'react';
-import { Redirect, useRouteMatch, Switch, Route, useHistory } from 'react-router-dom';
+import React, { useMemo, useState, useCallback } from 'react';
+import { Redirect, useRouteMatch, Switch, Route, useHistory, useLocation } from 'react-router-dom';
 import { i18n } from '@kbn/i18n';
 import { FormattedMessage, FormattedDate } from '@kbn/i18n/react';
 import {
@@ -21,14 +21,15 @@ import {
 } from '@elastic/eui';
 import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
 import styled from 'styled-components';
-import { AgentConfig } from '../../../types';
+import { AgentConfig, AgentConfigDetailsDeployAgentAction } from '../../../types';
 import { PAGE_ROUTING_PATHS } from '../../../constants';
-import { useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks';
+import { useGetOneAgentConfig, useLink, useBreadcrumbs, useCore } from '../../../hooks';
 import { Loading } from '../../../components';
 import { WithHeaderLayout } from '../../../layouts';
 import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks';
 import { LinkedAgentCount, AgentConfigActionMenu } from '../components';
 import { ConfigDatasourcesView, ConfigSettingsView } from './components';
+import { useIntraAppState } from '../../../hooks/use_intra_app_state';
 
 const Divider = styled.div`
   width: 0;
@@ -48,7 +49,13 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => {
   const [redirectToAgentConfigList] = useState<boolean>(false);
   const agentStatusRequest = useGetAgentStatus(configId);
   const { refreshAgentStatus } = agentStatusRequest;
+  const {
+    application: { navigateToApp },
+  } = useCore();
+  const routeState = useIntraAppState<AgentConfigDetailsDeployAgentAction>();
   const agentStatus = agentStatusRequest.data?.results;
+  const queryParams = new URLSearchParams(useLocation().search);
+  const openEnrollmentFlyoutOpenByDefault = queryParams.get('openEnrollmentFlyout') === 'true';
 
   const headerLeftContent = useMemo(
     () => (
@@ -95,6 +102,12 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => {
     [getHref, agentConfig, configId]
   );
 
+  const enrollmentCancelClickHandler = useCallback(() => {
+    if (routeState && routeState.onDoneNavigateTo) {
+      navigateToApp(routeState.onDoneNavigateTo[0], routeState.onDoneNavigateTo[1]);
+    }
+  }, [routeState, navigateToApp]);
+
   const headerRightContent = useMemo(
     () => (
       <EuiFlexGroup justifyContent={'flexEnd'} direction="row">
@@ -155,6 +168,12 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => {
                 onCopySuccess={(newAgentConfig: AgentConfig) => {
                   history.push(getPath('configuration_details', { configId: newAgentConfig.id }));
                 }}
+                enrollmentFlyoutOpenByDefault={openEnrollmentFlyoutOpenByDefault}
+                onCancelEnrollment={
+                  routeState && routeState.onDoneNavigateTo
+                    ? enrollmentCancelClickHandler
+                    : undefined
+                }
               />
             ),
           },
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts
index 6e85d12f71891..b2948686ff6e5 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts
@@ -21,7 +21,17 @@ export interface CreateDatasourceRouteState {
   onCancelUrl?: string;
 }
 
+/**
+ * Supported routing state for the agent config details page routes with deploy agents action
+ */
+export interface AgentConfigDetailsDeployAgentAction {
+  /** On done, navigate to the given app */
+  onDoneNavigateTo?: Parameters<ApplicationStart['navigateToApp']>;
+}
+
 /**
  * All possible Route states.
  */
-export type AnyIntraAppRouteState = CreateDatasourceRouteState;
+export type AnyIntraAppRouteState =
+  | CreateDatasourceRouteState
+  | AgentConfigDetailsDeployAgentAction;
diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
new file mode 100644
index 0000000000000..5dd47d4e88028
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx
@@ -0,0 +1,277 @@
+/*
+ * 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, { useMemo, MouseEvent, CSSProperties } from 'react';
+import {
+  EuiText,
+  EuiFlexGroup,
+  EuiFlexItem,
+  EuiSpacer,
+  EuiButton,
+  EuiSteps,
+  EuiTitle,
+  EuiSelectable,
+  EuiSelectableMessage,
+  EuiSelectableProps,
+  EuiLoadingSpinner,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+const TEXT_ALIGN_CENTER: CSSProperties = Object.freeze({
+  textAlign: 'center',
+});
+
+interface ManagementStep {
+  title: string;
+  children: JSX.Element;
+}
+
+const PolicyEmptyState = React.memo<{
+  loading: boolean;
+  onActionClick: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
+  actionDisabled?: boolean;
+}>(({ loading, onActionClick, actionDisabled }) => {
+  const policySteps = useMemo(
+    () => [
+      {
+        title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepOneTitle', {
+          defaultMessage: 'Head over to Ingest Manager.',
+        }),
+        children: (
+          <EuiText color="subdued" size="xs">
+            <FormattedMessage
+              id="xpack.securitySolution.endpoint.policyList.stepOne"
+              defaultMessage="Here, you’ll add the Elastic Endpoint Security Integration to your Agent Configuration."
+            />
+          </EuiText>
+        ),
+      },
+      {
+        title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepTwoTitle', {
+          defaultMessage: 'We’ll create a recommended security policy for you.',
+        }),
+        children: (
+          <EuiText color="subdued" size="xs">
+            <FormattedMessage
+              id="xpack.securitySolution.endpoint.policyList.stepTwo"
+              defaultMessage="You can edit this policy in the “Policies” tab after you’ve added the Elastic Endpoint integration."
+            />
+          </EuiText>
+        ),
+      },
+      {
+        title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepThreeTitle', {
+          defaultMessage: 'Enroll your agents through Fleet.',
+        }),
+        children: (
+          <EuiText color="subdued" size="xs">
+            <FormattedMessage
+              id="xpack.securitySolution.endpoint.policyList.stepThree"
+              defaultMessage="If you haven’t already, enroll your agents through Fleet using the same agent configuration."
+            />
+          </EuiText>
+        ),
+      },
+    ],
+    []
+  );
+
+  return (
+    <ManagementEmptyState
+      loading={loading}
+      onActionClick={onActionClick}
+      actionDisabled={actionDisabled}
+      dataTestSubj="emptyPolicyTable"
+      steps={policySteps}
+      headerComponent={
+        <FormattedMessage
+          id="xpack.securitySolution.endpoint.policyList.noPolicyPrompt"
+          defaultMessage="Looks like you're not using the Elastic Endpoint"
+        />
+      }
+      bodyComponent={
+        <FormattedMessage
+          id="xpack.securitySolution.endpoint.policyList.noPolicyInstructions"
+          defaultMessage="Elastic Endpoint Security gives you the power to keep your endpoints safe from attack, as well as unparalleled visibility into any threat in your environment."
+        />
+      }
+    />
+  );
+});
+
+const EndpointsEmptyState = React.memo<{
+  loading: boolean;
+  onActionClick: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
+  actionDisabled: boolean;
+  handleSelectableOnChange: (o: EuiSelectableProps['options']) => void;
+  selectionOptions: EuiSelectableProps['options'];
+}>(({ loading, onActionClick, actionDisabled, handleSelectableOnChange, selectionOptions }) => {
+  const policySteps = useMemo(
+    () => [
+      {
+        title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepOneTitle', {
+          defaultMessage: 'Select a policy you created from the list below.',
+        }),
+        children: (
+          <>
+            <EuiText color="subdued" size="xs">
+              <FormattedMessage
+                id="xpack.securitySolution.endpoint.endpointList.stepOne"
+                defaultMessage="These are existing policies."
+              />
+            </EuiText>
+            <EuiSpacer size="m" />
+            <EuiSelectable
+              options={selectionOptions}
+              singleSelection="always"
+              isLoading={loading}
+              height={100}
+              listProps={{ bordered: true, singleSelection: true }}
+              onChange={handleSelectableOnChange}
+              data-test-subj="onboardingPolicySelect"
+            >
+              {(list) => {
+                return loading ? (
+                  <EuiSelectableMessage>
+                    <FormattedMessage
+                      id="xpack.securitySolution.endpoint.endpointList.loadingPolicies"
+                      defaultMessage="Loading policy configs"
+                    />
+                  </EuiSelectableMessage>
+                ) : selectionOptions.length ? (
+                  list
+                ) : (
+                  <FormattedMessage
+                    id="xpack.securitySolution.endpoint.endpointList.noPolicies"
+                    defaultMessage="There are no policies."
+                  />
+                );
+              }}
+            </EuiSelectable>
+          </>
+        ),
+      },
+      {
+        title: i18n.translate('xpack.securitySolution.endpoint.endpointList.stepTwoTitle', {
+          defaultMessage:
+            'Head over to Ingest to deploy your Agent with Endpoint Security enabled.',
+        }),
+        children: (
+          <EuiText color="subdued" size="xs">
+            <FormattedMessage
+              id="xpack.securitySolution.endpoint.endpointList.stepTwo"
+              defaultMessage="You'll be given a command in Ingest to get you started."
+            />
+          </EuiText>
+        ),
+      },
+    ],
+    [selectionOptions, handleSelectableOnChange, loading]
+  );
+
+  return (
+    <ManagementEmptyState
+      loading={loading}
+      onActionClick={onActionClick}
+      actionDisabled={actionDisabled}
+      dataTestSubj="emptyEndpointsTable"
+      steps={policySteps}
+      headerComponent={
+        <FormattedMessage
+          id="xpack.securitySolution.endpoint.endpointList.noEndpointsPrompt"
+          defaultMessage="You have a policy, but no Endpoints are deployed!"
+        />
+      }
+      bodyComponent={
+        <FormattedMessage
+          id="xpack.securitySolution.endpoint.endpointList.noEndpointsInstructions"
+          defaultMessage="Elastic Endpoint Security gives you the power to keep your endpoints safe from attack, as well as unparalleled visibility into any threat in your environment."
+        />
+      }
+    />
+  );
+});
+
+const ManagementEmptyState = React.memo<{
+  loading: boolean;
+  onActionClick?: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
+  actionDisabled?: boolean;
+  actionButton?: JSX.Element;
+  dataTestSubj: string;
+  steps?: ManagementStep[];
+  headerComponent: JSX.Element;
+  bodyComponent: JSX.Element;
+}>(
+  ({
+    loading,
+    onActionClick,
+    actionDisabled,
+    dataTestSubj,
+    steps,
+    actionButton,
+    headerComponent,
+    bodyComponent,
+  }) => {
+    return (
+      <div data-test-subj={dataTestSubj}>
+        {loading ? (
+          <EuiFlexGroup alignItems="center" justifyContent="center">
+            <EuiFlexItem grow={false}>
+              <EuiLoadingSpinner size="xl" className="essentialAnimation" />
+            </EuiFlexItem>
+          </EuiFlexGroup>
+        ) : (
+          <>
+            <EuiSpacer size="xxl" />
+            <EuiTitle size="m">
+              <h2 style={TEXT_ALIGN_CENTER}>{headerComponent}</h2>
+            </EuiTitle>
+            <EuiSpacer size="xxl" />
+            <EuiText textAlign="center" color="subdued" size="s">
+              {bodyComponent}
+            </EuiText>
+            <EuiSpacer size="xxl" />
+            {steps && (
+              <EuiFlexGroup alignItems="center" justifyContent="center">
+                <EuiFlexItem grow={false}>
+                  <EuiSteps steps={steps} data-test-subj={'onboardingSteps'} />
+                </EuiFlexItem>
+              </EuiFlexGroup>
+            )}
+            <EuiFlexGroup alignItems="center" justifyContent="center">
+              <EuiFlexItem grow={false}>
+                <>
+                  {actionButton ? (
+                    actionButton
+                  ) : (
+                    <EuiButton
+                      fill
+                      onClick={onActionClick}
+                      isDisabled={actionDisabled}
+                      data-test-subj="onboardingStartButton"
+                    >
+                      <FormattedMessage
+                        id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
+                        defaultMessage="Click here to get started"
+                      />
+                    </EuiButton>
+                  )}
+                </>
+              </EuiFlexItem>
+            </EuiFlexGroup>
+          </>
+        )}
+      </div>
+    );
+  }
+);
+
+PolicyEmptyState.displayName = 'PolicyEmptyState';
+EndpointsEmptyState.displayName = 'EndpointsEmptyState';
+ManagementEmptyState.displayName = 'ManagementEmptyState';
+
+export { PolicyEmptyState, EndpointsEmptyState, ManagementEmptyState };
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts
index 62a2d9e3205c2..4c01b3644cf63 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts
@@ -10,6 +10,8 @@ import {
   GetHostPolicyResponse,
 } from '../../../../../common/endpoint/types';
 import { ServerApiError } from '../../../../common/types';
+import { GetPolicyListResponse } from '../../policy/types';
+import { GetPackagesResponse } from '../../../../../../ingest_manager/common';
 
 interface ServerReturnedHostList {
   type: 'serverReturnedHostList';
@@ -41,10 +43,48 @@ interface ServerFailedToReturnHostPolicyResponse {
   payload: ServerApiError;
 }
 
+interface ServerReturnedPoliciesForOnboarding {
+  type: 'serverReturnedPoliciesForOnboarding';
+  payload: {
+    policyItems: GetPolicyListResponse['items'];
+  };
+}
+
+interface ServerFailedToReturnPoliciesForOnboarding {
+  type: 'serverFailedToReturnPoliciesForOnboarding';
+  payload: ServerApiError;
+}
+
+interface UserSelectedEndpointPolicy {
+  type: 'userSelectedEndpointPolicy';
+  payload: {
+    selectedPolicyId: string;
+  };
+}
+
+interface ServerCancelledHostListLoading {
+  type: 'serverCancelledHostListLoading';
+}
+
+interface ServerCancelledPolicyItemsLoading {
+  type: 'serverCancelledPolicyItemsLoading';
+}
+
+interface ServerReturnedEndpointPackageInfo {
+  type: 'serverReturnedEndpointPackageInfo';
+  payload: GetPackagesResponse['response'][0];
+}
+
 export type HostAction =
   | ServerReturnedHostList
   | ServerFailedToReturnHostList
   | ServerReturnedHostDetails
   | ServerFailedToReturnHostDetails
   | ServerReturnedHostPolicyResponse
-  | ServerFailedToReturnHostPolicyResponse;
+  | ServerFailedToReturnHostPolicyResponse
+  | ServerReturnedPoliciesForOnboarding
+  | ServerFailedToReturnPoliciesForOnboarding
+  | UserSelectedEndpointPolicy
+  | ServerCancelledHostListLoading
+  | ServerCancelledPolicyItemsLoading
+  | ServerReturnedEndpointPackageInfo;
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
index 71452993abf07..f2c205661b32c 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts
@@ -46,6 +46,10 @@ describe('HostList store concerns', () => {
         policyResponseLoading: false,
         policyResponseError: undefined,
         location: undefined,
+        policyItems: [],
+        selectedPolicyId: undefined,
+        policyItemsLoading: false,
+        endpointPackageInfo: undefined,
       });
     });
 
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
index 85667c9f9fc37..ce164318fdadc 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts
@@ -5,9 +5,20 @@
  */
 
 import { HostResultList } from '../../../../../common/endpoint/types';
+import { GetPolicyListResponse } from '../../policy/types';
 import { ImmutableMiddlewareFactory } from '../../../../common/store';
-import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors';
+import {
+  isOnHostPage,
+  hasSelectedHost,
+  uiQueryParams,
+  listData,
+  endpointPackageInfo,
+} from './selectors';
 import { HostState } from '../types';
+import {
+  sendGetEndpointSpecificDatasources,
+  sendGetEndpointSecurityPackage,
+} from '../../policy/store/policy_list/services/ingest';
 
 export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (coreStart) => {
   return ({ getState, dispatch }) => (next) => async (action) => {
@@ -18,17 +29,34 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (cor
       isOnHostPage(state) &&
       hasSelectedHost(state) !== true
     ) {
+      if (!endpointPackageInfo(state)) {
+        sendGetEndpointSecurityPackage(coreStart.http)
+          .then((packageInfo) => {
+            dispatch({
+              type: 'serverReturnedEndpointPackageInfo',
+              payload: packageInfo,
+            });
+          })
+          .catch((error) => {
+            // eslint-disable-next-line no-console
+            console.error(error);
+          });
+      }
+
       const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state);
+      let hostResponse;
+
       try {
-        const response = await coreStart.http.post<HostResultList>('/api/endpoint/metadata', {
+        hostResponse = await coreStart.http.post<HostResultList>('/api/endpoint/metadata', {
           body: JSON.stringify({
             paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }],
           }),
         });
-        response.request_page_index = Number(pageIndex);
+        hostResponse.request_page_index = Number(pageIndex);
+
         dispatch({
           type: 'serverReturnedHostList',
-          payload: response,
+          payload: hostResponse,
         });
       } catch (error) {
         dispatch({
@@ -36,8 +64,45 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (cor
           payload: error,
         });
       }
+
+      // No hosts, so we should check to see if there are policies for onboarding
+      if (hostResponse && hostResponse.hosts.length === 0) {
+        const http = coreStart.http;
+        try {
+          const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificDatasources(
+            http,
+            {
+              query: {
+                perPage: 50, // Since this is an oboarding flow, we'll cap at 50 policies.
+                page: 1,
+              },
+            }
+          );
+
+          dispatch({
+            type: 'serverReturnedPoliciesForOnboarding',
+            payload: {
+              policyItems: policyDataResponse.items,
+            },
+          });
+        } catch (error) {
+          dispatch({
+            type: 'serverFailedToReturnPoliciesForOnboarding',
+            payload: error.body ?? error,
+          });
+          return;
+        }
+      } else {
+        dispatch({
+          type: 'serverCancelledPolicyItemsLoading',
+        });
+      }
     }
     if (action.type === 'userChangedUrl' && hasSelectedHost(state) === true) {
+      dispatch({
+        type: 'serverCancelledPolicyItemsLoading',
+      });
+
       // If user navigated directly to a host details page, load the host list
       if (listData(state).length === 0) {
         const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state);
@@ -59,6 +124,10 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = (cor
           });
           return;
         }
+      } else {
+        dispatch({
+          type: 'serverCancelledHostListLoading',
+        });
       }
 
       // call the host details api
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
index 23682544ec423..993267cf1a704 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts
@@ -24,8 +24,13 @@ export const initialHostListState: Immutable<HostState> = {
   policyResponseLoading: false,
   policyResponseError: undefined,
   location: undefined,
+  policyItems: [],
+  selectedPolicyId: undefined,
+  policyItemsLoading: false,
+  endpointPackageInfo: undefined,
 };
 
+/* eslint-disable-next-line complexity */
 export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
   state = initialHostListState,
   action
@@ -65,6 +70,18 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
       detailsError: action.payload,
       detailsLoading: false,
     };
+  } else if (action.type === 'serverReturnedPoliciesForOnboarding') {
+    return {
+      ...state,
+      policyItems: action.payload.policyItems,
+      policyItemsLoading: false,
+    };
+  } else if (action.type === 'serverFailedToReturnPoliciesForOnboarding') {
+    return {
+      ...state,
+      error: action.payload,
+      policyItemsLoading: false,
+    };
   } else if (action.type === 'serverReturnedHostPolicyResponse') {
     return {
       ...state,
@@ -78,6 +95,27 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
       policyResponseError: action.payload,
       policyResponseLoading: false,
     };
+  } else if (action.type === 'userSelectedEndpointPolicy') {
+    return {
+      ...state,
+      selectedPolicyId: action.payload.selectedPolicyId,
+      policyResponseLoading: false,
+    };
+  } else if (action.type === 'serverCancelledHostListLoading') {
+    return {
+      ...state,
+      loading: false,
+    };
+  } else if (action.type === 'serverCancelledPolicyItemsLoading') {
+    return {
+      ...state,
+      policyItemsLoading: false,
+    };
+  } else if (action.type === 'serverReturnedEndpointPackageInfo') {
+    return {
+      ...state,
+      endpointPackageInfo: action.payload,
+    };
   } else if (action.type === 'userChangedUrl') {
     const newState: Immutable<HostState> = {
       ...state,
@@ -95,6 +133,7 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
           ...state,
           location: action.payload,
           loading: true,
+          policyItemsLoading: true,
           error: undefined,
           detailsError: undefined,
         };
@@ -122,6 +161,7 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
           error: undefined,
           detailsError: undefined,
           policyResponseError: undefined,
+          policyItemsLoading: true,
         };
       }
     }
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
index 20365b3fe100b..e75d2129f61a5 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts
@@ -37,6 +37,19 @@ export const detailsLoading = (state: Immutable<HostState>): boolean => state.de
 
 export const detailsError = (state: Immutable<HostState>) => state.detailsError;
 
+export const policyItems = (state: Immutable<HostState>) => state.policyItems;
+
+export const policyItemsLoading = (state: Immutable<HostState>) => state.policyItemsLoading;
+
+export const selectedPolicyId = (state: Immutable<HostState>) => state.selectedPolicyId;
+
+export const endpointPackageInfo = (state: Immutable<HostState>) => state.endpointPackageInfo;
+
+export const endpointPackageVersion = createSelector(
+  endpointPackageInfo,
+  (info) => info?.version ?? undefined
+);
+
 /**
  * Returns the full policy response from the endpoint after a user modifies a policy.
  */
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
index 4881342c06573..a5f37a0b49e8f 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts
@@ -10,8 +10,10 @@ import {
   HostMetadata,
   HostPolicyResponse,
   AppLocation,
+  PolicyData,
 } from '../../../../common/endpoint/types';
 import { ServerApiError } from '../../../common/types';
+import { GetPackagesResponse } from '../../../../../ingest_manager/common';
 
 export interface HostState {
   /** list of host **/
@@ -40,6 +42,14 @@ export interface HostState {
   policyResponseError?: ServerApiError;
   /** current location info */
   location?: Immutable<AppLocation>;
+  /** policies */
+  policyItems: PolicyData[];
+  /** policies are loading */
+  policyItemsLoading: boolean;
+  /** the selected policy ID in the onboarding flow */
+  selectedPolicyId?: string;
+  /** Endpoint package info */
+  endpointPackageInfo?: GetPackagesResponse['response'][0];
 }
 
 /**
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
index 7bc101b891477..9690ac5c1b9bf 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx
@@ -40,12 +40,60 @@ describe('when on the hosts page', () => {
     expect(timelineFlyout).toBeNull();
   });
 
-  it('should show a table', async () => {
+  it('should show the empty state when there are no hosts or polices', async () => {
     const renderResult = render();
-    const table = await renderResult.findByTestId('hostListTable');
+    // Initially, there are no endpoints or policies, so we prompt to add policies first.
+    const table = await renderResult.findByTestId('emptyPolicyTable');
     expect(table).not.toBeNull();
   });
 
+  describe('when there are policies, but no hosts', () => {
+    beforeEach(() => {
+      reactTestingLibrary.act(() => {
+        const hostListData = mockHostResultList({ total: 0 });
+        coreStart.http.get.mockReturnValue(Promise.resolve(hostListData));
+        const hostAction: AppAction = {
+          type: 'serverReturnedHostList',
+          payload: hostListData,
+        };
+        store.dispatch(hostAction);
+
+        jest.clearAllMocks();
+
+        const policyListData = mockPolicyResultList({ total: 3 });
+        coreStart.http.get.mockReturnValue(Promise.resolve(policyListData));
+        const policyAction: AppAction = {
+          type: 'serverReturnedPoliciesForOnboarding',
+          payload: {
+            policyItems: policyListData.items,
+          },
+        };
+        store.dispatch(policyAction);
+      });
+    });
+    afterEach(() => {
+      jest.clearAllMocks();
+    });
+
+    it('should show the no hosts empty state', async () => {
+      const renderResult = render();
+      const emptyEndpointsTable = await renderResult.findByTestId('emptyEndpointsTable');
+      expect(emptyEndpointsTable).not.toBeNull();
+    });
+
+    it('should display the onboarding steps', async () => {
+      const renderResult = render();
+      const onboardingSteps = await renderResult.findByTestId('onboardingSteps');
+      expect(onboardingSteps).not.toBeNull();
+    });
+
+    it('should show policy selection', async () => {
+      const renderResult = render();
+      const onboardingPolicySelect = await renderResult.findByTestId('onboardingPolicySelect');
+      expect(onboardingPolicySelect).not.toBeNull();
+    });
+  });
+
   describe('when there is no selected host in the url', () => {
     it('should not show the flyout', () => {
       const renderResult = render();
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
index 45a33f76ee0c5..3601b8db5ee59 100644
--- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx
@@ -13,12 +13,13 @@ import {
   EuiLink,
   EuiHealth,
   EuiToolTip,
+  EuiSelectableProps,
 } from '@elastic/eui';
 import { useHistory } from 'react-router-dom';
 import { i18n } from '@kbn/i18n';
 import { FormattedMessage } from '@kbn/i18n/react';
 import { createStructuredSelector } from 'reselect';
-
+import { useDispatch } from 'react-redux';
 import { HostDetailsFlyout } from './details';
 import * as selectors from '../store/selectors';
 import { useHostSelector } from './hooks';
@@ -32,7 +33,13 @@ import { CreateStructuredSelector } from '../../../../common/store';
 import { Immutable, HostInfo } from '../../../../../common/endpoint/types';
 import { SpyRoute } from '../../../../common/utils/route/spy_routes';
 import { ManagementPageView } from '../../../components/management_page_view';
+import { PolicyEmptyState, EndpointsEmptyState } from '../../../components/management_empty_state';
 import { FormattedDate } from '../../../../common/components/formatted_date';
+import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
+import {
+  CreateDatasourceRouteState,
+  AgentConfigDetailsDeployAgentAction,
+} from '../../../../../../ingest_manager/public';
 import { SecurityPageName } from '../../../../app/types';
 import {
   getEndpointListPath,
@@ -40,6 +47,7 @@ import {
   getPolicyDetailPath,
 } from '../../../common/routing';
 import { useFormatUrl } from '../../../../common/components/link_to';
+import { HostAction } from '../store/action';
 
 const HostListNavLink = memo<{
   name: string;
@@ -75,9 +83,15 @@ export const HostList = () => {
     listError,
     uiQueryParams: queryParams,
     hasSelectedHost,
+    policyItems,
+    selectedPolicyId,
+    policyItemsLoading,
+    endpointPackageVersion,
   } = useHostSelector(selector);
   const { formatUrl, search } = useFormatUrl(SecurityPageName.management);
 
+  const dispatch = useDispatch<(a: HostAction) => void>();
+
   const paginationSetup = useMemo(() => {
     return {
       pageIndex,
@@ -104,6 +118,67 @@ export const HostList = () => {
     [history, queryParams]
   );
 
+  const handleCreatePolicyClick = useNavigateToAppEventHandler<CreateDatasourceRouteState>(
+    'ingestManager',
+    {
+      path: `#/integrations${
+        endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-datasource` : ''
+      }`,
+      state: {
+        onCancelNavigateTo: [
+          'securitySolution:management',
+          { path: getEndpointListPath({ name: 'endpointList' }) },
+        ],
+        onCancelUrl: formatUrl(getEndpointListPath({ name: 'endpointList' })),
+        onSaveNavigateTo: [
+          'securitySolution:management',
+          { path: getEndpointListPath({ name: 'endpointList' }) },
+        ],
+      },
+    }
+  );
+
+  const handleDeployEndpointsClick = useNavigateToAppEventHandler<
+    AgentConfigDetailsDeployAgentAction
+  >('ingestManager', {
+    path: `#/configs/${selectedPolicyId}?openEnrollmentFlyout=true`,
+    state: {
+      onDoneNavigateTo: [
+        'securitySolution:management',
+        { path: getEndpointListPath({ name: 'endpointList' }) },
+      ],
+    },
+  });
+
+  const selectionOptions = useMemo<EuiSelectableProps['options']>(() => {
+    return policyItems.map((item) => {
+      return {
+        key: item.config_id,
+        label: item.name,
+        checked: selectedPolicyId === item.config_id ? 'on' : undefined,
+      };
+    });
+  }, [policyItems, selectedPolicyId]);
+
+  const handleSelectableOnChange = useCallback<(o: EuiSelectableProps['options']) => void>(
+    (changedOptions) => {
+      return changedOptions.some((option) => {
+        if ('checked' in option && option.checked === 'on') {
+          dispatch({
+            type: 'userSelectedEndpointPolicy',
+            payload: {
+              selectedPolicyId: option.key as string,
+            },
+          });
+          return true;
+        } else {
+          return false;
+        }
+      });
+    },
+    [dispatch]
+  );
+
   const columns: Array<EuiBasicTableColumn<Immutable<HostInfo>>> = useMemo(() => {
     const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpointList.lastActive', {
       defaultMessage: 'Last Active',
@@ -252,6 +327,49 @@ export const HostList = () => {
     ];
   }, [formatUrl, queryParams, search]);
 
+  const renderTableOrEmptyState = useMemo(() => {
+    if (!loading && listData && listData.length > 0) {
+      return (
+        <EuiBasicTable
+          data-test-subj="hostListTable"
+          items={[...listData]}
+          columns={columns}
+          error={listError?.message}
+          pagination={paginationSetup}
+          onChange={onTableChange}
+        />
+      );
+    } else if (!policyItemsLoading && policyItems && policyItems.length > 0) {
+      return (
+        <EndpointsEmptyState
+          loading={loading}
+          onActionClick={handleDeployEndpointsClick}
+          actionDisabled={selectedPolicyId === undefined}
+          handleSelectableOnChange={handleSelectableOnChange}
+          selectionOptions={selectionOptions}
+        />
+      );
+    } else {
+      return (
+        <PolicyEmptyState loading={policyItemsLoading} onActionClick={handleCreatePolicyClick} />
+      );
+    }
+  }, [
+    listData,
+    policyItems,
+    columns,
+    loading,
+    paginationSetup,
+    onTableChange,
+    listError?.message,
+    handleCreatePolicyClick,
+    handleDeployEndpointsClick,
+    handleSelectableOnChange,
+    selectedPolicyId,
+    selectionOptions,
+    policyItemsLoading,
+  ]);
+
   return (
     <ManagementPageView
       viewType="list"
@@ -261,23 +379,19 @@ export const HostList = () => {
       })}
     >
       {hasSelectedHost && <HostDetailsFlyout />}
-      <EuiText color="subdued" size="xs" data-test-subj="hostListTableTotal">
-        <FormattedMessage
-          id="xpack.securitySolution.endpointList.totalCount"
-          defaultMessage="{totalItemCount, plural, one {# Host} other {# Hosts}}"
-          values={{ totalItemCount }}
-        />
-      </EuiText>
-      <EuiHorizontalRule margin="xs" />
-      <EuiBasicTable
-        data-test-subj="hostListTable"
-        items={useMemo(() => [...listData], [listData])}
-        columns={columns}
-        loading={loading}
-        error={listError?.message}
-        pagination={paginationSetup}
-        onChange={onTableChange}
-      />
+      {listData && listData.length > 0 && (
+        <>
+          <EuiText color="subdued" size="xs" data-test-subj="hostListTableTotal">
+            <FormattedMessage
+              id="xpack.securitySolution.endpointList.totalCount"
+              defaultMessage="{totalItemCount, plural, one {# Host} other {# Hosts}}"
+              values={{ totalItemCount }}
+            />
+          </EuiText>
+          <EuiHorizontalRule margin="xs" />
+        </>
+      )}
+      {renderTableOrEmptyState}
       <SpyRoute pageName={SecurityPageName.management} />
     </ManagementPageView>
   );
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
index 26b6ecb540cd9..8a760334c53af 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
@@ -4,7 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import React, { useCallback, useEffect, useMemo, CSSProperties, useState, MouseEvent } from 'react';
+import React, { useCallback, useEffect, useMemo, CSSProperties, useState } from 'react';
 import {
   EuiBasicTable,
   EuiText,
@@ -22,9 +22,6 @@ import {
   EuiCallOut,
   EuiSpacer,
   EuiButton,
-  EuiSteps,
-  EuiTitle,
-  EuiProgress,
 } from '@elastic/eui';
 import { i18n } from '@kbn/i18n';
 import { FormattedMessage } from '@kbn/i18n/react';
@@ -41,6 +38,7 @@ import { Immutable, PolicyData } from '../../../../../common/endpoint/types';
 import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
 import { LinkToApp } from '../../../../common/components/endpoint/link_to_app';
 import { ManagementPageView } from '../../../components/management_page_view';
+import { PolicyEmptyState } from '../../../components/management_empty_state';
 import { SpyRoute } from '../../../../common/utils/route/spy_routes';
 import { FormattedDateAndTime } from '../../../../common/components/endpoint/formatted_date_time';
 import { SecurityPageName } from '../../../../app/types';
@@ -65,10 +63,6 @@ const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({
   whiteSpace: 'nowrap',
 });
 
-const TEXT_ALIGN_CENTER: CSSProperties = Object.freeze({
-  textAlign: 'center',
-});
-
 const DangerEuiContextMenuItem = styled(EuiContextMenuItem)`
   color: ${(props) => props.theme.eui.textColors.danger};
 `;
@@ -437,12 +431,7 @@ export const PolicyList = React.memo(() => {
                   hasActions={false}
                 />
               ) : (
-                <EmptyPolicyTable
-                  loading={loading}
-                  onActionClick={handleCreatePolicyClick}
-                  actionDisabled={false}
-                  dataTestSubj="emptyPolicyTable"
-                />
+                <PolicyEmptyState loading={loading} onActionClick={handleCreatePolicyClick} />
               )}
             </>
           );
@@ -462,107 +451,6 @@ export const PolicyList = React.memo(() => {
 
 PolicyList.displayName = 'PolicyList';
 
-const EmptyPolicyTable = React.memo<{
-  loading: boolean;
-  onActionClick: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
-  actionDisabled: boolean;
-  dataTestSubj: string;
-}>(({ loading, onActionClick, actionDisabled, dataTestSubj }) => {
-  const policySteps = useMemo(
-    () => [
-      {
-        title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepOneTitle', {
-          defaultMessage: 'Head over to Ingest Manager.',
-        }),
-        children: (
-          <EuiText color="subdued" size="xs">
-            <FormattedMessage
-              id="xpack.securitySolution.endpoint.policyList.stepOne"
-              defaultMessage="Here, you’ll add the Elastic Endpoint Security Integration to your Agent Configuration."
-            />
-          </EuiText>
-        ),
-      },
-      {
-        title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepTwoTitle', {
-          defaultMessage: 'We’ll create a recommended security policy for you.',
-        }),
-        children: (
-          <EuiText color="subdued" size="xs">
-            <FormattedMessage
-              id="xpack.securitySolution.endpoint.policyList.stepTwo"
-              defaultMessage="You can edit this policy in the “Policies” tab after you’ve added the Elastic Endpoint integration."
-            />
-          </EuiText>
-        ),
-      },
-      {
-        title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepThreeTitle', {
-          defaultMessage: 'Enroll your agents through Fleet.',
-        }),
-        children: (
-          <EuiText color="subdued" size="xs">
-            <FormattedMessage
-              id="xpack.securitySolution.endpoint.policyList.stepThree"
-              defaultMessage="If you haven’t already, enroll your agents through Fleet using the same agent configuration."
-            />
-          </EuiText>
-        ),
-      },
-    ],
-    []
-  );
-  return (
-    <div data-test-subj={dataTestSubj}>
-      {loading ? (
-        <EuiProgress size="xs" color="accent" className="essentialAnimation" />
-      ) : (
-        <>
-          <EuiSpacer size="xxl" />
-          <EuiTitle size="m">
-            <h2 style={TEXT_ALIGN_CENTER}>
-              <FormattedMessage
-                id="xpack.securitySolution.endpoint.policyList.noPoliciesPrompt"
-                defaultMessage="Looks like you're not using Elastic Endpoint"
-              />
-            </h2>
-          </EuiTitle>
-          <EuiSpacer size="xxl" />
-          <EuiText textAlign="center" color="subdued" size="s">
-            <FormattedMessage
-              id="xpack.securitySolution.endpoint.policyList.noPoliciesInstructions"
-              defaultMessage="Elastic Endpoint Security gives you the power to keep your endpoints safe from attack, as well as unparalleled visibility into any threat in your environment."
-            />
-          </EuiText>
-          <EuiSpacer size="xxl" />
-          <EuiFlexGroup alignItems="center" justifyContent="center">
-            <EuiFlexItem grow={false}>
-              <EuiSteps steps={policySteps} data-test-subj={'onboardingSteps'} />
-            </EuiFlexItem>
-          </EuiFlexGroup>
-          <EuiFlexGroup alignItems="center" justifyContent="center">
-            <EuiFlexItem grow={false}>
-              <EuiButton
-                fill
-                onClick={onActionClick}
-                isDisabled={actionDisabled}
-                data-test-subj="onboardingStartButton"
-              >
-                <FormattedMessage
-                  id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
-                  defaultMessage="Click here to get started"
-                />
-              </EuiButton>
-            </EuiFlexItem>
-          </EuiFlexGroup>
-        </>
-      )}
-    </div>
-  );
-});
-
-EmptyPolicyTable.displayName = 'EmptyPolicyTable';
-
 const ConfirmDelete = React.memo<{
   hostCount: number;
   isDeleting: boolean;

From 266f853b0bde6169fbe6622aca2146380bb8cbe9 Mon Sep 17 00:00:00 2001
From: Ahmad Bamieh <ahmadbamieh@gmail.com>
Date: Sat, 27 Jun 2020 02:52:26 +0300
Subject: [PATCH 18/21] [Telemetry] Collector Schema (#64942)

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
---
 .github/CODEOWNERS                            |   6 +
 .telemetryrc.json                             |  25 ++
 package.json                                  |   1 +
 packages/kbn-telemetry-tools/README.md        |  89 +++++++
 packages/kbn-telemetry-tools/babel.config.js  |  23 ++
 packages/kbn-telemetry-tools/package.json     |  22 ++
 .../src/cli/run_telemetry_check.ts            | 109 ++++++++
 .../src/cli/run_telemetry_extract.ts          |  75 ++++++
 packages/kbn-telemetry-tools/src/index.ts     |  21 ++
 .../src/tools/__fixture__/mock_schema.json    |  24 ++
 .../parsed_externally_defined_collector.ts    |  68 +++++
 .../__fixture__/parsed_imported_schema.ts     |  46 ++++
 .../parsed_imported_usage_interface.ts        |  46 ++++
 .../__fixture__/parsed_nested_collector.ts    |  44 ++++
 .../__fixture__/parsed_working_collector.ts   |  69 +++++
 .../extract_collectors.test.ts.snap           | 163 ++++++++++++
 .../__snapshots__/ts_parser.test.ts.snap      |   6 +
 .../tools/check_collector__integrity.test.ts  | 125 +++++++++
 .../src/tools/check_collector_integrity.ts    | 103 ++++++++
 .../src/tools/config.test.ts                  |  40 +++
 .../kbn-telemetry-tools/src/tools/config.ts   |  60 +++++
 .../src/tools/constants.ts                    |  20 ++
 .../src/tools/extract_collectors.test.ts      |  40 +++
 .../src/tools/extract_collectors.ts           |  75 ++++++
 .../src/tools/manage_schema.test.ts           |  39 +++
 .../src/tools/manage_schema.ts                |  86 ++++++
 .../src/tools/serializer.test.ts              | 105 ++++++++
 .../src/tools/serializer.ts                   | 169 ++++++++++++
 .../tasks/check_compatible_types_task.ts      |  43 +++
 .../tasks/check_matching_schemas_task.ts      |  40 +++
 .../src/tools/tasks/error_reporter.ts         |  34 +++
 .../tools/tasks/extract_collectors_task.ts    |  58 ++++
 .../src/tools/tasks/generate_schemas_task.ts  |  35 +++
 .../src/tools/tasks/index.ts                  |  28 ++
 .../src/tools/tasks/parse_configs_task.ts     |  46 ++++
 .../src/tools/tasks/task_context.ts           |  41 +++
 .../src/tools/tasks/write_to_file_task.ts     |  35 +++
 .../src/tools/ts_parser.test.ts               |  94 +++++++
 .../src/tools/ts_parser.ts                    | 210 +++++++++++++++
 .../kbn-telemetry-tools/src/tools/utils.ts    | 238 +++++++++++++++++
 packages/kbn-telemetry-tools/tsconfig.json    |   6 +
 scripts/telemetry_check.js                    |  21 ++
 scripts/telemetry_extract.js                  |  21 ++
 .../telemetry_collectors/.telemetryrc.json    |   7 +
 .../telemetry_collectors/constants.ts         |  53 ++++
 .../externally_defined_collector.ts           |  71 +++++
 .../file_with_no_collector.ts                 |  20 ++
 .../telemetry_collectors/imported_schema.ts   |  41 +++
 .../imported_usage_interface.ts               |  41 +++
 .../telemetry_collectors/nested_collector.ts  |  49 ++++
 .../unmapped_collector.ts                     |  39 +++
 .../telemetry_collectors/working_collector.ts |  81 ++++++
 .../csp_usage_collector/csp_collector.test.ts |  15 +-
 .../lib/csp_usage_collector/csp_collector.ts  |  27 +-
 .../kql_telemetry/usage_collector/fetch.ts    |  10 +-
 .../make_kql_usage_collector.ts               |  12 +-
 .../services/sample_data/usage/collector.ts   |  12 +-
 .../sample_data/usage/collector_fetch.ts      |   2 +-
 .../common/constants.ts                       |  21 --
 .../telemetry_application_usage_collector.ts  |   3 +-
 .../kibana/kibana_usage_collector.ts          |   4 +-
 .../telemetry_management_collector.ts         |   3 +-
 .../telemetry_ui_metric_collector.ts          |   3 +-
 src/plugins/telemetry/common/constants.ts     |   5 -
 .../telemetry/schema/legacy_oss_plugins.json  |  17 ++
 src/plugins/telemetry/schema/oss_plugins.json |  59 +++++
 .../telemetry_plugin_collector.ts             |  10 +-
 src/plugins/usage_collection/README.md        |  67 ++++-
 .../server/collector/collector.ts             |  24 ++
 .../server/collector/collector_set.ts         |   2 +-
 .../server/collector/index.ts                 |   8 +-
 src/plugins/usage_collection/server/index.ts  |   7 +
 .../validation_telemetry_service.ts           |   8 +-
 tasks/config/run.js                           |   6 +
 tasks/jenkins.js                              |   1 +
 x-pack/.telemetryrc.json                      |  14 +
 .../server/usage/actions_usage_collector.ts   |   4 +-
 .../server/usage/alerts_usage_collector.ts    |   4 +-
 x-pack/plugins/canvas/common/lib/constants.ts |   1 -
 .../canvas/server/collectors/collector.ts     |  13 +-
 x-pack/plugins/cloud/common/constants.ts      |   1 -
 .../collectors/cloud_usage_collector.ts       |  12 +-
 .../telemetry/file_upload_usage_collector.ts  |  20 +-
 .../infra/server/usage/usage_collector.ts     |   4 +-
 .../lib/telemetry/ml_usage_collector.ts       |  22 +-
 .../ml/server/lib/telemetry/telemetry.ts      |   2 +-
 x-pack/plugins/reporting/common/constants.ts  |   6 -
 .../server/usage/reporting_usage_collector.ts |  17 +-
 .../rollup/server/collectors/register.ts      |  35 ++-
 x-pack/plugins/spaces/common/constants.ts     |   6 -
 .../spaces_usage_collector.ts                 |  57 +++-
 .../schema/xpack_plugins.json                 | 247 ++++++++++++++++++
 .../server/lib/telemetry/usage_collector.ts   |  24 +-
 .../telemetry/kibana_telemetry_adapter.ts     |  32 ++-
 .../server/lib/adapters/telemetry/types.ts    |   6 +
 95 files changed, 3766 insertions(+), 138 deletions(-)
 create mode 100644 .telemetryrc.json
 create mode 100644 packages/kbn-telemetry-tools/README.md
 create mode 100644 packages/kbn-telemetry-tools/babel.config.js
 create mode 100644 packages/kbn-telemetry-tools/package.json
 create mode 100644 packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts
 create mode 100644 packages/kbn-telemetry-tools/src/cli/run_telemetry_extract.ts
 create mode 100644 packages/kbn-telemetry-tools/src/index.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json
 create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_externally_defined_collector.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_schema.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_usage_interface.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_nested_collector.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap
 create mode 100644 packages/kbn-telemetry-tools/src/tools/__snapshots__/ts_parser.test.ts.snap
 create mode 100644 packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/config.test.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/config.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/constants.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/extract_collectors.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/manage_schema.test.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/manage_schema.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/serializer.test.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/serializer.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/check_compatible_types_task.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/error_reporter.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/extract_collectors_task.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/generate_schemas_task.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/index.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/parse_configs_task.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/task_context.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/tasks/write_to_file_task.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/ts_parser.ts
 create mode 100644 packages/kbn-telemetry-tools/src/tools/utils.ts
 create mode 100644 packages/kbn-telemetry-tools/tsconfig.json
 create mode 100644 scripts/telemetry_check.js
 create mode 100644 scripts/telemetry_extract.js
 create mode 100644 src/fixtures/telemetry_collectors/.telemetryrc.json
 create mode 100644 src/fixtures/telemetry_collectors/constants.ts
 create mode 100644 src/fixtures/telemetry_collectors/externally_defined_collector.ts
 create mode 100644 src/fixtures/telemetry_collectors/file_with_no_collector.ts
 create mode 100644 src/fixtures/telemetry_collectors/imported_schema.ts
 create mode 100644 src/fixtures/telemetry_collectors/imported_usage_interface.ts
 create mode 100644 src/fixtures/telemetry_collectors/nested_collector.ts
 create mode 100644 src/fixtures/telemetry_collectors/unmapped_collector.ts
 create mode 100644 src/fixtures/telemetry_collectors/working_collector.ts
 create mode 100644 src/plugins/telemetry/schema/legacy_oss_plugins.json
 create mode 100644 src/plugins/telemetry/schema/oss_plugins.json
 create mode 100644 x-pack/.telemetryrc.json
 create mode 100644 x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json

diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index e6f6e83253c8b..47f9942162f75 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -170,6 +170,7 @@
 
 # Kibana Telemetry
 /packages/kbn-analytics/ @elastic/kibana-telemetry
+/packages/kbn-telemetry-tools/ @elastic/kibana-telemetry
 /src/plugins/kibana_usage_collection/ @elastic/kibana-telemetry
 /src/plugins/newsfeed/ @elastic/kibana-telemetry
 /src/plugins/telemetry/ @elastic/kibana-telemetry
@@ -177,6 +178,11 @@
 /src/plugins/telemetry_management_section/ @elastic/kibana-telemetry
 /src/plugins/usage_collection/ @elastic/kibana-telemetry
 /x-pack/plugins/telemetry_collection_xpack/ @elastic/kibana-telemetry
+/.telemetryrc.json @elastic/kibana-telemetry
+/x-pack/.telemetryrc.json @elastic/kibana-telemetry
+src/plugins/telemetry/schema/legacy_oss_plugins.json @elastic/kibana-telemetry
+src/plugins/telemetry/schema/oss_plugins.json @elastic/kibana-telemetry
+x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kibana-telemetry
 
 # Kibana Alerting Services
 /x-pack/plugins/alerts/ @elastic/kibana-alerting-services
diff --git a/.telemetryrc.json b/.telemetryrc.json
new file mode 100644
index 0000000000000..30643a104c1cd
--- /dev/null
+++ b/.telemetryrc.json
@@ -0,0 +1,25 @@
+[
+  {
+    "output": "src/plugins/telemetry/schema/legacy_oss_plugins.json",
+    "root": "src/legacy/core_plugins/",
+    "exclude": [
+      "src/legacy/core_plugins/testbed",
+      "src/legacy/core_plugins/elasticsearch",
+      "src/legacy/core_plugins/tests_bundle"
+    ]
+  },
+  {
+    "output": "src/plugins/telemetry/schema/oss_plugins.json",
+    "root": "src/plugins/",
+    "exclude": [
+      "src/plugins/kibana_react/",
+      "src/plugins/testbed/",
+      "src/plugins/kibana_utils/",
+      "src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts",
+      "src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts",
+      "src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts",
+      "src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts",
+      "src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts"
+    ]
+  }
+]
diff --git a/package.json b/package.json
index 10eaef8ed5dc7..b1202631a0c02 100644
--- a/package.json
+++ b/package.json
@@ -139,6 +139,7 @@
     "@kbn/babel-preset": "1.0.0",
     "@kbn/config-schema": "1.0.0",
     "@kbn/i18n": "1.0.0",
+    "@kbn/telemetry-tools": "1.0.0",
     "@kbn/interpreter": "1.0.0",
     "@kbn/pm": "1.0.0",
     "@kbn/test-subj-selector": "0.2.1",
diff --git a/packages/kbn-telemetry-tools/README.md b/packages/kbn-telemetry-tools/README.md
new file mode 100644
index 0000000000000..ccd092c76a17c
--- /dev/null
+++ b/packages/kbn-telemetry-tools/README.md
@@ -0,0 +1,89 @@
+# Telemetry Tools
+
+## Schema extraction tool
+
+### Description
+
+The tool is used to extract telemetry collectors schema from all `*.{ts}` files in provided plugins directories to JSON files. The tool looks for `.telemetryrc.json` files in the root of the project and in the `x-pack` dir for its runtime configurations.
+
+It uses typescript parser to build an AST for each file. The tool is able to validate, extract and match collector schemas.
+
+### Examples and restrictions
+
+**Global restrictions**:
+
+The `id` can be only a string literal, it cannot be a template literals w/o expressions or string-only concatenation expressions or anything else.
+
+```
+export const myCollector = makeUsageCollector<Usage>({
+  type: 'string_literal_only',
+  ...
+});
+```
+
+### Usage
+
+```bash
+node scripts/telemetry_extract.js
+```
+
+This command has no additional flags or arguments. The `.telemetryrc.json` files specify the path to the directory where searching should start, output json files, and files to exclude.
+
+
+### Output
+
+
+The generated JSON files contain an ES mapping for each schema. This mapping is used to verify changes in the collectors and as the basis to map those fields into the external telemetry cluster.
+
+**Example**:
+
+```json
+{
+  "properties": {
+    "cloud": {
+      "properties": {
+        "isCloudEnabled": {
+          "type": "boolean"
+        }
+      }
+    }
+  }
+}
+```
+
+## Schema validation tool
+
+### Description
+
+The tool performs a number of checks on all telemetry collectors and verifies the following:
+
+1. Verifies the collector structure, fields, and returned values are using the appropriate types.
+2. Verifies that the collector `fetch` function Type matches the specified `schema` in the collector.
+3. Verifies that the collector `schema` matches the stored json schema .
+
+### Notes
+
+We don't catch every possible misuse of the collectors, but only the most common and critical ones.
+
+What will not be caught by the validator:
+
+* Mistyped SavedObject/CallCluster return value. Since the hits returned from ES can be typed to anything without any checks. It is advised to add functional tests that grabs the schema json file and checks that the returned usage matches the types exactly. 
+
+* Fields in the schema that are never collected. If you are trying to report a field from ES but that value is never stored in ES, the check will not be able to detect if that field is ever collected in the first palce. It is advised to add unit/functional tests to check that all the fields are being reported as expected.
+
+The tool looks for `.telemetryrc.json` files in the root of the project and in the `x-pack` dir for its runtime configurations.
+
+Currently auto-fixer (`--fix`) can automatically fix the json files with the following errors:
+
+* incompatible schema - this error means that the collector schema was changed but the stored json schema file was not updated.
+
+* unused schemas - this error means that a collector was removed or its `type` renamed, the json schema file contains a schema that does not have a corrisponding collector.
+
+### Usage
+
+```bash
+node scripts/telemetry_check --fix
+```
+
+* `--path` specifies a collector path instead of checking all collectors specified in the `.telemetryrc.json` files. Accepts a `.ts` file. The file must be discoverable by at least one rc file.
+* `--fix` tells the tool to try to fix as many violations as possible. All errors that tool won't be able to fix will be reported.
diff --git a/packages/kbn-telemetry-tools/babel.config.js b/packages/kbn-telemetry-tools/babel.config.js
new file mode 100644
index 0000000000000..3b09c7d74ccb5
--- /dev/null
+++ b/packages/kbn-telemetry-tools/babel.config.js
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+module.exports = {
+  presets: ['@kbn/babel-preset/node_preset'],
+  ignore: ['**/*.test.ts', '**/__fixture__/**'],
+};
diff --git a/packages/kbn-telemetry-tools/package.json b/packages/kbn-telemetry-tools/package.json
new file mode 100644
index 0000000000000..5593a72ecd965
--- /dev/null
+++ b/packages/kbn-telemetry-tools/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "@kbn/telemetry-tools",
+  "version": "1.0.0",
+  "license": "Apache-2.0",
+  "main": "./target/index.js",
+  "private": true,
+  "scripts": {
+    "build": "babel src --out-dir target --delete-dir-on-start --extensions .ts --source-maps=inline",
+    "kbn:bootstrap": "yarn build",
+    "kbn:watch": "yarn build --watch"
+  },
+  "devDependencies": {
+    "lodash": "npm:@elastic/lodash@3.10.1-kibana4",
+    "@kbn/dev-utils": "1.0.0",
+    "@kbn/utility-types": "1.0.0",
+    "@types/normalize-path": "^3.0.0",
+    "normalize-path": "^3.0.0",
+    "@types/lodash": "^3.10.1",
+    "moment": "^2.24.0",
+    "typescript": "3.9.5"
+  }
+}
diff --git a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts
new file mode 100644
index 0000000000000..116c484a5c36a
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 Listr from 'listr';
+import chalk from 'chalk';
+import { createFailError, run } from '@kbn/dev-utils';
+
+import {
+  createTaskContext,
+  ErrorReporter,
+  parseConfigsTask,
+  extractCollectorsTask,
+  checkMatchingSchemasTask,
+  generateSchemasTask,
+  checkCompatibleTypesTask,
+  writeToFileTask,
+  TaskContext,
+} from '../tools/tasks';
+
+export function runTelemetryCheck() {
+  run(
+    async ({ flags: { fix = false, path }, log }) => {
+      if (typeof fix !== 'boolean') {
+        throw createFailError(`${chalk.white.bgRed(' TELEMETRY ERROR ')} --fix can't have a value`);
+      }
+
+      if (typeof path === 'boolean') {
+        throw createFailError(`${chalk.white.bgRed(' TELEMETRY ERROR ')} --path require a value`);
+      }
+
+      if (fix && typeof path !== 'undefined') {
+        throw createFailError(
+          `${chalk.white.bgRed(' TELEMETRY ERROR ')} --fix is incompatible with --path flag.`
+        );
+      }
+
+      const list = new Listr([
+        {
+          title: 'Checking .telemetryrc.json files',
+          task: () => new Listr(parseConfigsTask(), { exitOnError: true }),
+        },
+        {
+          title: 'Extracting Collectors',
+          task: (context) => new Listr(extractCollectorsTask(context, path), { exitOnError: true }),
+        },
+        {
+          title: 'Checking Compatible collector.schema with collector.fetch type',
+          task: (context) => new Listr(checkCompatibleTypesTask(context), { exitOnError: true }),
+        },
+        {
+          title: 'Checking Matching collector.schema against stored json files',
+          task: (context) => new Listr(checkMatchingSchemasTask(context), { exitOnError: true }),
+        },
+        {
+          enabled: (_) => fix,
+          skip: ({ roots }: TaskContext) => {
+            return roots.every(({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length);
+          },
+          title: 'Generating new telemetry mappings',
+          task: (context) => new Listr(generateSchemasTask(context), { exitOnError: true }),
+        },
+        {
+          enabled: (_) => fix,
+          skip: ({ roots }: TaskContext) => {
+            return roots.every(({ esMappingDiffs }) => !esMappingDiffs || !esMappingDiffs.length);
+          },
+          title: 'Updating telemetry mapping files',
+          task: (context) => new Listr(writeToFileTask(context), { exitOnError: true }),
+        },
+      ]);
+
+      try {
+        const context = createTaskContext();
+        await list.run(context);
+      } catch (error) {
+        process.exitCode = 1;
+        if (error instanceof ErrorReporter) {
+          error.errors.forEach((e: string | Error) => log.error(e));
+        } else {
+          log.error('Unhandled exception!');
+          log.error(error);
+        }
+      }
+      process.exit();
+    },
+    {
+      flags: {
+        allowUnexpected: true,
+        guessTypesForUnexpectedFlags: true,
+      },
+    }
+  );
+}
diff --git a/packages/kbn-telemetry-tools/src/cli/run_telemetry_extract.ts b/packages/kbn-telemetry-tools/src/cli/run_telemetry_extract.ts
new file mode 100644
index 0000000000000..27a406a4e216d
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/cli/run_telemetry_extract.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 Listr from 'listr';
+import { run } from '@kbn/dev-utils';
+
+import {
+  createTaskContext,
+  ErrorReporter,
+  parseConfigsTask,
+  extractCollectorsTask,
+  generateSchemasTask,
+  writeToFileTask,
+} from '../tools/tasks';
+
+export function runTelemetryExtract() {
+  run(
+    async ({ flags: {}, log }) => {
+      const list = new Listr([
+        {
+          title: 'Parsing .telemetryrc.json files',
+          task: () => new Listr(parseConfigsTask(), { exitOnError: true }),
+        },
+        {
+          title: 'Extracting Telemetry Collectors',
+          task: (context) => new Listr(extractCollectorsTask(context), { exitOnError: true }),
+        },
+        {
+          title: 'Generating Schema files',
+          task: (context) => new Listr(generateSchemasTask(context), { exitOnError: true }),
+        },
+        {
+          title: 'Writing to file',
+          task: (context) => new Listr(writeToFileTask(context), { exitOnError: true }),
+        },
+      ]);
+
+      try {
+        const context = createTaskContext();
+        await list.run(context);
+      } catch (error) {
+        process.exitCode = 1;
+        if (error instanceof ErrorReporter) {
+          error.errors.forEach((e: string | Error) => log.error(e));
+        } else {
+          log.error('Unhandled exception');
+          log.error(error);
+        }
+      }
+      process.exit();
+    },
+    {
+      flags: {
+        allowUnexpected: true,
+        guessTypesForUnexpectedFlags: true,
+      },
+    }
+  );
+}
diff --git a/packages/kbn-telemetry-tools/src/index.ts b/packages/kbn-telemetry-tools/src/index.ts
new file mode 100644
index 0000000000000..3a018a9b3002c
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/index.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 { runTelemetryCheck } from './cli/run_telemetry_check';
+export { runTelemetryExtract } from './cli/run_telemetry_extract';
diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json
new file mode 100644
index 0000000000000..885fe0e38dacf
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/mock_schema.json
@@ -0,0 +1,24 @@
+{
+  "properties": {
+    "my_working_collector": {
+      "properties": {
+        "flat": {
+          "type": "keyword"
+        },
+        "my_str": {
+          "type": "text"
+        },
+        "my_objects": {
+          "properties": {
+            "total": {
+              "type": "number"
+            },
+            "type": {
+              "type": "boolean"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_externally_defined_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_externally_defined_collector.ts
new file mode 100644
index 0000000000000..fe45f6b7f3042
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_externally_defined_collector.ts
@@ -0,0 +1,68 @@
+/*
+ * 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 { SyntaxKind } from 'typescript';
+import { ParsedUsageCollection } from '../ts_parser';
+
+export const parsedExternallyDefinedCollector: ParsedUsageCollection[] = [
+  [
+    'src/fixtures/telemetry_collectors/externally_defined_collector.ts',
+    {
+      collectorName: 'from_variable_collector',
+      schema: {
+        value: {
+          locale: {
+            type: 'keyword',
+          },
+        },
+      },
+      fetch: {
+        typeName: 'Usage',
+        typeDescriptor: {
+          locale: {
+            kind: SyntaxKind.StringKeyword,
+            type: 'StringKeyword',
+          },
+        },
+      },
+    },
+  ],
+  [
+    'src/fixtures/telemetry_collectors/externally_defined_collector.ts',
+    {
+      collectorName: 'from_fn_collector',
+      schema: {
+        value: {
+          locale: {
+            type: 'keyword',
+          },
+        },
+      },
+      fetch: {
+        typeName: 'Usage',
+        typeDescriptor: {
+          locale: {
+            kind: SyntaxKind.StringKeyword,
+            type: 'StringKeyword',
+          },
+        },
+      },
+    },
+  ],
+];
diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_schema.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_schema.ts
new file mode 100644
index 0000000000000..4870252082950
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_schema.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { SyntaxKind } from 'typescript';
+import { ParsedUsageCollection } from '../ts_parser';
+
+export const parsedImportedSchemaCollector: ParsedUsageCollection[] = [
+  [
+    'src/fixtures/telemetry_collectors/imported_schema.ts',
+    {
+      collectorName: 'with_imported_schema',
+      schema: {
+        value: {
+          locale: {
+            type: 'keyword',
+          },
+        },
+      },
+      fetch: {
+        typeName: 'Usage',
+        typeDescriptor: {
+          locale: {
+            kind: SyntaxKind.StringKeyword,
+            type: 'StringKeyword',
+          },
+        },
+      },
+    },
+  ],
+];
diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_usage_interface.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_usage_interface.ts
new file mode 100644
index 0000000000000..42ed2140b5208
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_imported_usage_interface.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { SyntaxKind } from 'typescript';
+import { ParsedUsageCollection } from '../ts_parser';
+
+export const parsedImportedUsageInterface: ParsedUsageCollection[] = [
+  [
+    'src/fixtures/telemetry_collectors/imported_usage_interface.ts',
+    {
+      collectorName: 'imported_usage_interface_collector',
+      schema: {
+        value: {
+          locale: {
+            type: 'keyword',
+          },
+        },
+      },
+      fetch: {
+        typeName: 'Usage',
+        typeDescriptor: {
+          locale: {
+            kind: SyntaxKind.StringKeyword,
+            type: 'StringKeyword',
+          },
+        },
+      },
+    },
+  ],
+];
diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_nested_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_nested_collector.ts
new file mode 100644
index 0000000000000..ed727c15b7c86
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_nested_collector.ts
@@ -0,0 +1,44 @@
+/*
+ * 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 { SyntaxKind } from 'typescript';
+import { ParsedUsageCollection } from '../ts_parser';
+
+export const parsedNestedCollector: ParsedUsageCollection = [
+  'src/fixtures/telemetry_collectors/nested_collector.ts',
+  {
+    collectorName: 'my_nested_collector',
+    schema: {
+      value: {
+        locale: {
+          type: 'keyword',
+        },
+      },
+    },
+    fetch: {
+      typeName: 'Usage',
+      typeDescriptor: {
+        locale: {
+          kind: SyntaxKind.StringKeyword,
+          type: 'StringKeyword',
+        },
+      },
+    },
+  },
+];
diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts
new file mode 100644
index 0000000000000..25e49ea221c94
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { SyntaxKind } from 'typescript';
+import { ParsedUsageCollection } from '../ts_parser';
+
+export const parsedWorkingCollector: ParsedUsageCollection = [
+  'src/fixtures/telemetry_collectors/working_collector.ts',
+  {
+    collectorName: 'my_working_collector',
+    schema: {
+      value: {
+        flat: {
+          type: 'keyword',
+        },
+        my_str: {
+          type: 'text',
+        },
+        my_objects: {
+          total: {
+            type: 'number',
+          },
+          type: {
+            type: 'boolean',
+          },
+        },
+      },
+    },
+    fetch: {
+      typeName: 'Usage',
+      typeDescriptor: {
+        flat: {
+          kind: SyntaxKind.StringKeyword,
+          type: 'StringKeyword',
+        },
+        my_str: {
+          kind: SyntaxKind.StringKeyword,
+          type: 'StringKeyword',
+        },
+        my_objects: {
+          total: {
+            kind: SyntaxKind.NumberKeyword,
+            type: 'NumberKeyword',
+          },
+          type: {
+            kind: SyntaxKind.BooleanKeyword,
+            type: 'BooleanKeyword',
+          },
+        },
+      },
+    },
+  },
+];
diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap
new file mode 100644
index 0000000000000..44a12dfa9030c
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap
@@ -0,0 +1,163 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`extractCollectors extracts collectors given rc file 1`] = `
+Array [
+  Array [
+    "src/fixtures/telemetry_collectors/externally_defined_collector.ts",
+    Object {
+      "collectorName": "from_variable_collector",
+      "fetch": Object {
+        "typeDescriptor": Object {
+          "locale": Object {
+            "kind": 143,
+            "type": "StringKeyword",
+          },
+        },
+        "typeName": "Usage",
+      },
+      "schema": Object {
+        "value": Object {
+          "locale": Object {
+            "type": "keyword",
+          },
+        },
+      },
+    },
+  ],
+  Array [
+    "src/fixtures/telemetry_collectors/externally_defined_collector.ts",
+    Object {
+      "collectorName": "from_fn_collector",
+      "fetch": Object {
+        "typeDescriptor": Object {
+          "locale": Object {
+            "kind": 143,
+            "type": "StringKeyword",
+          },
+        },
+        "typeName": "Usage",
+      },
+      "schema": Object {
+        "value": Object {
+          "locale": Object {
+            "type": "keyword",
+          },
+        },
+      },
+    },
+  ],
+  Array [
+    "src/fixtures/telemetry_collectors/imported_schema.ts",
+    Object {
+      "collectorName": "with_imported_schema",
+      "fetch": Object {
+        "typeDescriptor": Object {
+          "locale": Object {
+            "kind": 143,
+            "type": "StringKeyword",
+          },
+        },
+        "typeName": "Usage",
+      },
+      "schema": Object {
+        "value": Object {
+          "locale": Object {
+            "type": "keyword",
+          },
+        },
+      },
+    },
+  ],
+  Array [
+    "src/fixtures/telemetry_collectors/imported_usage_interface.ts",
+    Object {
+      "collectorName": "imported_usage_interface_collector",
+      "fetch": Object {
+        "typeDescriptor": Object {
+          "locale": Object {
+            "kind": 143,
+            "type": "StringKeyword",
+          },
+        },
+        "typeName": "Usage",
+      },
+      "schema": Object {
+        "value": Object {
+          "locale": Object {
+            "type": "keyword",
+          },
+        },
+      },
+    },
+  ],
+  Array [
+    "src/fixtures/telemetry_collectors/nested_collector.ts",
+    Object {
+      "collectorName": "my_nested_collector",
+      "fetch": Object {
+        "typeDescriptor": Object {
+          "locale": Object {
+            "kind": 143,
+            "type": "StringKeyword",
+          },
+        },
+        "typeName": "Usage",
+      },
+      "schema": Object {
+        "value": Object {
+          "locale": Object {
+            "type": "keyword",
+          },
+        },
+      },
+    },
+  ],
+  Array [
+    "src/fixtures/telemetry_collectors/working_collector.ts",
+    Object {
+      "collectorName": "my_working_collector",
+      "fetch": Object {
+        "typeDescriptor": Object {
+          "flat": Object {
+            "kind": 143,
+            "type": "StringKeyword",
+          },
+          "my_objects": Object {
+            "total": Object {
+              "kind": 140,
+              "type": "NumberKeyword",
+            },
+            "type": Object {
+              "kind": 128,
+              "type": "BooleanKeyword",
+            },
+          },
+          "my_str": Object {
+            "kind": 143,
+            "type": "StringKeyword",
+          },
+        },
+        "typeName": "Usage",
+      },
+      "schema": Object {
+        "value": Object {
+          "flat": Object {
+            "type": "keyword",
+          },
+          "my_objects": Object {
+            "total": Object {
+              "type": "number",
+            },
+            "type": Object {
+              "type": "boolean",
+            },
+          },
+          "my_str": Object {
+            "type": "text",
+          },
+        },
+      },
+    },
+  ],
+]
+`;
diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/ts_parser.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/ts_parser.test.ts.snap
new file mode 100644
index 0000000000000..5b1b3d9d35299
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/ts_parser.test.ts.snap
@@ -0,0 +1,6 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`parseUsageCollection throws when mapping fields is not defined 1`] = `
+"Error extracting collector in src/fixtures/telemetry_collectors/unmapped_collector.ts
+Error: usageCollector.schema must be defined."
+`;
diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts
new file mode 100644
index 0000000000000..6083593431d9b
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts
@@ -0,0 +1,125 @@
+/*
+ * 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 * as _ from 'lodash';
+import * as ts from 'typescript';
+import { parsedWorkingCollector } from './__fixture__/parsed_working_collector';
+import { checkCompatibleTypeDescriptor, checkMatchingMapping } from './check_collector_integrity';
+import * as path from 'path';
+import { readFile } from 'fs';
+import { promisify } from 'util';
+const read = promisify(readFile);
+
+async function parseJsonFile(relativePath: string) {
+  const schemaPath = path.resolve(__dirname, '__fixture__', relativePath);
+  const fileContent = await read(schemaPath, 'utf8');
+  return JSON.parse(fileContent);
+}
+
+describe('checkMatchingMapping', () => {
+  it('returns no diff on matching parsedCollections and stored mapping', async () => {
+    const mockSchema = await parseJsonFile('mock_schema.json');
+    const diffs = checkMatchingMapping([parsedWorkingCollector], mockSchema);
+    expect(diffs).toEqual({});
+  });
+
+  describe('Collector change', () => {
+    it('returns diff on mismatching parsedCollections and stored mapping', async () => {
+      const mockSchema = await parseJsonFile('mock_schema.json');
+      const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
+      const fieldMapping = { type: 'number' };
+      malformedParsedCollector[1].schema.value.flat = fieldMapping;
+
+      const diffs = checkMatchingMapping([malformedParsedCollector], mockSchema);
+      expect(diffs).toEqual({
+        properties: {
+          my_working_collector: {
+            properties: { flat: fieldMapping },
+          },
+        },
+      });
+    });
+
+    it('returns diff on unknown parsedCollections', async () => {
+      const mockSchema = await parseJsonFile('mock_schema.json');
+      const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
+      const collectorName = 'New Collector in town!';
+      const collectorMapping = { some_usage: { type: 'number' } };
+      malformedParsedCollector[1].collectorName = collectorName;
+      malformedParsedCollector[1].schema.value = { some_usage: { type: 'number' } };
+
+      const diffs = checkMatchingMapping([malformedParsedCollector], mockSchema);
+      expect(diffs).toEqual({
+        properties: {
+          [collectorName]: {
+            properties: collectorMapping,
+          },
+        },
+      });
+    });
+  });
+});
+
+describe('checkCompatibleTypeDescriptor', () => {
+  it('returns no diff on compatible type descriptor with mapping', () => {
+    const incompatibles = checkCompatibleTypeDescriptor([parsedWorkingCollector]);
+    expect(incompatibles).toHaveLength(0);
+  });
+
+  describe('Interface Change', () => {
+    it('returns diff on incompatible type descriptor with mapping', () => {
+      const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
+      malformedParsedCollector[1].fetch.typeDescriptor.flat.kind = ts.SyntaxKind.BooleanKeyword;
+      const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]);
+      expect(incompatibles).toHaveLength(1);
+      const { diff, message } = incompatibles[0];
+      expect(diff).toEqual({ 'flat.kind': 'boolean' });
+      expect(message).toHaveLength(1);
+      expect(message).toEqual([
+        'incompatible Type key (Usage.flat): expected ("string") got ("boolean").',
+      ]);
+    });
+
+    it.todo('returns diff when missing type descriptor');
+  });
+
+  describe('Mapping change', () => {
+    it('returns no diff when mapping change between text and keyword', () => {
+      const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
+      malformedParsedCollector[1].schema.value.flat.type = 'text';
+      const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]);
+      expect(incompatibles).toHaveLength(0);
+    });
+
+    it('returns diff on incompatible type descriptor with mapping', () => {
+      const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector);
+      malformedParsedCollector[1].schema.value.flat.type = 'boolean';
+      const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]);
+      expect(incompatibles).toHaveLength(1);
+      const { diff, message } = incompatibles[0];
+      expect(diff).toEqual({ 'flat.kind': 'string' });
+      expect(message).toHaveLength(1);
+      expect(message).toEqual([
+        'incompatible Type key (Usage.flat): expected ("boolean") got ("string").',
+      ]);
+    });
+
+    it.todo('returns diff when missing mapping');
+  });
+});
diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts b/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts
new file mode 100644
index 0000000000000..824132b05732c
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts
@@ -0,0 +1,103 @@
+/*
+ * 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 * as _ from 'lodash';
+import { difference, flattenKeys, pickDeep } from './utils';
+import { ParsedUsageCollection } from './ts_parser';
+import { generateMapping, compatibleSchemaTypes } from './manage_schema';
+import { kindToDescriptorName } from './serializer';
+
+export function checkMatchingMapping(
+  UsageCollections: ParsedUsageCollection[],
+  esMapping: any
+): any {
+  const generatedMapping = generateMapping(UsageCollections);
+  return difference(generatedMapping, esMapping);
+}
+
+interface IncompatibleDescriptor {
+  diff: Record<string, number>;
+  collectorPath: string;
+  message: string[];
+}
+export function checkCompatibleTypeDescriptor(
+  usageCollections: ParsedUsageCollection[]
+): IncompatibleDescriptor[] {
+  const results: Array<IncompatibleDescriptor | false> = usageCollections.map(
+    ([collectorPath, collectorDetails]) => {
+      const typeDescriptorTypes = flattenKeys(
+        pickDeep(collectorDetails.fetch.typeDescriptor, 'kind')
+      );
+      const typeDescriptorKinds = _.reduce(
+        typeDescriptorTypes,
+        (acc: any, type: number, key: string) => {
+          try {
+            acc[key] = kindToDescriptorName(type);
+          } catch (err) {
+            throw Error(`Unrecognized type (${key}: ${type}) in ${collectorPath}`);
+          }
+          return acc;
+        },
+        {} as any
+      );
+
+      const schemaTypes = flattenKeys(pickDeep(collectorDetails.schema.value, 'type'));
+      const transformedMappingKinds = _.reduce(
+        schemaTypes,
+        (acc: any, type: string, key: string) => {
+          try {
+            acc[key.replace(/.type$/, '.kind')] = compatibleSchemaTypes(type as any);
+          } catch (err) {
+            throw Error(`Unrecognized type (${key}: ${type}) in ${collectorPath}`);
+          }
+          return acc;
+        },
+        {} as any
+      );
+
+      const diff: any = difference(typeDescriptorKinds, transformedMappingKinds);
+      const diffEntries = Object.entries(diff);
+
+      if (!diffEntries.length) {
+        return false;
+      }
+
+      return {
+        diff,
+        collectorPath,
+        message: diffEntries.map(([key]) => {
+          const interfaceKey = key.replace('.kind', '');
+          try {
+            const expectedDescriptorType = JSON.stringify(transformedMappingKinds[key], null, 2);
+            const actualDescriptorType = JSON.stringify(typeDescriptorKinds[key], null, 2);
+            return `incompatible Type key (${collectorDetails.fetch.typeName}.${interfaceKey}): expected (${expectedDescriptorType}) got (${actualDescriptorType}).`;
+          } catch (err) {
+            throw Error(`Error converting ${key} in ${collectorPath}.\n${err}`);
+          }
+        }),
+      };
+    }
+  );
+
+  return results.filter((entry): entry is IncompatibleDescriptor => entry !== false);
+}
+
+export function checkCollectorIntegrity(UsageCollections: ParsedUsageCollection[], esMapping: any) {
+  return UsageCollections;
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/config.test.ts b/packages/kbn-telemetry-tools/src/tools/config.test.ts
new file mode 100644
index 0000000000000..51ca0493cbb5a
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/config.test.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 * as path from 'path';
+import { parseTelemetryRC } from './config';
+
+describe('parseTelemetryRC', () => {
+  it('throw if config path is not absolute', async () => {
+    const fixtureDir = './__fixture__/';
+    await expect(parseTelemetryRC(fixtureDir)).rejects.toThrowError();
+  });
+
+  it('returns parsed rc file', async () => {
+    const configRoot = path.join(process.cwd(), 'src', 'fixtures', 'telemetry_collectors');
+    const config = await parseTelemetryRC(configRoot);
+    expect(config).toStrictEqual([
+      {
+        root: configRoot,
+        output: configRoot,
+        exclude: [path.resolve(configRoot, './unmapped_collector.ts')],
+      },
+    ]);
+  });
+});
diff --git a/packages/kbn-telemetry-tools/src/tools/config.ts b/packages/kbn-telemetry-tools/src/tools/config.ts
new file mode 100644
index 0000000000000..5724b869e8f5e
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/config.ts
@@ -0,0 +1,60 @@
+/*
+ * 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 * as path from 'path';
+import { readFileAsync } from './utils';
+import { TELEMETRY_RC } from './constants';
+
+export interface TelemetryRC {
+  root: string;
+  output: string;
+  exclude: string[];
+}
+
+export async function readRcFile(rcRoot: string) {
+  if (!path.isAbsolute(rcRoot)) {
+    throw Error(`config root (${rcRoot}) must be an absolute path.`);
+  }
+
+  const rcFile = path.resolve(rcRoot, TELEMETRY_RC);
+  const configString = await readFileAsync(rcFile, 'utf8');
+  return JSON.parse(configString);
+}
+
+export async function parseTelemetryRC(rcRoot: string): Promise<TelemetryRC[]> {
+  const parsedRc = await readRcFile(rcRoot);
+  const configs = Array.isArray(parsedRc) ? parsedRc : [parsedRc];
+  return configs.map(({ root, output, exclude = [] }) => {
+    if (typeof root !== 'string') {
+      throw Error('config.root must be a string.');
+    }
+    if (typeof output !== 'string') {
+      throw Error('config.output must be a string.');
+    }
+    if (!Array.isArray(exclude)) {
+      throw Error('config.exclude must be an array of strings.');
+    }
+
+    return {
+      root: path.join(rcRoot, root),
+      output: path.join(rcRoot, output),
+      exclude: exclude.map((excludedPath) => path.resolve(rcRoot, excludedPath)),
+    };
+  });
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/constants.ts b/packages/kbn-telemetry-tools/src/tools/constants.ts
new file mode 100644
index 0000000000000..8635b1a2e2528
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/constants.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 const TELEMETRY_RC = '.telemetryrc.json';
diff --git a/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts
new file mode 100644
index 0000000000000..1b4ed21a1635c
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/extract_collectors.test.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 * as ts from 'typescript';
+import * as path from 'path';
+import { extractCollectors, getProgramPaths } from './extract_collectors';
+import { parseTelemetryRC } from './config';
+
+describe('extractCollectors', () => {
+  it('extracts collectors given rc file', async () => {
+    const configRoot = path.join(process.cwd(), 'src', 'fixtures', 'telemetry_collectors');
+    const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
+    if (!tsConfig) {
+      throw new Error('Could not find a valid tsconfig.json.');
+    }
+    const configs = await parseTelemetryRC(configRoot);
+    expect(configs).toHaveLength(1);
+    const programPaths = await getProgramPaths(configs[0]);
+
+    const results = [...extractCollectors(programPaths, tsConfig)];
+    expect(results).toHaveLength(6);
+    expect(results).toMatchSnapshot();
+  });
+});
diff --git a/packages/kbn-telemetry-tools/src/tools/extract_collectors.ts b/packages/kbn-telemetry-tools/src/tools/extract_collectors.ts
new file mode 100644
index 0000000000000..a638fde021458
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/extract_collectors.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 * as ts from 'typescript';
+import * as path from 'path';
+import { parseUsageCollection } from './ts_parser';
+import { globAsync } from './utils';
+import { TelemetryRC } from './config';
+
+export async function getProgramPaths({
+  root,
+  exclude,
+}: Pick<TelemetryRC, 'root' | 'exclude'>): Promise<string[]> {
+  const filePaths = await globAsync('**/*.ts', {
+    cwd: root,
+    ignore: [
+      '**/node_modules/**',
+      '**/*.test.*',
+      '**/*.mock.*',
+      '**/mocks.*',
+      '**/__fixture__/**',
+      '**/__tests__/**',
+      '**/public/**',
+      '**/dist/**',
+      '**/target/**',
+      '**/*.d.ts',
+    ],
+  });
+
+  if (filePaths.length === 0) {
+    throw Error(`No files found in ${root}`);
+  }
+
+  const fullPaths = filePaths
+    .map((filePath) => path.join(root, filePath))
+    .filter((fullPath) => !exclude.some((excludedPath) => fullPath.startsWith(excludedPath)));
+
+  if (fullPaths.length === 0) {
+    throw Error(`No paths covered from ${root} by the .telemetryrc.json`);
+  }
+
+  return fullPaths;
+}
+
+export function* extractCollectors(fullPaths: string[], tsConfig: any) {
+  const program = ts.createProgram(fullPaths, tsConfig);
+  program.getTypeChecker();
+  const sourceFiles = fullPaths.map((fullPath) => {
+    const sourceFile = program.getSourceFile(fullPath);
+    if (!sourceFile) {
+      throw Error(`Unable to get sourceFile ${fullPath}.`);
+    }
+    return sourceFile;
+  });
+
+  for (const sourceFile of sourceFiles) {
+    yield* parseUsageCollection(sourceFile, program);
+  }
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/manage_schema.test.ts b/packages/kbn-telemetry-tools/src/tools/manage_schema.test.ts
new file mode 100644
index 0000000000000..8f4bfc66b32ae
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/manage_schema.test.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { generateMapping } from './manage_schema';
+import { parsedWorkingCollector } from './__fixture__/parsed_working_collector';
+import * as path from 'path';
+import { readFile } from 'fs';
+import { promisify } from 'util';
+const read = promisify(readFile);
+
+async function parseJsonFile(relativePath: string) {
+  const schemaPath = path.resolve(__dirname, '__fixture__', relativePath);
+  const fileContent = await read(schemaPath, 'utf8');
+  return JSON.parse(fileContent);
+}
+
+describe('generateMapping', () => {
+  it('generates a mapping file', async () => {
+    const mockSchema = await parseJsonFile('mock_schema.json');
+    const result = generateMapping([parsedWorkingCollector]);
+    expect(result).toEqual(mockSchema);
+  });
+});
diff --git a/packages/kbn-telemetry-tools/src/tools/manage_schema.ts b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts
new file mode 100644
index 0000000000000..d422837140d80
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/manage_schema.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 { ParsedUsageCollection } from './ts_parser';
+
+export type AllowedSchemaTypes =
+  | 'keyword'
+  | 'text'
+  | 'number'
+  | 'boolean'
+  | 'long'
+  | 'date'
+  | 'float';
+
+export function compatibleSchemaTypes(type: AllowedSchemaTypes) {
+  switch (type) {
+    case 'keyword':
+    case 'text':
+    case 'date':
+      return 'string';
+    case 'boolean':
+      return 'boolean';
+    case 'number':
+    case 'float':
+    case 'long':
+      return 'number';
+    default:
+      throw new Error(`Unknown schema type ${type}`);
+  }
+}
+
+export function isObjectMapping(entity: any) {
+  if (typeof entity === 'object') {
+    // 'type' is explicitly specified to be an object.
+    if (typeof entity.type === 'string' && entity.type === 'object') {
+      return true;
+    }
+
+    // 'type' is not set; ES defaults to object mapping for when type is unspecified.
+    if (typeof entity.type === 'undefined') {
+      return true;
+    }
+
+    // 'type' is a field in the mapping and is not the type of the mapping.
+    if (typeof entity.type === 'object') {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+function transformToEsMapping(usageMappingValue: any) {
+  const fieldMapping: any = { properties: {} };
+  for (const [key, value] of Object.entries(usageMappingValue)) {
+    fieldMapping.properties[key] = isObjectMapping(value) ? transformToEsMapping(value) : value;
+  }
+  return fieldMapping;
+}
+
+export function generateMapping(usageCollections: ParsedUsageCollection[]) {
+  const esMapping: any = { properties: {} };
+  for (const [, collecionDetails] of usageCollections) {
+    esMapping.properties[collecionDetails.collectorName] = transformToEsMapping(
+      collecionDetails.schema.value
+    );
+  }
+
+  return esMapping;
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts
new file mode 100644
index 0000000000000..9475574a44219
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 * as ts from 'typescript';
+import * as path from 'path';
+import { getDescriptor, TelemetryKinds } from './serializer';
+import { traverseNodes } from './ts_parser';
+
+export function loadFixtureProgram(fixtureName: string) {
+  const fixturePath = path.resolve(
+    process.cwd(),
+    'src',
+    'fixtures',
+    'telemetry_collectors',
+    `${fixtureName}.ts`
+  );
+  const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
+  if (!tsConfig) {
+    throw new Error('Could not find a valid tsconfig.json.');
+  }
+  const program = ts.createProgram([fixturePath], tsConfig as any);
+  const checker = program.getTypeChecker();
+  const sourceFile = program.getSourceFile(fixturePath);
+  if (!sourceFile) {
+    throw Error('sourceFile is undefined!');
+  }
+  return { program, checker, sourceFile };
+}
+
+describe('getDescriptor', () => {
+  const usageInterfaces = new Map<string, ts.InterfaceDeclaration>();
+  let tsProgram: ts.Program;
+  beforeAll(() => {
+    const { program, sourceFile } = loadFixtureProgram('constants');
+    tsProgram = program;
+    for (const node of traverseNodes(sourceFile)) {
+      if (ts.isInterfaceDeclaration(node)) {
+        const interfaceName = node.name.getText();
+        usageInterfaces.set(interfaceName, node);
+      }
+    }
+  });
+
+  it('serializes flat types', () => {
+    const usageInterface = usageInterfaces.get('Usage');
+    const descriptor = getDescriptor(usageInterface!, tsProgram);
+    expect(descriptor).toEqual({
+      locale: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' },
+    });
+  });
+
+  it('serializes union types', () => {
+    const usageInterface = usageInterfaces.get('WithUnion');
+    const descriptor = getDescriptor(usageInterface!, tsProgram);
+
+    expect(descriptor).toEqual({
+      prop1: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' },
+      prop2: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' },
+      prop3: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' },
+      prop4: { kind: ts.SyntaxKind.StringLiteral, type: 'StringLiteral' },
+      prop5: { kind: ts.SyntaxKind.FirstLiteralToken, type: 'FirstLiteralToken' },
+    });
+  });
+
+  it('serializes Moment Dates', () => {
+    const usageInterface = usageInterfaces.get('WithMoment');
+    const descriptor = getDescriptor(usageInterface!, tsProgram);
+    expect(descriptor).toEqual({
+      prop1: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' },
+      prop2: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' },
+      prop3: { kind: TelemetryKinds.MomentDate, type: 'MomentDate' },
+      prop4: { kind: TelemetryKinds.Date, type: 'Date' },
+    });
+  });
+
+  it('throws error on conflicting union types', () => {
+    const usageInterface = usageInterfaces.get('WithConflictingUnion');
+    expect(() => getDescriptor(usageInterface!, tsProgram)).toThrowError(
+      'Mapping does not support conflicting union types.'
+    );
+  });
+
+  it('throws error on unsupported union types', () => {
+    const usageInterface = usageInterfaces.get('WithUnsupportedUnion');
+    expect(() => getDescriptor(usageInterface!, tsProgram)).toThrowError(
+      'Mapping does not support conflicting union types.'
+    );
+  });
+});
diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts
new file mode 100644
index 0000000000000..bce5dd7f58643
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts
@@ -0,0 +1,169 @@
+/*
+ * 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 * as ts from 'typescript';
+import { uniq } from 'lodash';
+import {
+  getResolvedModuleSourceFile,
+  getIdentifierDeclarationFromSource,
+  getModuleSpecifier,
+} from './utils';
+
+export enum TelemetryKinds {
+  MomentDate = 1000,
+  Date = 10001,
+}
+
+interface DescriptorValue {
+  kind: ts.SyntaxKind | TelemetryKinds;
+  type: keyof typeof ts.SyntaxKind | keyof typeof TelemetryKinds;
+}
+
+export interface Descriptor {
+  [name: string]: Descriptor | DescriptorValue;
+}
+
+export function isObjectDescriptor(value: any) {
+  if (typeof value === 'object') {
+    if (typeof value.type === 'string' && value.type === 'object') {
+      return true;
+    }
+
+    if (typeof value.type === 'undefined') {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+export function kindToDescriptorName(kind: number) {
+  switch (kind) {
+    case ts.SyntaxKind.StringKeyword:
+    case ts.SyntaxKind.StringLiteral:
+    case ts.SyntaxKind.SetKeyword:
+    case TelemetryKinds.Date:
+    case TelemetryKinds.MomentDate:
+      return 'string';
+    case ts.SyntaxKind.BooleanKeyword:
+      return 'boolean';
+    case ts.SyntaxKind.NumberKeyword:
+    case ts.SyntaxKind.NumericLiteral:
+      return 'number';
+    default:
+      throw new Error(`Unknown kind ${kind}`);
+  }
+}
+
+export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | DescriptorValue {
+  if (ts.isMethodSignature(node) || ts.isPropertySignature(node)) {
+    if (node.type) {
+      return getDescriptor(node.type, program);
+    }
+  }
+  if (ts.isTypeLiteralNode(node) || ts.isInterfaceDeclaration(node)) {
+    return node.members.reduce((acc, m) => {
+      acc[m.name?.getText() || ''] = getDescriptor(m, program);
+      return acc;
+    }, {} as any);
+  }
+
+  if (ts.SyntaxKind.FirstNode === node.kind) {
+    return getDescriptor((node as any).right, program);
+  }
+
+  if (ts.isIdentifier(node)) {
+    const identifierName = node.getText();
+    if (identifierName === 'Date') {
+      return { kind: TelemetryKinds.Date, type: 'Date' };
+    }
+    if (identifierName === 'Moment') {
+      return { kind: TelemetryKinds.MomentDate, type: 'MomentDate' };
+    }
+    throw new Error(`Unsupported Identifier ${identifierName}.`);
+  }
+
+  if (ts.isTypeReferenceNode(node)) {
+    const typeChecker = program.getTypeChecker();
+    const symbol = typeChecker.getSymbolAtLocation(node.typeName);
+    const symbolName = symbol?.getName();
+    if (symbolName === 'Moment') {
+      return { kind: TelemetryKinds.MomentDate, type: 'MomentDate' };
+    }
+    if (symbolName === 'Date') {
+      return { kind: TelemetryKinds.Date, type: 'Date' };
+    }
+    const declaration = (symbol?.getDeclarations() || [])[0];
+    if (declaration) {
+      return getDescriptor(declaration, program);
+    }
+    return getDescriptor(node.typeName, program);
+  }
+
+  if (ts.isImportSpecifier(node)) {
+    const source = node.getSourceFile();
+    const importedModuleName = getModuleSpecifier(node);
+
+    const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
+    const declarationNode = getIdentifierDeclarationFromSource(node.name, declarationSource);
+    return getDescriptor(declarationNode, program);
+  }
+
+  if (ts.isArrayTypeNode(node)) {
+    return getDescriptor(node.elementType, program);
+  }
+
+  if (ts.isLiteralTypeNode(node)) {
+    return {
+      kind: node.literal.kind,
+      type: ts.SyntaxKind[node.literal.kind] as keyof typeof ts.SyntaxKind,
+    };
+  }
+
+  if (ts.isUnionTypeNode(node)) {
+    const types = node.types.filter((typeNode) => {
+      return (
+        typeNode.kind !== ts.SyntaxKind.NullKeyword &&
+        typeNode.kind !== ts.SyntaxKind.UndefinedKeyword
+      );
+    });
+
+    const kinds = types.map((typeNode) => getDescriptor(typeNode, program));
+
+    const uniqueKinds = uniq(kinds, 'kind');
+
+    if (uniqueKinds.length !== 1) {
+      throw Error('Mapping does not support conflicting union types.');
+    }
+
+    return uniqueKinds[0];
+  }
+
+  switch (node.kind) {
+    case ts.SyntaxKind.NumberKeyword:
+    case ts.SyntaxKind.BooleanKeyword:
+    case ts.SyntaxKind.StringKeyword:
+    case ts.SyntaxKind.SetKeyword:
+      return { kind: node.kind, type: ts.SyntaxKind[node.kind] as keyof typeof ts.SyntaxKind };
+    case ts.SyntaxKind.UnionType:
+    case ts.SyntaxKind.AnyKeyword:
+    default:
+      throw new Error(`Unknown type ${ts.SyntaxKind[node.kind]}; ${node.getText()}`);
+  }
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/check_compatible_types_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/check_compatible_types_task.ts
new file mode 100644
index 0000000000000..dae4d0f1ad168
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/check_compatible_types_task.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { TaskContext } from './task_context';
+import { checkCompatibleTypeDescriptor } from '../check_collector_integrity';
+
+export function checkCompatibleTypesTask({ reporter, roots }: TaskContext) {
+  return roots.map((root) => ({
+    task: async () => {
+      if (root.parsedCollections) {
+        const differences = checkCompatibleTypeDescriptor(root.parsedCollections);
+        const reporterWithContext = reporter.withContext({ name: root.config.root });
+        if (differences.length) {
+          reporterWithContext.report(
+            `${JSON.stringify(
+              differences,
+              null,
+              2
+            )}. \nPlease fix the collectors and run the check again.`
+          );
+          throw reporter;
+        }
+      }
+    },
+    title: `Checking in ${root.config.root}`,
+  }));
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts
new file mode 100644
index 0000000000000..a1f23bcd44c76
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/check_matching_schemas_task.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 * as path from 'path';
+import { TaskContext } from './task_context';
+import { checkMatchingMapping } from '../check_collector_integrity';
+import { readFileAsync } from '../utils';
+
+export function checkMatchingSchemasTask({ roots }: TaskContext) {
+  return roots.map((root) => ({
+    task: async () => {
+      const fullPath = path.resolve(process.cwd(), root.config.output);
+      const esMappingString = await readFileAsync(fullPath, 'utf-8');
+      const esMapping = JSON.parse(esMappingString);
+
+      if (root.parsedCollections) {
+        const differences = checkMatchingMapping(root.parsedCollections, esMapping);
+
+        root.esMappingDiffs = Object.keys(differences);
+      }
+    },
+    title: `Checking in ${root.config.root}`,
+  }));
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/error_reporter.ts b/packages/kbn-telemetry-tools/src/tools/tasks/error_reporter.ts
new file mode 100644
index 0000000000000..246d659667281
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/error_reporter.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 chalk from 'chalk';
+import { normalizePath } from '../utils';
+
+export class ErrorReporter {
+  errors: string[] = [];
+
+  withContext(context: any) {
+    return { report: (error: any) => this.report(error, context) };
+  }
+  report(error: any, context: any) {
+    this.errors.push(
+      `${chalk.white.bgRed(' TELEMETRY ERROR ')} Error in ${normalizePath(context.name)}\n${error}`
+    );
+  }
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/extract_collectors_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/extract_collectors_task.ts
new file mode 100644
index 0000000000000..834ec71e22032
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/extract_collectors_task.ts
@@ -0,0 +1,58 @@
+/*
+ * 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 * as ts from 'typescript';
+import * as path from 'path';
+import { TaskContext } from './task_context';
+import { extractCollectors, getProgramPaths } from '../extract_collectors';
+
+export function extractCollectorsTask(
+  { roots }: TaskContext,
+  restrictProgramToPath?: string | string[]
+) {
+  return roots.map((root) => ({
+    task: async () => {
+      const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
+      if (!tsConfig) {
+        throw new Error('Could not find a valid tsconfig.json.');
+      }
+      const programPaths = await getProgramPaths(root.config);
+
+      if (typeof restrictProgramToPath !== 'undefined') {
+        const restrictProgramToPaths = Array.isArray(restrictProgramToPath)
+          ? restrictProgramToPath
+          : [restrictProgramToPath];
+
+        const fullRestrictedPaths = restrictProgramToPaths.map((collectorPath) =>
+          path.resolve(process.cwd(), collectorPath)
+        );
+        const restrictedProgramPaths = programPaths.filter((programPath) =>
+          fullRestrictedPaths.includes(programPath)
+        );
+        if (restrictedProgramPaths.length) {
+          root.parsedCollections = [...extractCollectors(restrictedProgramPaths, tsConfig)];
+        }
+        return;
+      }
+
+      root.parsedCollections = [...extractCollectors(programPaths, tsConfig)];
+    },
+    title: `Extracting collectors in ${root.config.root}`,
+  }));
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/generate_schemas_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/generate_schemas_task.ts
new file mode 100644
index 0000000000000..f6d15c7127d4e
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/generate_schemas_task.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 * as _ from 'lodash';
+import { TaskContext } from './task_context';
+import { generateMapping } from '../manage_schema';
+
+export function generateSchemasTask({ roots }: TaskContext) {
+  return roots.map((root) => ({
+    task: () => {
+      if (!root.parsedCollections || !root.parsedCollections.length) {
+        return;
+      }
+      const mapping = generateMapping(root.parsedCollections);
+      root.mapping = mapping;
+    },
+    title: `Generating mapping for ${root.config.root}`,
+  }));
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/index.ts b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
new file mode 100644
index 0000000000000..cbe74aeb483e4
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/index.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { ErrorReporter } from './error_reporter';
+export { TaskContext, createTaskContext } from './task_context';
+
+export { parseConfigsTask } from './parse_configs_task';
+export { extractCollectorsTask } from './extract_collectors_task';
+export { generateSchemasTask } from './generate_schemas_task';
+export { writeToFileTask } from './write_to_file_task';
+export { checkMatchingSchemasTask } from './check_matching_schemas_task';
+export { checkCompatibleTypesTask } from './check_compatible_types_task';
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/parse_configs_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/parse_configs_task.ts
new file mode 100644
index 0000000000000..00b319006e2ee
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/parse_configs_task.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 * as path from 'path';
+import { parseTelemetryRC } from '../config';
+import { TaskContext } from './task_context';
+
+export function parseConfigsTask() {
+  const kibanaRoot = process.cwd();
+  const xpackRoot = path.join(kibanaRoot, 'x-pack');
+
+  const configRoots = [kibanaRoot, xpackRoot];
+
+  return configRoots.map((configRoot) => ({
+    task: async (context: TaskContext) => {
+      try {
+        const configs = await parseTelemetryRC(configRoot);
+        configs.forEach((config) => {
+          context.roots.push({ config });
+        });
+      } catch (err) {
+        const { reporter } = context;
+        const reporterWithContext = reporter.withContext({ name: configRoot });
+        reporterWithContext.report(err);
+        throw reporter;
+      }
+    },
+    title: `Parsing configs in ${configRoot}`,
+  }));
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/task_context.ts b/packages/kbn-telemetry-tools/src/tools/tasks/task_context.ts
new file mode 100644
index 0000000000000..78d0b7fbd6c2d
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/task_context.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { TelemetryRC } from '../config';
+import { ErrorReporter } from './error_reporter';
+import { ParsedUsageCollection } from '../ts_parser';
+export interface TelemetryRoot {
+  config: TelemetryRC;
+  parsedCollections?: ParsedUsageCollection[];
+  mapping?: any;
+  esMappingDiffs?: string[];
+}
+
+export interface TaskContext {
+  reporter: ErrorReporter;
+  roots: TelemetryRoot[];
+}
+
+export function createTaskContext(): TaskContext {
+  const reporter = new ErrorReporter();
+  return {
+    roots: [],
+    reporter,
+  };
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/write_to_file_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/write_to_file_task.ts
new file mode 100644
index 0000000000000..fcfc09db65426
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/tasks/write_to_file_task.ts
@@ -0,0 +1,35 @@
+/*
+ * 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 * as path from 'path';
+import { writeFileAsync } from '../utils';
+import { TaskContext } from './task_context';
+
+export function writeToFileTask({ roots }: TaskContext) {
+  return roots.map((root) => ({
+    task: async () => {
+      const fullPath = path.resolve(process.cwd(), root.config.output);
+      if (root.mapping && Object.keys(root.mapping.properties).length > 0) {
+        const serializedMapping = JSON.stringify(root.mapping, null, 2).concat('\n');
+        await writeFileAsync(fullPath, serializedMapping);
+      }
+    },
+    title: `Writing mapping for ${root.config.root}`,
+  }));
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts b/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts
new file mode 100644
index 0000000000000..b7ca33a7bcd74
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts
@@ -0,0 +1,94 @@
+/*
+ * 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 { parseUsageCollection } from './ts_parser';
+import * as ts from 'typescript';
+import * as path from 'path';
+import { parsedWorkingCollector } from './__fixture__/parsed_working_collector';
+import { parsedNestedCollector } from './__fixture__/parsed_nested_collector';
+import { parsedExternallyDefinedCollector } from './__fixture__/parsed_externally_defined_collector';
+import { parsedImportedUsageInterface } from './__fixture__/parsed_imported_usage_interface';
+import { parsedImportedSchemaCollector } from './__fixture__/parsed_imported_schema';
+
+export function loadFixtureProgram(fixtureName: string) {
+  const fixturePath = path.resolve(
+    process.cwd(),
+    'src',
+    'fixtures',
+    'telemetry_collectors',
+    `${fixtureName}.ts`
+  );
+  const tsConfig = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
+  if (!tsConfig) {
+    throw new Error('Could not find a valid tsconfig.json.');
+  }
+  const program = ts.createProgram([fixturePath], tsConfig as any);
+  const checker = program.getTypeChecker();
+  const sourceFile = program.getSourceFile(fixturePath);
+  if (!sourceFile) {
+    throw Error('sourceFile is undefined!');
+  }
+  return { program, checker, sourceFile };
+}
+
+describe('parseUsageCollection', () => {
+  it.todo('throws when a function is returned from fetch');
+  it.todo('throws when an object is not returned from fetch');
+
+  it('throws when mapping fields is not defined', () => {
+    const { program, sourceFile } = loadFixtureProgram('unmapped_collector');
+    expect(() => [...parseUsageCollection(sourceFile, program)]).toThrowErrorMatchingSnapshot();
+  });
+
+  it('parses root level defined collector', () => {
+    const { program, sourceFile } = loadFixtureProgram('working_collector');
+    const result = [...parseUsageCollection(sourceFile, program)];
+    expect(result).toEqual([parsedWorkingCollector]);
+  });
+
+  it('parses nested collectors', () => {
+    const { program, sourceFile } = loadFixtureProgram('nested_collector');
+    const result = [...parseUsageCollection(sourceFile, program)];
+    expect(result).toEqual([parsedNestedCollector]);
+  });
+
+  it('parses imported schema property', () => {
+    const { program, sourceFile } = loadFixtureProgram('imported_schema');
+    const result = [...parseUsageCollection(sourceFile, program)];
+    expect(result).toEqual(parsedImportedSchemaCollector);
+  });
+
+  it('parses externally defined collectors', () => {
+    const { program, sourceFile } = loadFixtureProgram('externally_defined_collector');
+    const result = [...parseUsageCollection(sourceFile, program)];
+    expect(result).toEqual(parsedExternallyDefinedCollector);
+  });
+
+  it('parses imported Usage interface', () => {
+    const { program, sourceFile } = loadFixtureProgram('imported_usage_interface');
+    const result = [...parseUsageCollection(sourceFile, program)];
+    expect(result).toEqual(parsedImportedUsageInterface);
+  });
+
+  it('skips files that do not define a collector', () => {
+    const { program, sourceFile } = loadFixtureProgram('file_with_no_collector');
+    const result = [...parseUsageCollection(sourceFile, program)];
+    expect(result).toEqual([]);
+  });
+});
diff --git a/packages/kbn-telemetry-tools/src/tools/ts_parser.ts b/packages/kbn-telemetry-tools/src/tools/ts_parser.ts
new file mode 100644
index 0000000000000..6af8450f5a2e8
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/ts_parser.ts
@@ -0,0 +1,210 @@
+/*
+ * 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 * as ts from 'typescript';
+import { createFailError } from '@kbn/dev-utils';
+import * as path from 'path';
+import { getProperty, getPropertyValue } from './utils';
+import { getDescriptor, Descriptor } from './serializer';
+
+export function* traverseNodes(maybeNodes: ts.Node | ts.Node[]): Generator<ts.Node> {
+  const nodes: ts.Node[] = Array.isArray(maybeNodes) ? maybeNodes : [maybeNodes];
+
+  for (const node of nodes) {
+    const children: ts.Node[] = [];
+    yield node;
+    ts.forEachChild(node, (child) => {
+      children.push(child);
+    });
+    for (const child of children) {
+      yield* traverseNodes(child);
+    }
+  }
+}
+
+export function isMakeUsageCollectorFunction(
+  node: ts.Node,
+  sourceFile: ts.SourceFile
+): node is ts.CallExpression {
+  if (ts.isCallExpression(node)) {
+    const isMakeUsageCollector = /makeUsageCollector$/.test(node.expression.getText(sourceFile));
+    if (isMakeUsageCollector) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+export interface CollectorDetails {
+  collectorName: string;
+  fetch: { typeName: string; typeDescriptor: Descriptor };
+  schema: { value: any };
+}
+
+function getCollectionConfigNode(
+  collectorNode: ts.CallExpression,
+  sourceFile: ts.SourceFile
+): ts.Expression {
+  if (collectorNode.arguments.length > 1) {
+    throw Error(`makeUsageCollector does not accept more than one argument.`);
+  }
+  const collectorConfig = collectorNode.arguments[0];
+
+  if (ts.isObjectLiteralExpression(collectorConfig)) {
+    return collectorConfig;
+  }
+
+  const variableDefintionName = collectorConfig.getText();
+  for (const node of traverseNodes(sourceFile)) {
+    if (ts.isVariableDeclaration(node)) {
+      const declarationName = node.name.getText();
+      if (declarationName === variableDefintionName) {
+        if (!node.initializer) {
+          throw Error(`Unable to parse collector configs.`);
+        }
+        if (ts.isObjectLiteralExpression(node.initializer)) {
+          return node.initializer;
+        }
+        if (ts.isCallExpression(node.initializer)) {
+          const functionName = node.initializer.expression.getText(sourceFile);
+          for (const sfNode of traverseNodes(sourceFile)) {
+            if (ts.isFunctionDeclaration(sfNode)) {
+              const fnDeclarationName = sfNode.name?.getText();
+              if (fnDeclarationName === functionName) {
+                const returnStatements: ts.ReturnStatement[] = [];
+                for (const fnNode of traverseNodes(sfNode)) {
+                  if (ts.isReturnStatement(fnNode) && fnNode.parent === sfNode.body) {
+                    returnStatements.push(fnNode);
+                  }
+                }
+
+                if (returnStatements.length > 1) {
+                  throw Error(`Collector function cannot have multiple return statements.`);
+                }
+                if (returnStatements.length === 0) {
+                  throw Error(`Collector function must have a return statement.`);
+                }
+                if (!returnStatements[0].expression) {
+                  throw Error(`Collector function return statement must be an expression.`);
+                }
+
+                return returnStatements[0].expression;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  throw Error(`makeUsageCollector argument must be an object.`);
+}
+
+function extractCollectorDetails(
+  collectorNode: ts.CallExpression,
+  program: ts.Program,
+  sourceFile: ts.SourceFile
+): CollectorDetails {
+  if (collectorNode.arguments.length > 1) {
+    throw Error(`makeUsageCollector does not accept more than one argument.`);
+  }
+
+  const collectorConfig = getCollectionConfigNode(collectorNode, sourceFile);
+
+  const typeProperty = getProperty(collectorConfig, 'type');
+  if (!typeProperty) {
+    throw Error(`usageCollector.type must be defined.`);
+  }
+  const typePropertyValue = getPropertyValue(typeProperty, program);
+  if (!typePropertyValue || typeof typePropertyValue !== 'string') {
+    throw Error(`usageCollector.type must be be a non-empty string literal.`);
+  }
+
+  const fetchProperty = getProperty(collectorConfig, 'fetch');
+  if (!fetchProperty) {
+    throw Error(`usageCollector.fetch must be defined.`);
+  }
+  const schemaProperty = getProperty(collectorConfig, 'schema');
+  if (!schemaProperty) {
+    throw Error(`usageCollector.schema must be defined.`);
+  }
+
+  const schemaPropertyValue = getPropertyValue(schemaProperty, program, { chaseImport: true });
+  if (!schemaPropertyValue || typeof schemaPropertyValue !== 'object') {
+    throw Error(`usageCollector.schema must be be an object.`);
+  }
+
+  const collectorNodeType = collectorNode.typeArguments;
+  if (!collectorNodeType || collectorNodeType?.length === 0) {
+    throw Error(`makeUsageCollector requires a Usage type makeUsageCollector<Usage>({ ... }).`);
+  }
+
+  const usageTypeNode = collectorNodeType[0];
+  const usageTypeName = usageTypeNode.getText();
+  const usageType = getDescriptor(usageTypeNode, program) as Descriptor;
+
+  return {
+    collectorName: typePropertyValue,
+    schema: {
+      value: schemaPropertyValue,
+    },
+    fetch: {
+      typeName: usageTypeName,
+      typeDescriptor: usageType,
+    },
+  };
+}
+
+export function sourceHasUsageCollector(sourceFile: ts.SourceFile) {
+  if (sourceFile.isDeclarationFile === true || (sourceFile as any).identifierCount === 0) {
+    return false;
+  }
+
+  const identifiers = (sourceFile as any).identifiers;
+  if (
+    (!identifiers.get('makeUsageCollector') && !identifiers.get('type')) ||
+    !identifiers.get('fetch')
+  ) {
+    return false;
+  }
+
+  return true;
+}
+
+export type ParsedUsageCollection = [string, CollectorDetails];
+
+export function* parseUsageCollection(
+  sourceFile: ts.SourceFile,
+  program: ts.Program
+): Generator<ParsedUsageCollection> {
+  const relativePath = path.relative(process.cwd(), sourceFile.fileName);
+  if (sourceHasUsageCollector(sourceFile)) {
+    for (const node of traverseNodes(sourceFile)) {
+      if (isMakeUsageCollectorFunction(node, sourceFile)) {
+        try {
+          const collectorDetails = extractCollectorDetails(node, program, sourceFile);
+          yield [relativePath, collectorDetails];
+        } catch (err) {
+          throw createFailError(`Error extracting collector in ${relativePath}\n${err}`);
+        }
+      }
+    }
+  }
+}
diff --git a/packages/kbn-telemetry-tools/src/tools/utils.ts b/packages/kbn-telemetry-tools/src/tools/utils.ts
new file mode 100644
index 0000000000000..f5cf74ae35e45
--- /dev/null
+++ b/packages/kbn-telemetry-tools/src/tools/utils.ts
@@ -0,0 +1,238 @@
+/*
+ * 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 * as ts from 'typescript';
+import * as _ from 'lodash';
+import * as path from 'path';
+import glob from 'glob';
+import { readFile, writeFile } from 'fs';
+import { promisify } from 'util';
+import normalize from 'normalize-path';
+import { Optional } from '@kbn/utility-types';
+
+export const readFileAsync = promisify(readFile);
+export const writeFileAsync = promisify(writeFile);
+export const globAsync = promisify(glob);
+
+export function isPropertyWithKey(property: ts.Node, identifierName: string) {
+  if (ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property)) {
+    if (ts.isIdentifier(property.name)) {
+      return property.name.text === identifierName;
+    }
+  }
+
+  return false;
+}
+
+export function getProperty(objectNode: any, propertyName: string): ts.Node | null {
+  let foundProperty = null;
+  ts.visitNodes(objectNode?.properties || [], (node) => {
+    if (isPropertyWithKey(node, propertyName)) {
+      foundProperty = node;
+      return node;
+    }
+  });
+
+  return foundProperty;
+}
+
+export function getModuleSpecifier(node: ts.Node): string {
+  if ((node as any).moduleSpecifier) {
+    return (node as any).moduleSpecifier.text;
+  }
+  return getModuleSpecifier(node.parent);
+}
+
+export function getIdentifierDeclarationFromSource(node: ts.Node, source: ts.SourceFile) {
+  if (!ts.isIdentifier(node)) {
+    throw new Error(`node is not an identifier ${node.getText()}`);
+  }
+
+  const identifierName = node.getText();
+  const identifierDefinition: ts.Node = (source as any).locals.get(identifierName);
+  if (!identifierDefinition) {
+    throw new Error(`Unable to fine identifier in source ${identifierName}`);
+  }
+  const declarations = (identifierDefinition as any).declarations as ts.Node[];
+
+  const latestDeclaration: ts.Node | false | undefined =
+    Array.isArray(declarations) && declarations[declarations.length - 1];
+  if (!latestDeclaration) {
+    throw new Error(`Unable to fine declaration for identifier ${identifierName}`);
+  }
+
+  return latestDeclaration;
+}
+
+export function getIdentifierDeclaration(node: ts.Node) {
+  const source = node.getSourceFile();
+  if (!source) {
+    throw new Error('Unable to get source from node; check program configs.');
+  }
+
+  return getIdentifierDeclarationFromSource(node, source);
+}
+
+export function getVariableValue(node: ts.Node): string | Record<string, any> {
+  if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
+    return node.text;
+  }
+
+  if (ts.isObjectLiteralExpression(node)) {
+    return serializeObject(node);
+  }
+
+  throw Error(`Unsuppored Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`);
+}
+
+export function serializeObject(node: ts.Node) {
+  if (!ts.isObjectLiteralExpression(node)) {
+    throw new Error(`Expecting Object literal Expression got ${node.getText()}`);
+  }
+
+  const value: Record<string, any> = {};
+  for (const property of node.properties) {
+    const propertyName = property.name?.getText();
+    if (typeof propertyName === 'undefined') {
+      throw new Error(`Unable to get property name ${property.getText()}`);
+    }
+    if (ts.isPropertyAssignment(property)) {
+      value[propertyName] = getVariableValue(property.initializer);
+    } else {
+      value[propertyName] = getVariableValue(property);
+    }
+  }
+
+  return value;
+}
+
+export function getResolvedModuleSourceFile(
+  originalSource: ts.SourceFile,
+  program: ts.Program,
+  importedModuleName: string
+) {
+  const resolvedModule = (originalSource as any).resolvedModules.get(importedModuleName);
+  const resolvedModuleSourceFile = program.getSourceFile(resolvedModule.resolvedFileName);
+  if (!resolvedModuleSourceFile) {
+    throw new Error(`Unable to find resolved module ${importedModuleName}`);
+  }
+  return resolvedModuleSourceFile;
+}
+
+export function getPropertyValue(
+  node: ts.Node,
+  program: ts.Program,
+  config: Optional<{ chaseImport: boolean }> = {}
+) {
+  const { chaseImport = false } = config;
+
+  if (ts.isPropertyAssignment(node)) {
+    const { initializer } = node;
+
+    if (ts.isIdentifier(initializer)) {
+      const identifierName = initializer.getText();
+      const declaration = getIdentifierDeclaration(initializer);
+      if (ts.isImportSpecifier(declaration)) {
+        if (!chaseImport) {
+          throw new Error(
+            `Value of node ${identifierName} is imported from another file. Chasing imports is not allowed.`
+          );
+        }
+
+        const importedModuleName = getModuleSpecifier(declaration);
+
+        const source = node.getSourceFile();
+        const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
+        const declarationNode = getIdentifierDeclarationFromSource(initializer, declarationSource);
+        if (!ts.isVariableDeclaration(declarationNode)) {
+          throw new Error(`Expected ${identifierName} to be variable declaration.`);
+        }
+        if (!declarationNode.initializer) {
+          throw new Error(`Expected ${identifierName} to be initialized.`);
+        }
+        const serializedObject = serializeObject(declarationNode.initializer);
+        return serializedObject;
+      }
+
+      return getVariableValue(declaration);
+    }
+
+    return getVariableValue(initializer);
+  }
+}
+
+export function pickDeep(collection: any, identity: any, thisArg?: any) {
+  const picked: any = _.pick(collection, identity, thisArg);
+  const collections = _.pick(collection, _.isObject, thisArg);
+
+  _.each(collections, function (item, key) {
+    let object;
+    if (_.isArray(item)) {
+      object = _.reduce(
+        item,
+        function (result, value) {
+          const pickedDeep = pickDeep(value, identity, thisArg);
+          if (!_.isEmpty(pickedDeep)) {
+            result.push(pickedDeep);
+          }
+          return result;
+        },
+        [] as any[]
+      );
+    } else {
+      object = pickDeep(item, identity, thisArg);
+    }
+
+    if (!_.isEmpty(object)) {
+      picked[key || ''] = object;
+    }
+  });
+
+  return picked;
+}
+
+export const flattenKeys = (obj: any, keyPath: any[] = []): any => {
+  if (_.isObject(obj)) {
+    return _.reduce(
+      obj,
+      (cum, next, key) => {
+        const keys = [...keyPath, key];
+        return _.merge(cum, flattenKeys(next, keys));
+      },
+      {}
+    );
+  }
+  return { [keyPath.join('.')]: obj };
+};
+
+export function difference(actual: any, expected: any) {
+  function changes(obj: any, base: any) {
+    return _.transform(obj, function (result, value, key) {
+      if (key && !_.isEqual(value, base[key])) {
+        result[key] =
+          _.isObject(value) && _.isObject(base[key]) ? changes(value, base[key]) : value;
+      }
+    });
+  }
+  return changes(actual, expected);
+}
+
+export function normalizePath(inputPath: string) {
+  return normalize(path.relative('.', inputPath));
+}
diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json
new file mode 100644
index 0000000000000..13ce8ef2bad60
--- /dev/null
+++ b/packages/kbn-telemetry-tools/tsconfig.json
@@ -0,0 +1,6 @@
+{
+  "extends": "../../tsconfig.json",
+  "include": [
+    "src/**/*",
+  ]
+}
diff --git a/scripts/telemetry_check.js b/scripts/telemetry_check.js
new file mode 100644
index 0000000000000..06b3ed46bdba6
--- /dev/null
+++ b/scripts/telemetry_check.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+require('../src/setup_node_env/prebuilt_dev_only_entry');
+require('@kbn/telemetry-tools').runTelemetryCheck();
diff --git a/scripts/telemetry_extract.js b/scripts/telemetry_extract.js
new file mode 100644
index 0000000000000..051bee26537b9
--- /dev/null
+++ b/scripts/telemetry_extract.js
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+require('../src/setup_node_env/prebuilt_dev_only_entry');
+require('@kbn/telemetry-tools').runTelemetryExtract();
diff --git a/src/fixtures/telemetry_collectors/.telemetryrc.json b/src/fixtures/telemetry_collectors/.telemetryrc.json
new file mode 100644
index 0000000000000..31203149c9b57
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/.telemetryrc.json
@@ -0,0 +1,7 @@
+{
+  "root": ".",
+  "output": ".",
+  "exclude": [
+    "./unmapped_collector.ts"
+  ]
+}
diff --git a/src/fixtures/telemetry_collectors/constants.ts b/src/fixtures/telemetry_collectors/constants.ts
new file mode 100644
index 0000000000000..4aac9e66cdbdb
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/constants.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 moment, { Moment } from 'moment';
+import { MakeSchemaFrom } from '../../plugins/usage_collection/server';
+
+export interface Usage {
+  locale: string;
+}
+
+export interface WithUnion {
+  prop1: string | null;
+  prop2: string | null | undefined;
+  prop3?: string | null;
+  prop4: 'opt1' | 'opt2';
+  prop5: 123 | 431;
+}
+
+export interface WithMoment {
+  prop1: Moment;
+  prop2: moment.Moment;
+  prop3: Moment[];
+  prop4: Date[];
+}
+
+export interface WithConflictingUnion {
+  prop1: 123 | 'str';
+}
+
+export interface WithUnsupportedUnion {
+  prop1: 123 | Moment;
+}
+
+export const externallyDefinedSchema: MakeSchemaFrom<{ locale: string }> = {
+  locale: {
+    type: 'keyword',
+  },
+};
diff --git a/src/fixtures/telemetry_collectors/externally_defined_collector.ts b/src/fixtures/telemetry_collectors/externally_defined_collector.ts
new file mode 100644
index 0000000000000..00a8d643e27b3
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/externally_defined_collector.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 { CollectorSet, CollectorOptions } from '../../plugins/usage_collection/server/collector';
+import { loggerMock } from '../../core/server/logging/logger.mock';
+
+const collectorSet = new CollectorSet({
+  logger: loggerMock.create(),
+  maximumWaitTimeForAllCollectorsInS: 0,
+});
+
+interface Usage {
+  locale: string;
+}
+
+function createCollector(): CollectorOptions<Usage> {
+  return {
+    type: 'from_fn_collector',
+    isReady: () => true,
+    fetch(): Usage {
+      return {
+        locale: 'en',
+      };
+    },
+    schema: {
+      locale: {
+        type: 'keyword',
+      },
+    },
+  };
+}
+
+export function defineCollectorFromVariable() {
+  const fromVarCollector: CollectorOptions<Usage> = {
+    type: 'from_variable_collector',
+    isReady: () => true,
+    fetch(): Usage {
+      return {
+        locale: 'en',
+      };
+    },
+    schema: {
+      locale: {
+        type: 'keyword',
+      },
+    },
+  };
+
+  collectorSet.makeUsageCollector<Usage>(fromVarCollector);
+}
+
+export function defineCollectorFromFn() {
+  const fromFnCollector = createCollector();
+
+  collectorSet.makeUsageCollector<Usage>(fromFnCollector);
+}
diff --git a/src/fixtures/telemetry_collectors/file_with_no_collector.ts b/src/fixtures/telemetry_collectors/file_with_no_collector.ts
new file mode 100644
index 0000000000000..2e1870e486269
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/file_with_no_collector.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 const SOME_CONST: number = 123;
diff --git a/src/fixtures/telemetry_collectors/imported_schema.ts b/src/fixtures/telemetry_collectors/imported_schema.ts
new file mode 100644
index 0000000000000..66d04700642d1
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/imported_schema.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { CollectorSet } from '../../plugins/usage_collection/server/collector';
+import { loggerMock } from '../../core/server/logging/logger.mock';
+import { externallyDefinedSchema } from './constants';
+
+const { makeUsageCollector } = new CollectorSet({
+  logger: loggerMock.create(),
+  maximumWaitTimeForAllCollectorsInS: 0,
+});
+
+interface Usage {
+  locale?: string;
+}
+
+export const myCollector = makeUsageCollector<Usage>({
+  type: 'with_imported_schema',
+  isReady: () => true,
+  schema: externallyDefinedSchema,
+  fetch(): Usage {
+    return {
+      locale: 'en',
+    };
+  },
+});
diff --git a/src/fixtures/telemetry_collectors/imported_usage_interface.ts b/src/fixtures/telemetry_collectors/imported_usage_interface.ts
new file mode 100644
index 0000000000000..a4a0f4ae1b3c4
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/imported_usage_interface.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { CollectorSet } from '../../plugins/usage_collection/server/collector';
+import { loggerMock } from '../../core/server/logging/logger.mock';
+import { Usage } from './constants';
+
+const { makeUsageCollector } = new CollectorSet({
+  logger: loggerMock.create(),
+  maximumWaitTimeForAllCollectorsInS: 0,
+});
+
+export const myCollector = makeUsageCollector<Usage>({
+  type: 'imported_usage_interface_collector',
+  isReady: () => true,
+  fetch() {
+    return {
+      locale: 'en',
+    };
+  },
+  schema: {
+    locale: {
+      type: 'keyword',
+    },
+  },
+});
diff --git a/src/fixtures/telemetry_collectors/nested_collector.ts b/src/fixtures/telemetry_collectors/nested_collector.ts
new file mode 100644
index 0000000000000..bde89fe4a7060
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/nested_collector.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { CollectorSet, UsageCollector } from '../../plugins/usage_collection/server/collector';
+import { loggerMock } from '../../core/server/logging/logger.mock';
+
+const collectorSet = new CollectorSet({
+  logger: loggerMock.create(),
+  maximumWaitTimeForAllCollectorsInS: 0,
+});
+
+interface Usage {
+  locale?: string;
+}
+
+export class NestedInside {
+  collector?: UsageCollector<Usage, Usage>;
+  createMyCollector() {
+    this.collector = collectorSet.makeUsageCollector<Usage>({
+      type: 'my_nested_collector',
+      isReady: () => true,
+      fetch: async () => {
+        return {
+          locale: 'en',
+        };
+      },
+      schema: {
+        locale: {
+          type: 'keyword',
+        },
+      },
+    });
+  }
+}
diff --git a/src/fixtures/telemetry_collectors/unmapped_collector.ts b/src/fixtures/telemetry_collectors/unmapped_collector.ts
new file mode 100644
index 0000000000000..1ea360fcd9e96
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/unmapped_collector.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { CollectorSet } from '../../plugins/usage_collection/server/collector';
+import { loggerMock } from '../../core/server/logging/logger.mock';
+
+const { makeUsageCollector } = new CollectorSet({
+  logger: loggerMock.create(),
+  maximumWaitTimeForAllCollectorsInS: 0,
+});
+
+interface Usage {
+  locale: string;
+}
+
+export const myCollector = makeUsageCollector<Usage>({
+  type: 'unmapped_collector',
+  isReady: () => true,
+  fetch(): Usage {
+    return {
+      locale: 'en',
+    };
+  },
+});
diff --git a/src/fixtures/telemetry_collectors/working_collector.ts b/src/fixtures/telemetry_collectors/working_collector.ts
new file mode 100644
index 0000000000000..d70a247c61e70
--- /dev/null
+++ b/src/fixtures/telemetry_collectors/working_collector.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { CollectorSet } from '../../plugins/usage_collection/server/collector';
+import { loggerMock } from '../../core/server/logging/logger.mock';
+
+const { makeUsageCollector } = new CollectorSet({
+  logger: loggerMock.create(),
+  maximumWaitTimeForAllCollectorsInS: 0,
+});
+
+interface MyObject {
+  total: number;
+  type: boolean;
+}
+
+interface Usage {
+  flat?: string;
+  my_str?: string;
+  my_objects: MyObject;
+}
+
+const SOME_NUMBER: number = 123;
+
+export const myCollector = makeUsageCollector<Usage>({
+  type: 'my_working_collector',
+  isReady: () => true,
+  fetch() {
+    const testString = '123';
+    // query ES and get some data
+
+    // summarize the data into a model
+    // return the modeled object that includes whatever you want to track
+    try {
+      return {
+        flat: 'hello',
+        my_str: testString,
+        my_objects: {
+          total: SOME_NUMBER,
+          type: true,
+        },
+      };
+    } catch (err) {
+      return {
+        my_objects: {
+          total: 0,
+          type: true,
+        },
+      };
+    }
+  },
+  schema: {
+    flat: {
+      type: 'keyword',
+    },
+    my_str: {
+      type: 'text',
+    },
+    my_objects: {
+      total: {
+        type: 'number',
+      },
+      type: { type: 'boolean' },
+    },
+  },
+});
diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts
index 395cb60587832..63c2cbec21b57 100644
--- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts
+++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts
@@ -34,6 +34,7 @@ const createMockKbnServer = () => ({
 
 describe('csp collector', () => {
   let kbnServer: ReturnType<typeof createMockKbnServer>;
+  const mockCallCluster = null as any;
 
   function updateCsp(config: Partial<ICspConfig>) {
     kbnServer.newPlatform.setup.core.http.csp = new CspConfig(config);
@@ -46,28 +47,28 @@ describe('csp collector', () => {
   test('fetches whether strict mode is enabled', async () => {
     const collector = createCspCollector(kbnServer as any);
 
-    expect((await collector.fetch()).strict).toEqual(true);
+    expect((await collector.fetch(mockCallCluster)).strict).toEqual(true);
 
     updateCsp({ strict: false });
-    expect((await collector.fetch()).strict).toEqual(false);
+    expect((await collector.fetch(mockCallCluster)).strict).toEqual(false);
   });
 
   test('fetches whether the legacy browser warning is enabled', async () => {
     const collector = createCspCollector(kbnServer as any);
 
-    expect((await collector.fetch()).warnLegacyBrowsers).toEqual(true);
+    expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(true);
 
     updateCsp({ warnLegacyBrowsers: false });
-    expect((await collector.fetch()).warnLegacyBrowsers).toEqual(false);
+    expect((await collector.fetch(mockCallCluster)).warnLegacyBrowsers).toEqual(false);
   });
 
   test('fetches whether the csp rules have been changed or not', async () => {
     const collector = createCspCollector(kbnServer as any);
 
-    expect((await collector.fetch()).rulesChangedFromDefault).toEqual(false);
+    expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(false);
 
     updateCsp({ rules: ['not', 'default'] });
-    expect((await collector.fetch()).rulesChangedFromDefault).toEqual(true);
+    expect((await collector.fetch(mockCallCluster)).rulesChangedFromDefault).toEqual(true);
   });
 
   test('does not include raw csp rules under any property names', async () => {
@@ -79,7 +80,7 @@ describe('csp collector', () => {
     //
     // We use a snapshot here to ensure csp.rules isn't finding its way into the
     // payload under some new and unexpected variable name (e.g. cspRules).
-    expect(await collector.fetch()).toMatchInlineSnapshot(`
+    expect(await collector.fetch(mockCallCluster)).toMatchInlineSnapshot(`
       Object {
         "rulesChangedFromDefault": false,
         "strict": true,
diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts
index 6622ed4bef478..9c124a90e66eb 100644
--- a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts
+++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts
@@ -19,9 +19,18 @@
 
 import { Server } from 'hapi';
 import { CspConfig } from '../../../../../../core/server';
-import { UsageCollectionSetup } from '../../../../../../plugins/usage_collection/server';
+import {
+  UsageCollectionSetup,
+  CollectorOptions,
+} from '../../../../../../plugins/usage_collection/server';
 
-export function createCspCollector(server: Server) {
+interface Usage {
+  strict: boolean;
+  warnLegacyBrowsers: boolean;
+  rulesChangedFromDefault: boolean;
+}
+
+export function createCspCollector(server: Server): CollectorOptions<Usage> {
   return {
     type: 'csp',
     isReady: () => true,
@@ -37,10 +46,22 @@ export function createCspCollector(server: Server) {
         rulesChangedFromDefault: header !== CspConfig.DEFAULT.header,
       };
     },
+    schema: {
+      strict: {
+        type: 'boolean',
+      },
+      warnLegacyBrowsers: {
+        type: 'boolean',
+      },
+      rulesChangedFromDefault: {
+        type: 'boolean',
+      },
+    },
   };
 }
 
 export function registerCspCollector(usageCollection: UsageCollectionSetup, server: Server): void {
-  const collector = usageCollection.makeUsageCollector(createCspCollector(server));
+  const collectorConfig = createCspCollector(server);
+  const collector = usageCollection.makeUsageCollector<Usage>(collectorConfig);
   usageCollection.registerCollector(collector);
 }
diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts
index 157716b38f523..29f9be903a36f 100644
--- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts
+++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts
@@ -23,8 +23,14 @@ import { DEFAULT_QUERY_LANGUAGE, UI_SETTINGS } from '../../../common';
 
 const defaultSearchQueryLanguageSetting = DEFAULT_QUERY_LANGUAGE;
 
+export interface Usage {
+  optInCount: number;
+  optOutCount: number;
+  defaultQueryLanguage: string;
+}
+
 export function fetchProvider(index: string) {
-  return async (callCluster: APICaller) => {
+  return async (callCluster: APICaller): Promise<Usage> => {
     const [response, config] = await Promise.all([
       callCluster('get', {
         index,
@@ -38,7 +44,7 @@ export function fetchProvider(index: string) {
       }),
     ]);
 
-    const queryLanguageConfigValue = get(
+    const queryLanguageConfigValue: string | null | undefined = get(
       config,
       `hits.hits[0]._source.config.${UI_SETTINGS.SEARCH_QUERY_LANGUAGE}`
     );
diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/make_kql_usage_collector.ts b/src/plugins/data/server/kql_telemetry/usage_collector/make_kql_usage_collector.ts
index db4c9a8f0b4c7..6d0ca00122018 100644
--- a/src/plugins/data/server/kql_telemetry/usage_collector/make_kql_usage_collector.ts
+++ b/src/plugins/data/server/kql_telemetry/usage_collector/make_kql_usage_collector.ts
@@ -17,18 +17,22 @@
  * under the License.
  */
 
-import { fetchProvider } from './fetch';
+import { fetchProvider, Usage } from './fetch';
 import { UsageCollectionSetup } from '../../../../usage_collection/server';
 
 export async function makeKQLUsageCollector(
   usageCollection: UsageCollectionSetup,
   kibanaIndex: string
 ) {
-  const fetch = fetchProvider(kibanaIndex);
-  const kqlUsageCollector = usageCollection.makeUsageCollector({
+  const kqlUsageCollector = usageCollection.makeUsageCollector<Usage>({
     type: 'kql',
-    fetch,
+    fetch: fetchProvider(kibanaIndex),
     isReady: () => true,
+    schema: {
+      optInCount: { type: 'long' },
+      optOutCount: { type: 'long' },
+      defaultQueryLanguage: { type: 'keyword' },
+    },
   });
 
   usageCollection.registerCollector(kqlUsageCollector);
diff --git a/src/plugins/home/server/services/sample_data/usage/collector.ts b/src/plugins/home/server/services/sample_data/usage/collector.ts
index 19ceceb4cba14..d819d67a8d432 100644
--- a/src/plugins/home/server/services/sample_data/usage/collector.ts
+++ b/src/plugins/home/server/services/sample_data/usage/collector.ts
@@ -19,7 +19,7 @@
 
 import { PluginInitializerContext } from 'kibana/server';
 import { first } from 'rxjs/operators';
-import { fetchProvider } from './collector_fetch';
+import { fetchProvider, TelemetryResponse } from './collector_fetch';
 import { UsageCollectionSetup } from '../../../../../usage_collection/server';
 
 export async function makeSampleDataUsageCollector(
@@ -33,10 +33,18 @@ export async function makeSampleDataUsageCollector(
   } catch (err) {
     return; // kibana plugin is not enabled (test environment)
   }
-  const collector = usageCollection.makeUsageCollector({
+  const collector = usageCollection.makeUsageCollector<TelemetryResponse>({
     type: 'sample-data',
     fetch: fetchProvider(index),
     isReady: () => true,
+    schema: {
+      installed: { type: 'keyword' },
+      last_install_date: { type: 'date' },
+      last_install_set: { type: 'keyword' },
+      last_uninstall_date: { type: 'date' },
+      last_uninstall_set: { type: 'keyword' },
+      uninstalled: { type: 'keyword' },
+    },
   });
 
   usageCollection.registerCollector(collector);
diff --git a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts
index 4c7316c853018..d43458cfc64db 100644
--- a/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts
+++ b/src/plugins/home/server/services/sample_data/usage/collector_fetch.ts
@@ -31,7 +31,7 @@ interface SearchHit {
   };
 }
 
-interface TelemetryResponse {
+export interface TelemetryResponse {
   installed: string[];
   uninstalled: string[];
   last_install_date: moment.Moment | null;
diff --git a/src/plugins/kibana_usage_collection/common/constants.ts b/src/plugins/kibana_usage_collection/common/constants.ts
index df0adfc52184b..c4e7eaac51cf4 100644
--- a/src/plugins/kibana_usage_collection/common/constants.ts
+++ b/src/plugins/kibana_usage_collection/common/constants.ts
@@ -20,27 +20,6 @@
 export const PLUGIN_ID = 'kibanaUsageCollection';
 export const PLUGIN_NAME = 'kibana_usage_collection';
 
-/**
- * UI metric usage type
- */
-export const UI_METRIC_USAGE_TYPE = 'ui_metric';
-
-/**
- * Application Usage type
- */
-export const APPLICATION_USAGE_TYPE = 'application_usage';
-
-/**
- * The type name used within the Monitoring index to publish management stats.
- */
-export const KIBANA_STACK_MANAGEMENT_STATS_TYPE = 'stack_management';
-
-/**
- * The type name used to publish Kibana usage stats.
- * NOTE: this string shows as-is in the stats API as a field name for the kibana usage stats
- */
-export const KIBANA_USAGE_TYPE = 'kibana';
-
 /**
  * The type name used to publish Kibana usage stats in the formatted as bulk.
  */
diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts
index f52687038bbbc..1f22ab0100101 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.ts
@@ -20,7 +20,6 @@
 import moment from 'moment';
 import { ISavedObjectsRepository, SavedObjectsServiceSetup } from 'kibana/server';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { APPLICATION_USAGE_TYPE } from '../../../common/constants';
 import { findAll } from '../find_all';
 import {
   ApplicationUsageTotal,
@@ -62,7 +61,7 @@ export function registerApplicationUsageCollector(
   registerMappings(registerType);
 
   const collector = usageCollection.makeUsageCollector({
-    type: APPLICATION_USAGE_TYPE,
+    type: 'application_usage',
     isReady: () => typeof getSavedObjectsClient() !== 'undefined',
     fetch: async () => {
       const savedObjectsClient = getSavedObjectsClient();
diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts
index d0da6fcc523cc..9cc079a9325d5 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts
@@ -21,7 +21,7 @@ import { Observable } from 'rxjs';
 import { take } from 'rxjs/operators';
 import { SharedGlobalConfig } from 'kibana/server';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { KIBANA_STATS_TYPE, KIBANA_USAGE_TYPE } from '../../../common/constants';
+import { KIBANA_STATS_TYPE } from '../../../common/constants';
 import { getSavedObjectsCounts } from './get_saved_object_counts';
 
 export function getKibanaUsageCollector(
@@ -29,7 +29,7 @@ export function getKibanaUsageCollector(
   legacyConfig$: Observable<SharedGlobalConfig>
 ) {
   return usageCollection.makeUsageCollector({
-    type: KIBANA_USAGE_TYPE,
+    type: 'kibana',
     isReady: () => true,
     async fetch(callCluster) {
       const {
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts
index 39cd351884955..3a777beebd90a 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts
@@ -19,7 +19,6 @@
 
 import { IUiSettingsClient } from 'kibana/server';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { KIBANA_STACK_MANAGEMENT_STATS_TYPE } from '../../../common/constants';
 
 export type UsageStats = Record<string, any>;
 
@@ -47,7 +46,7 @@ export function registerManagementUsageCollector(
   getUiSettingsClient: () => IUiSettingsClient | undefined
 ) {
   const collector = usageCollection.makeUsageCollector({
-    type: KIBANA_STACK_MANAGEMENT_STATS_TYPE,
+    type: 'stack_management',
     isReady: () => typeof getUiSettingsClient() !== 'undefined',
     fetch: createCollectorFetch(getUiSettingsClient),
   });
diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts
index 603742f612a6b..ec2f1bfdfc25f 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts
@@ -23,7 +23,6 @@ import {
   SavedObjectsServiceSetup,
 } from 'kibana/server';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { UI_METRIC_USAGE_TYPE } from '../../../common/constants';
 import { findAll } from '../find_all';
 
 interface UIMetricsSavedObjects extends SavedObjectAttributes {
@@ -49,7 +48,7 @@ export function registerUiMetricUsageCollector(
   });
 
   const collector = usageCollection.makeUsageCollector({
-    type: UI_METRIC_USAGE_TYPE,
+    type: 'ui_metric',
     fetch: async () => {
       const savedObjectsClient = getSavedObjectsClient();
       if (typeof savedObjectsClient === 'undefined') {
diff --git a/src/plugins/telemetry/common/constants.ts b/src/plugins/telemetry/common/constants.ts
index 53c79b738f750..fc77332c18fc9 100644
--- a/src/plugins/telemetry/common/constants.ts
+++ b/src/plugins/telemetry/common/constants.ts
@@ -56,11 +56,6 @@ export const PATH_TO_ADVANCED_SETTINGS = 'management/kibana/settings';
  */
 export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`;
 
-/**
- * The type name used to publish telemetry plugin stats.
- */
-export const TELEMETRY_STATS_TYPE = 'telemetry';
-
 /**
  * The endpoint version when hitting the remote telemetry service
  */
diff --git a/src/plugins/telemetry/schema/legacy_oss_plugins.json b/src/plugins/telemetry/schema/legacy_oss_plugins.json
new file mode 100644
index 0000000000000..e660ccac9dc36
--- /dev/null
+++ b/src/plugins/telemetry/schema/legacy_oss_plugins.json
@@ -0,0 +1,17 @@
+{
+  "properties": {
+    "csp": {
+      "properties": {
+        "strict": {
+          "type": "boolean"
+        },
+        "warnLegacyBrowsers": {
+          "type": "boolean"
+        },
+        "rulesChangedFromDefault": {
+          "type": "boolean"
+        }
+      }
+    }
+  }
+}
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
new file mode 100644
index 0000000000000..a5172c01b1dad
--- /dev/null
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -0,0 +1,59 @@
+{
+  "properties": {
+    "kql": {
+      "properties": {
+        "optInCount": {
+          "type": "long"
+        },
+        "optOutCount": {
+          "type": "long"
+        },
+        "defaultQueryLanguage": {
+          "type": "keyword"
+        }
+      }
+    },
+    "sample-data": {
+      "properties": {
+        "installed": {
+          "type": "keyword"
+        },
+        "last_install_date": {
+          "type": "date"
+        },
+        "last_install_set": {
+          "type": "keyword"
+        },
+        "last_uninstall_date": {
+          "type": "date"
+        },
+        "last_uninstall_set": {
+          "type": "keyword"
+        },
+        "uninstalled": {
+          "type": "keyword"
+        }
+      }
+    },
+    "telemetry": {
+      "properties": {
+        "opt_in_status": {
+          "type": "boolean"
+        },
+        "usage_fetcher": {
+          "type": "keyword"
+        },
+        "last_reported": {
+          "type": "long"
+        }
+      }
+    },
+    "tsvb-validation": {
+      "properties": {
+        "failed_validations": {
+          "type": "long"
+        }
+      }
+    }
+  }
+}
diff --git a/src/plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts b/src/plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts
index ab90935266d69..05836b8448a68 100644
--- a/src/plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts
+++ b/src/plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts
@@ -20,7 +20,6 @@
 import { Observable } from 'rxjs';
 import { take } from 'rxjs/operators';
 import { ISavedObjectsRepository, SavedObjectsClient } from '../../../../../core/server';
-import { TELEMETRY_STATS_TYPE } from '../../../common/constants';
 import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository';
 import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../../common/telemetry_config';
 import { UsageCollectionSetup } from '../../../../usage_collection/server';
@@ -81,10 +80,15 @@ export function registerTelemetryPluginUsageCollector(
   usageCollection: UsageCollectionSetup,
   options: TelemetryPluginUsageCollectorOptions
 ) {
-  const collector = usageCollection.makeUsageCollector({
-    type: TELEMETRY_STATS_TYPE,
+  const collector = usageCollection.makeUsageCollector<TelemetryUsageStats>({
+    type: 'telemetry',
     isReady: () => typeof options.getSavedObjectsClient() !== 'undefined',
     fetch: createCollectorFetch(options),
+    schema: {
+      opt_in_status: { type: 'boolean' },
+      usage_fetcher: { type: 'keyword' },
+      last_reported: { type: 'long' },
+    },
   });
 
   usageCollection.registerCollector(collector);
diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md
index 99075d5d48f59..9520dfc03cfa4 100644
--- a/src/plugins/usage_collection/README.md
+++ b/src/plugins/usage_collection/README.md
@@ -8,7 +8,7 @@ To integrate with the telemetry services for usage collection of your feature, t
 
 ## Creating and Registering Usage Collector
 
-All you need to provide is a `type` for organizing your fields, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it.
+All you need to provide is a `type` for organizing your fields, `schema` field to define the expected types of usage fields reported, and a `fetch` method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it.
 
 ### New Platform
 
@@ -45,6 +45,12 @@ All you need to provide is a `type` for organizing your fields, and a `fetch` me
     import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
     import { APICluster } from 'kibana/server';
 
+    interface Usage {
+      my_objects: {
+        total: number,
+      },
+    }
+
     export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void {
       // usageCollection is an optional dependency, so make sure to return if it is not registered.
       if (!usageCollection) {
@@ -52,8 +58,13 @@ All you need to provide is a `type` for organizing your fields, and a `fetch` me
       }
 
       // create usage collector
-      const myCollector = usageCollection.makeUsageCollector({
+      const myCollector = usageCollection.makeUsageCollector<Usage>({
         type: MY_USAGE_TYPE,
+        schema: {
+          my_objects: {
+            total: 'long',
+          },
+        },
         fetch: async (callCluster: APICluster) => {
 
         // query ES and get some data
@@ -98,10 +109,8 @@ class Plugin {
 ```ts
 // server/collectors/register.ts
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { ISavedObjectsRepository } from 'kibana/server';
 
 export function registerMyPluginUsageCollector(
-  getSavedObjectsRepository: () => ISavedObjectsRepository | undefined,
   usageCollection?: UsageCollectionSetup
   ): void {
   // usageCollection is an optional dependency, so make sure to return if it is not registered.
@@ -110,22 +119,52 @@ export function registerMyPluginUsageCollector(
   }
 
   // create usage collector
-  const myCollector = usageCollection.makeUsageCollector({
-    type: MY_USAGE_TYPE,
-    isReady: () => typeof getSavedObjectsRepository() !== 'undefined',
-    fetch: async () => {
-      const savedObjectsRepository = getSavedObjectsRepository()!;
-      // get something from the savedObjects
-
-      return { my_objects };
-    },
-  });
+  const myCollector = usageCollection.makeUsageCollector<Usage>(...)
 
   // register usage collector
   usageCollection.registerCollector(myCollector);
 }
 ```
 
+## Schema Field
+
+The `schema` field is a proscribed data model assists with detecting changes in usage collector payloads. To define the collector schema add a schema field that specifies every possible field reported when registering the collector. Whenever the `schema` field is set or changed please run `node scripts/telemetry_check.js --fix` to update the stored schema json files.
+
+### Allowed Schema Types
+
+The `AllowedSchemaTypes` is the list of allowed schema types for the usage fields getting reported:
+
+```
+'keyword', 'text', 'number', 'boolean', 'long', 'date', 'float'
+```
+
+### Example
+
+```ts
+export const myCollector = makeUsageCollector<Usage>({
+  type: 'my_working_collector',
+  isReady: () => true,
+  fetch() {
+    return {
+      my_greeting: 'hello',
+      some_obj: {
+        total: 123,
+      },
+    };
+  },
+  schema: {
+    my_greeting: {
+      type: 'keyword',
+    },
+    some_obj: {
+      total: {
+        type: 'number',
+      },
+    },
+  },
+});
+```
+
 ## Update the telemetry payload and telemetry cluster field mappings
 
 There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster.
diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts
index b4f86f67e798d..00d55ef1c06db 100644
--- a/src/plugins/usage_collection/server/collector/collector.ts
+++ b/src/plugins/usage_collection/server/collector/collector.ts
@@ -21,9 +21,33 @@ import { Logger, APICaller } from 'kibana/server';
 
 export type CollectorFormatForBulkUpload<T, U> = (result: T) => { type: string; payload: U };
 
+export type AllowedSchemaTypes =
+  | 'keyword'
+  | 'text'
+  | 'number'
+  | 'boolean'
+  | 'long'
+  | 'date'
+  | 'float';
+
+export interface SchemaField {
+  type: string;
+}
+
+type Purify<T extends string> = { [P in T]: T }[T];
+
+export type MakeSchemaFrom<Base> = {
+  [Key in Purify<Extract<keyof Base, string>>]: Base[Key] extends Array<infer U>
+    ? { type: AllowedSchemaTypes }
+    : Base[Key] extends object
+    ? MakeSchemaFrom<Base[Key]>
+    : { type: AllowedSchemaTypes };
+};
+
 export interface CollectorOptions<T = unknown, U = T> {
   type: string;
   init?: Function;
+  schema?: MakeSchemaFrom<T>;
   fetch: (callCluster: APICaller) => Promise<T> | T;
   /*
    * A hook for allowing the fetched data payload to be organized into a typed
diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts
index e8791138c5e26..04ba7452f99e2 100644
--- a/src/plugins/usage_collection/server/collector/collector_set.ts
+++ b/src/plugins/usage_collection/server/collector/collector_set.ts
@@ -42,7 +42,7 @@ export class CollectorSet {
   public makeStatsCollector = <T, U>(options: CollectorOptions<T, U>) => {
     return new Collector(this.logger, options);
   };
-  public makeUsageCollector = <T, U>(options: CollectorOptions<T, U>) => {
+  public makeUsageCollector = <T, U = T>(options: CollectorOptions<T, U>) => {
     return new UsageCollector(this.logger, options);
   };
 
diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts
index 0d3939e1dc681..1816e845b4d66 100644
--- a/src/plugins/usage_collection/server/collector/index.ts
+++ b/src/plugins/usage_collection/server/collector/index.ts
@@ -18,5 +18,11 @@
  */
 
 export { CollectorSet } from './collector_set';
-export { Collector } from './collector';
+export {
+  Collector,
+  AllowedSchemaTypes,
+  SchemaField,
+  MakeSchemaFrom,
+  CollectorOptions,
+} from './collector';
 export { UsageCollector } from './usage_collector';
diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts
index a2769c8b4b405..87761bca9a507 100644
--- a/src/plugins/usage_collection/server/index.ts
+++ b/src/plugins/usage_collection/server/index.ts
@@ -20,6 +20,13 @@
 import { PluginInitializerContext } from 'kibana/server';
 import { UsageCollectionPlugin } from './plugin';
 
+export {
+  AllowedSchemaTypes,
+  MakeSchemaFrom,
+  SchemaField,
+  CollectorOptions,
+  Collector,
+} from './collector';
 export { UsageCollectionSetup } from './plugin';
 export { config } from './config';
 export const plugin = (initializerContext: PluginInitializerContext) =>
diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts
index 505816d48af52..22e427bed24c3 100644
--- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts
+++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts
@@ -24,6 +24,9 @@ import { tsvbTelemetrySavedObjectType } from '../saved_objects';
 export interface ValidationTelemetryServiceSetup {
   logFailedValidation: () => void;
 }
+export interface Usage {
+  failed_validations: number;
+}
 
 export class ValidationTelemetryService implements Plugin<ValidationTelemetryServiceSetup> {
   private kibanaIndex: string = '';
@@ -43,7 +46,7 @@ export class ValidationTelemetryService implements Plugin<ValidationTelemetrySer
     });
     if (usageCollection) {
       usageCollection.registerCollector(
-        usageCollection.makeUsageCollector({
+        usageCollection.makeUsageCollector<Usage>({
           type: 'tsvb-validation',
           isReady: () => this.kibanaIndex !== '',
           fetch: async (callCluster: APICaller) => {
@@ -63,6 +66,9 @@ export class ValidationTelemetryService implements Plugin<ValidationTelemetrySer
               };
             }
           },
+          schema: {
+            failed_validations: { type: 'long' },
+          },
         })
       );
     }
diff --git a/tasks/config/run.js b/tasks/config/run.js
index 22deead1d380e..32adf4f1f87c2 100644
--- a/tasks/config/run.js
+++ b/tasks/config/run.js
@@ -149,6 +149,12 @@ module.exports = function (grunt) {
       args: ['scripts/i18n_check', '--ignore-missing'],
     }),
 
+    telemetryCheck: scriptWithGithubChecks({
+      title: 'Telemetry Schema check',
+      cmd: NODE,
+      args: ['scripts/telemetry_check'],
+    }),
+
     // used by the test:quick task
     //    runs all node.js/server mocha tests
     mocha: scriptWithGithubChecks({
diff --git a/tasks/jenkins.js b/tasks/jenkins.js
index 4d92ff406a325..b40bb8156098d 100644
--- a/tasks/jenkins.js
+++ b/tasks/jenkins.js
@@ -27,6 +27,7 @@ module.exports = function (grunt) {
     'run:checkDocApiChanges',
     'run:typeCheck',
     'run:i18nCheck',
+    'run:telemetryCheck',
     'run:checkFileCasing',
     'run:checkLockfileSymlinks',
     'run:licenses',
diff --git a/x-pack/.telemetryrc.json b/x-pack/.telemetryrc.json
new file mode 100644
index 0000000000000..2c16491c1096b
--- /dev/null
+++ b/x-pack/.telemetryrc.json
@@ -0,0 +1,14 @@
+{
+  "output": "plugins/telemetry_collection_xpack/schema/xpack_plugins.json",
+  "root": "plugins/",
+  "exclude": [
+    "plugins/actions/server/usage/actions_usage_collector.ts",
+    "plugins/alerts/server/usage/alerts_usage_collector.ts",
+    "plugins/apm/server/lib/apm_telemetry/index.ts",
+    "plugins/canvas/server/collectors/collector.ts",
+    "plugins/infra/server/usage/usage_collector.ts",
+    "plugins/lens/server/usage/collectors.ts",
+    "plugins/reporting/server/usage/reporting_usage_collector.ts",
+    "plugins/maps/server/maps_telemetry/collectors/register.ts"
+  ]
+}
diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts
index e298b3ad9d00c..5dff062922200 100644
--- a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts
+++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts
@@ -13,10 +13,10 @@ export function createActionsUsageCollector(
   usageCollection: UsageCollectionSetup,
   taskManager: TaskManagerStartContract
 ) {
-  return usageCollection.makeUsageCollector({
+  return usageCollection.makeUsageCollector<ActionsUsage>({
     type: 'actions',
     isReady: () => true,
-    fetch: async (): Promise<ActionsUsage> => {
+    fetch: async () => {
       try {
         const doc = await getLatestTaskState(await taskManager);
         // get the accumulated state from the recurring task
diff --git a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts
index d2cef0f717e94..7491508ee0745 100644
--- a/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts
+++ b/x-pack/plugins/alerts/server/usage/alerts_usage_collector.ts
@@ -13,10 +13,10 @@ export function createAlertsUsageCollector(
   usageCollection: UsageCollectionSetup,
   taskManager: TaskManagerStartContract
 ) {
-  return usageCollection.makeUsageCollector({
+  return usageCollection.makeUsageCollector<AlertsUsage>({
     type: 'alerts',
     isReady: () => true,
-    fetch: async (): Promise<AlertsUsage> => {
+    fetch: async () => {
       try {
         const doc = await getLatestTaskState(await taskManager);
         // get the accumulated state from the recurring task
diff --git a/x-pack/plugins/canvas/common/lib/constants.ts b/x-pack/plugins/canvas/common/lib/constants.ts
index f2155d9202939..f42f4095c2697 100644
--- a/x-pack/plugins/canvas/common/lib/constants.ts
+++ b/x-pack/plugins/canvas/common/lib/constants.ts
@@ -20,7 +20,6 @@ export const LOCALSTORAGE_PREFIX = `kibana.canvas`;
 export const LOCALSTORAGE_CLIPBOARD = `${LOCALSTORAGE_PREFIX}.clipboard`;
 export const SESSIONSTORAGE_LASTPATH = 'lastPath:canvas';
 export const FETCH_TIMEOUT = 30000; // 30 seconds
-export const CANVAS_USAGE_TYPE = 'canvas';
 export const DEFAULT_WORKPAD_CSS = '.canvasPage {\n\n}';
 export const DEFAULT_ELEMENT_CSS = '.canvasRenderEl{\n\n}';
 export const VALID_IMAGE_TYPES = ['gif', 'jpeg', 'png', 'svg+xml'];
diff --git a/x-pack/plugins/canvas/server/collectors/collector.ts b/x-pack/plugins/canvas/server/collectors/collector.ts
index e266e9826a47d..48396d93d13e6 100644
--- a/x-pack/plugins/canvas/server/collectors/collector.ts
+++ b/x-pack/plugins/canvas/server/collectors/collector.ts
@@ -6,7 +6,6 @@
 
 import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { CANVAS_USAGE_TYPE } from '../../common/lib/constants';
 import { TelemetryCollector } from '../../types';
 
 import { workpadCollector } from './workpad_collector';
@@ -31,20 +30,16 @@ export function registerCanvasUsageCollector(
   }
 
   const canvasCollector = usageCollection.makeUsageCollector({
-    type: CANVAS_USAGE_TYPE,
+    type: 'canvas',
     isReady: () => true,
     fetch: async (callCluster: CallCluster) => {
       const collectorResults = await Promise.all(
         collectors.map((collector) => collector(kibanaIndex, callCluster))
       );
 
-      return collectorResults.reduce(
-        (reduction, usage) => {
-          return { ...reduction, ...usage };
-        },
-
-        {}
-      );
+      return collectorResults.reduce((reduction, usage) => {
+        return { ...reduction, ...usage };
+      }, {});
     },
   });
 
diff --git a/x-pack/plugins/cloud/common/constants.ts b/x-pack/plugins/cloud/common/constants.ts
index 4fafafb9e4213..b72f68247d02b 100644
--- a/x-pack/plugins/cloud/common/constants.ts
+++ b/x-pack/plugins/cloud/common/constants.ts
@@ -4,5 +4,4 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-export const KIBANA_CLOUD_STATS_TYPE = 'cloud';
 export const ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/';
diff --git a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts
index f3eb92eeddfbe..b0495f06e7ad4 100644
--- a/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts
+++ b/x-pack/plugins/cloud/server/collectors/cloud_usage_collector.ts
@@ -5,17 +5,23 @@
  */
 
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { KIBANA_CLOUD_STATS_TYPE } from '../../common/constants';
 
 interface Config {
   isCloudEnabled: boolean;
 }
 
+interface CloudUsage {
+  isCloudEnabled: boolean;
+}
+
 export function createCloudUsageCollector(usageCollection: UsageCollectionSetup, config: Config) {
   const { isCloudEnabled } = config;
-  return usageCollection.makeUsageCollector({
-    type: KIBANA_CLOUD_STATS_TYPE,
+  return usageCollection.makeUsageCollector<CloudUsage>({
+    type: 'cloud',
     isReady: () => true,
+    schema: {
+      isCloudEnabled: { type: 'boolean' },
+    },
     fetch: () => {
       return {
         isCloudEnabled,
diff --git a/x-pack/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts b/x-pack/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts
index 2c2b1183fd5bf..81b82c141e46f 100644
--- a/x-pack/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts
+++ b/x-pack/plugins/file_upload/server/telemetry/file_upload_usage_collector.ts
@@ -5,15 +5,23 @@
  */
 
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { getTelemetry, initTelemetry } from './telemetry';
-
-const TELEMETRY_TYPE = 'fileUploadTelemetry';
+import { getTelemetry, initTelemetry, Telemetry } from './telemetry';
 
 export function registerFileUploadUsageCollector(usageCollection: UsageCollectionSetup): void {
-  const fileUploadUsageCollector = usageCollection.makeUsageCollector({
-    type: TELEMETRY_TYPE,
+  const fileUploadUsageCollector = usageCollection.makeUsageCollector<Telemetry>({
+    type: 'fileUploadTelemetry',
     isReady: () => true,
-    fetch: async () => (await getTelemetry()) || initTelemetry(),
+    fetch: async () => {
+      const fileUploadUsage = await getTelemetry();
+      if (!fileUploadUsage) {
+        return initTelemetry();
+      }
+
+      return fileUploadUsage;
+    },
+    schema: {
+      filesUploadedTotalCount: { type: 'long' },
+    },
   });
 
   usageCollection.registerCollector(fileUploadUsageCollector);
diff --git a/x-pack/plugins/infra/server/usage/usage_collector.ts b/x-pack/plugins/infra/server/usage/usage_collector.ts
index 7be7364c331fa..598ee21e6f273 100644
--- a/x-pack/plugins/infra/server/usage/usage_collector.ts
+++ b/x-pack/plugins/infra/server/usage/usage_collector.ts
@@ -7,8 +7,6 @@
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
 import { InventoryItemType } from '../../common/inventory_models/types';
 
-const KIBANA_REPORTING_TYPE = 'infraops';
-
 interface InfraopsSum {
   infraopsHosts: number;
   infraopsDocker: number;
@@ -24,7 +22,7 @@ export class UsageCollector {
 
   public static getUsageCollector(usageCollection: UsageCollectionSetup) {
     return usageCollection.makeUsageCollector({
-      type: KIBANA_REPORTING_TYPE,
+      type: 'infraops',
       isReady: () => true,
       fetch: async () => {
         return this.getReport();
diff --git a/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts b/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts
index 21e5dce8e4706..35c6936598c40 100644
--- a/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts
+++ b/x-pack/plugins/ml/server/lib/telemetry/ml_usage_collector.ts
@@ -7,12 +7,10 @@
 import { CoreSetup } from 'kibana/server';
 
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { getTelemetry, initTelemetry } from './telemetry';
+import { getTelemetry, initTelemetry, Telemetry } from './telemetry';
 import { mlTelemetryMappingsType } from './mappings';
 import { setInternalRepository } from './internal_repository';
 
-const TELEMETRY_TYPE = 'mlTelemetry';
-
 export function initMlTelemetry(coreSetup: CoreSetup, usageCollection: UsageCollectionSetup) {
   coreSetup.savedObjects.registerType(mlTelemetryMappingsType);
   registerMlUsageCollector(usageCollection);
@@ -22,10 +20,22 @@ export function initMlTelemetry(coreSetup: CoreSetup, usageCollection: UsageColl
 }
 
 function registerMlUsageCollector(usageCollection: UsageCollectionSetup): void {
-  const mlUsageCollector = usageCollection.makeUsageCollector({
-    type: TELEMETRY_TYPE,
+  const mlUsageCollector = usageCollection.makeUsageCollector<Telemetry>({
+    type: 'mlTelemetry',
     isReady: () => true,
-    fetch: async () => (await getTelemetry()) || initTelemetry(),
+    schema: {
+      file_data_visualizer: {
+        index_creation_count: { type: 'long' },
+      },
+    },
+    fetch: async () => {
+      const mlUsage = await getTelemetry();
+      if (!mlUsage) {
+        return initTelemetry();
+      }
+
+      return mlUsage;
+    },
   });
 
   usageCollection.registerCollector(mlUsageCollector);
diff --git a/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts b/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts
index bc56e8b2a4372..f2162ff2c3d30 100644
--- a/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts
+++ b/x-pack/plugins/ml/server/lib/telemetry/telemetry.ts
@@ -11,7 +11,7 @@ import { getInternalRepository } from './internal_repository';
 
 export const TELEMETRY_DOC_ID = 'ml-telemetry';
 
-interface Telemetry {
+export interface Telemetry {
   file_data_visualizer: {
     index_creation_count: number;
   };
diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts
index 48483c79d1af2..c461c2de4e2ad 100644
--- a/x-pack/plugins/reporting/common/constants.ts
+++ b/x-pack/plugins/reporting/common/constants.ts
@@ -54,12 +54,6 @@ export const KBN_SCREENSHOT_HEADER_BLACKLIST_STARTS_WITH_PATTERN = ['proxy-'];
 
 export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo';
 
-/**
- * The type name used within the Monitoring index to publish reporting stats.
- * @type {string}
- */
-export const KIBANA_REPORTING_TYPE = 'reporting';
-
 export const PDF_JOB_TYPE = 'printable_pdf';
 export const PNG_JOB_TYPE = 'PNG';
 export const CSV_JOB_TYPE = 'csv';
diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts
index 364f5187f056c..100d09a2da7e4 100644
--- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts
+++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts
@@ -8,16 +8,22 @@ import { first, map } from 'rxjs/operators';
 import { CallCluster } from 'src/legacy/core_plugins/elasticsearch';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
 import { ReportingCore } from '../';
-import { KIBANA_REPORTING_TYPE } from '../../common/constants';
 import { ExportTypesRegistry } from '../lib/export_types_registry';
 import { ReportingSetupDeps } from '../types';
 import { GetLicense } from './';
 import { getReportingUsage } from './get_reporting_usage';
-import { RangeStats } from './types';
+import { ReportingUsageType } from './types';
 
 // places the reporting data as kibana stats
 const METATYPE = 'kibana_stats';
 
+interface XpackBulkUpload {
+  usage: {
+    xpack: {
+      reporting: ReportingUsageType;
+    };
+  };
+}
 /*
  * @return {Object} kibana usage stats type collection object
  */
@@ -28,20 +34,19 @@ export function getReportingUsageCollector(
   exportTypesRegistry: ExportTypesRegistry,
   isReady: () => Promise<boolean>
 ) {
-  return usageCollection.makeUsageCollector({
-    type: KIBANA_REPORTING_TYPE,
+  return usageCollection.makeUsageCollector<ReportingUsageType, XpackBulkUpload>({
+    type: 'reporting',
     fetch: (callCluster: CallCluster) => {
       const config = reporting.getConfig();
       return getReportingUsage(config, getLicense, callCluster, exportTypesRegistry);
     },
     isReady,
-
     /*
      * Format the response data into a model for internal upload
      * 1. Make this data part of the "kibana_stats" type
      * 2. Organize the payload in the usage.xpack.reporting namespace of the data payload
      */
-    formatForBulkUpload: (result: RangeStats) => {
+    formatForBulkUpload: (result: ReportingUsageType) => {
       return {
         type: METATYPE,
         payload: {
diff --git a/x-pack/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts
index 629dd8b180fdd..c679098bc05be 100644
--- a/x-pack/plugins/rollup/server/collectors/register.ts
+++ b/x-pack/plugins/rollup/server/collectors/register.ts
@@ -12,8 +12,6 @@ interface IdToFlagMap {
   [key: string]: boolean;
 }
 
-const ROLLUP_USAGE_TYPE = 'rollups';
-
 // elasticsearch index.max_result_window default value
 const ES_MAX_RESULT_WINDOW_DEFAULT_VALUE = 1000;
 
@@ -174,13 +172,42 @@ async function fetchRollupVisualizations(
   };
 }
 
+interface Usage {
+  index_patterns: {
+    total: number;
+  };
+  saved_searches: {
+    total: number;
+  };
+  visualizations: {
+    total: number;
+    saved_searches: {
+      total: number;
+    };
+  };
+}
+
 export function registerRollupUsageCollector(
   usageCollection: UsageCollectionSetup,
   kibanaIndex: string
 ): void {
-  const collector = usageCollection.makeUsageCollector({
-    type: ROLLUP_USAGE_TYPE,
+  const collector = usageCollection.makeUsageCollector<Usage>({
+    type: 'rollups',
     isReady: () => true,
+    schema: {
+      index_patterns: {
+        total: { type: 'long' },
+      },
+      saved_searches: {
+        total: { type: 'long' },
+      },
+      visualizations: {
+        saved_searches: {
+          total: { type: 'long' },
+        },
+        total: { type: 'long' },
+      },
+    },
     fetch: async (callCluster: CallCluster) => {
       const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster);
       const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns);
diff --git a/x-pack/plugins/spaces/common/constants.ts b/x-pack/plugins/spaces/common/constants.ts
index 11882ca2f1b3a..33f1aae70ea00 100644
--- a/x-pack/plugins/spaces/common/constants.ts
+++ b/x-pack/plugins/spaces/common/constants.ts
@@ -16,12 +16,6 @@ export const SPACE_SEARCH_COUNT_THRESHOLD = 8;
  */
 export const MAX_SPACE_INITIALS = 2;
 
-/**
- * The type name used within the Monitoring index to publish spaces stats.
- * @type {string}
- */
-export const KIBANA_SPACES_STATS_TYPE = 'spaces';
-
 /**
  * The path to enter a space.
  */
diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts
index fa1a81fe080f8..9f980df8da1b9 100644
--- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts
+++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts
@@ -9,7 +9,6 @@ import { take } from 'rxjs/operators';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
 import { Observable } from 'rxjs';
 import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants';
-import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants';
 import { PluginsSetup } from '../plugin';
 
 type CallCluster = <T = unknown>(
@@ -118,8 +117,25 @@ export interface UsageStats {
   enabled: boolean;
   count?: number;
   usesFeatureControls?: boolean;
-  disabledFeatures?: {
-    [featureId: string]: number;
+  disabledFeatures: {
+    indexPatterns?: number;
+    discover?: number;
+    canvas?: number;
+    maps?: number;
+    siem?: number;
+    monitoring?: number;
+    graph?: number;
+    uptime?: number;
+    savedObjectsManagement?: number;
+    timelion?: number;
+    dev_tools?: number;
+    advancedSettings?: number;
+    infrastructure?: number;
+    visualize?: number;
+    logs?: number;
+    dashboard?: number;
+    ml?: number;
+    apm?: number;
   };
 }
 
@@ -129,6 +145,11 @@ interface CollectorDeps {
   licensing: PluginsSetup['licensing'];
 }
 
+interface BulkUpload {
+  usage: {
+    spaces: UsageStats;
+  };
+}
 /*
  * @param {Object} server
  * @return {Object} kibana usage stats type collection object
@@ -137,9 +158,35 @@ export function getSpacesUsageCollector(
   usageCollection: UsageCollectionSetup,
   deps: CollectorDeps
 ) {
-  return usageCollection.makeUsageCollector({
-    type: KIBANA_SPACES_STATS_TYPE,
+  return usageCollection.makeUsageCollector<UsageStats, BulkUpload>({
+    type: 'spaces',
     isReady: () => true,
+    schema: {
+      usesFeatureControls: { type: 'boolean' },
+      disabledFeatures: {
+        indexPatterns: { type: 'long' },
+        discover: { type: 'long' },
+        canvas: { type: 'long' },
+        maps: { type: 'long' },
+        siem: { type: 'long' },
+        monitoring: { type: 'long' },
+        graph: { type: 'long' },
+        uptime: { type: 'long' },
+        savedObjectsManagement: { type: 'long' },
+        timelion: { type: 'long' },
+        dev_tools: { type: 'long' },
+        advancedSettings: { type: 'long' },
+        infrastructure: { type: 'long' },
+        visualize: { type: 'long' },
+        logs: { type: 'long' },
+        dashboard: { type: 'long' },
+        ml: { type: 'long' },
+        apm: { type: 'long' },
+      },
+      available: { type: 'boolean' },
+      enabled: { type: 'boolean' },
+      count: { type: 'long' },
+    },
     fetch: async (callCluster: CallCluster) => {
       const license = await deps.licensing.license$.pipe(take(1)).toPromise();
       const available = license.isAvailable; // some form of spaces is available for all valid licenses
diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
new file mode 100644
index 0000000000000..13d7c62316040
--- /dev/null
+++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json
@@ -0,0 +1,247 @@
+{
+  "properties": {
+    "cloud": {
+      "properties": {
+        "isCloudEnabled": {
+          "type": "boolean"
+        }
+      }
+    },
+    "fileUploadTelemetry": {
+      "properties": {
+        "filesUploadedTotalCount": {
+          "type": "long"
+        }
+      }
+    },
+    "mlTelemetry": {
+      "properties": {
+        "file_data_visualizer": {
+          "properties": {
+            "index_creation_count": {
+              "type": "long"
+            }
+          }
+        }
+      }
+    },
+    "rollups": {
+      "properties": {
+        "index_patterns": {
+          "properties": {
+            "total": {
+              "type": "long"
+            }
+          }
+        },
+        "saved_searches": {
+          "properties": {
+            "total": {
+              "type": "long"
+            }
+          }
+        },
+        "visualizations": {
+          "properties": {
+            "saved_searches": {
+              "properties": {
+                "total": {
+                  "type": "long"
+                }
+              }
+            },
+            "total": {
+              "type": "long"
+            }
+          }
+        }
+      }
+    },
+    "spaces": {
+      "properties": {
+        "usesFeatureControls": {
+          "type": "boolean"
+        },
+        "disabledFeatures": {
+          "properties": {
+            "indexPatterns": {
+              "type": "long"
+            },
+            "discover": {
+              "type": "long"
+            },
+            "canvas": {
+              "type": "long"
+            },
+            "maps": {
+              "type": "long"
+            },
+            "siem": {
+              "type": "long"
+            },
+            "monitoring": {
+              "type": "long"
+            },
+            "graph": {
+              "type": "long"
+            },
+            "uptime": {
+              "type": "long"
+            },
+            "savedObjectsManagement": {
+              "type": "long"
+            },
+            "timelion": {
+              "type": "long"
+            },
+            "dev_tools": {
+              "type": "long"
+            },
+            "advancedSettings": {
+              "type": "long"
+            },
+            "infrastructure": {
+              "type": "long"
+            },
+            "visualize": {
+              "type": "long"
+            },
+            "logs": {
+              "type": "long"
+            },
+            "dashboard": {
+              "type": "long"
+            },
+            "ml": {
+              "type": "long"
+            },
+            "apm": {
+              "type": "long"
+            }
+          }
+        },
+        "available": {
+          "type": "boolean"
+        },
+        "enabled": {
+          "type": "boolean"
+        },
+        "count": {
+          "type": "long"
+        }
+      }
+    },
+    "upgrade-assistant-telemetry": {
+      "properties": {
+        "features": {
+          "properties": {
+            "deprecation_logging": {
+              "properties": {
+                "enabled": {
+                  "type": "boolean"
+                }
+              }
+            }
+          }
+        },
+        "ui_open": {
+          "properties": {
+            "cluster": {
+              "type": "long"
+            },
+            "indices": {
+              "type": "long"
+            },
+            "overview": {
+              "type": "long"
+            }
+          }
+        },
+        "ui_reindex": {
+          "properties": {
+            "close": {
+              "type": "long"
+            },
+            "open": {
+              "type": "long"
+            },
+            "start": {
+              "type": "long"
+            },
+            "stop": {
+              "type": "long"
+            }
+          }
+        }
+      }
+    },
+    "uptime": {
+      "properties": {
+        "last_24_hours": {
+          "properties": {
+            "hits": {
+              "properties": {
+                "autoRefreshEnabled": {
+                  "type": "boolean"
+                },
+                "autorefreshInterval": {
+                  "type": "long"
+                },
+                "dateRangeEnd": {
+                  "type": "date"
+                },
+                "dateRangeStart": {
+                  "type": "date"
+                },
+                "monitor_frequency": {
+                  "type": "long"
+                },
+                "monitor_name_stats": {
+                  "properties": {
+                    "avg_length": {
+                      "type": "float"
+                    },
+                    "max_length": {
+                      "type": "long"
+                    },
+                    "min_length": {
+                      "type": "long"
+                    }
+                  }
+                },
+                "monitor_page": {
+                  "type": "long"
+                },
+                "no_of_unique_monitors": {
+                  "type": "long"
+                },
+                "no_of_unique_observer_locations": {
+                  "type": "long"
+                },
+                "observer_location_name_stats": {
+                  "properties": {
+                    "avg_length": {
+                      "type": "float"
+                    },
+                    "max_length": {
+                      "type": "long"
+                    },
+                    "min_length": {
+                      "type": "long"
+                    }
+                  }
+                },
+                "overview_page": {
+                  "type": "long"
+                },
+                "settings_page": {
+                  "type": "long"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts
index 0c2e3a1e43f4a..e511e27ee0e2c 100644
--- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts
+++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts
@@ -120,9 +120,29 @@ export function registerUpgradeAssistantUsageCollector({
   usageCollection,
   savedObjects,
 }: Dependencies) {
-  const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector({
-    type: UPGRADE_ASSISTANT_TYPE,
+  const upgradeAssistantUsageCollector = usageCollection.makeUsageCollector<
+    UpgradeAssistantTelemetry
+  >({
+    type: 'upgrade-assistant-telemetry',
     isReady: () => true,
+    schema: {
+      features: {
+        deprecation_logging: {
+          enabled: { type: 'boolean' },
+        },
+      },
+      ui_open: {
+        cluster: { type: 'long' },
+        indices: { type: 'long' },
+        overview: { type: 'long' },
+      },
+      ui_reindex: {
+        close: { type: 'long' },
+        open: { type: 'long' },
+        start: { type: 'long' },
+        stop: { type: 'long' },
+      },
+    },
     fetch: async () => fetchUpgradeAssistantMetrics(elasticsearch, savedObjects),
   });
 
diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
index 5d93a4d7f356d..44b95515039d8 100644
--- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
+++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts
@@ -7,7 +7,7 @@
 import moment from 'moment';
 import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server';
 import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
-import { PageViewParams, UptimeTelemetry } from './types';
+import { PageViewParams, UptimeTelemetry, Usage } from './types';
 import { APICaller } from '../framework';
 import { savedObjectsAdapter } from '../../saved_objects';
 
@@ -39,8 +39,36 @@ export class KibanaTelemetryAdapter {
     usageCollector: UsageCollectionSetup,
     getSavedObjectsClient: () => ISavedObjectsRepository | undefined
   ) {
-    return usageCollector.makeUsageCollector({
+    return usageCollector.makeUsageCollector<Usage>({
       type: 'uptime',
+      schema: {
+        last_24_hours: {
+          hits: {
+            autoRefreshEnabled: {
+              type: 'boolean',
+            },
+            autorefreshInterval: { type: 'long' },
+            dateRangeEnd: { type: 'date' },
+            dateRangeStart: { type: 'date' },
+            monitor_frequency: { type: 'long' },
+            monitor_name_stats: {
+              avg_length: { type: 'float' },
+              max_length: { type: 'long' },
+              min_length: { type: 'long' },
+            },
+            monitor_page: { type: 'long' },
+            no_of_unique_monitors: { type: 'long' },
+            no_of_unique_observer_locations: { type: 'long' },
+            observer_location_name_stats: {
+              avg_length: { type: 'float' },
+              max_length: { type: 'long' },
+              min_length: { type: 'long' },
+            },
+            overview_page: { type: 'long' },
+            settings_page: { type: 'long' },
+          },
+        },
+      },
       fetch: async (callCluster: APICaller) => {
         const savedObjectsClient = getSavedObjectsClient()!;
         if (savedObjectsClient) {
diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/types.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/types.ts
index ee3360ecc41b1..f2afeb2b7e50e 100644
--- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/types.ts
+++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/types.ts
@@ -19,6 +19,12 @@ export interface Stats {
   avg_length: number;
 }
 
+export interface Usage {
+  last_24_hours: {
+    hits: UptimeTelemetry;
+  };
+}
+
 export interface UptimeTelemetry {
   overview_page: number;
   monitor_page: number;

From 684289d6e3d27fe0c493f23812c790dca9478bf5 Mon Sep 17 00:00:00 2001
From: Candace Park <56409205+parkiino@users.noreply.github.com>
Date: Fri, 26 Jun 2020 20:25:01 -0400
Subject: [PATCH 19/21] [SECURITY SOLUTION][INGEST] UX update for ingest
 manager edit/create datasource for endpoint (#70079)

[security solution][ingest]UX update for ingest manager edit/create datasource for endpoint
---
 .../components/endpoint/link_to_app.tsx       | 25 +++++--
 .../configure_datasource.tsx                  | 68 ++++++++++++-------
 2 files changed, 63 insertions(+), 30 deletions(-)

diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx
index d6d8859b280b8..a12611ea27035 100644
--- a/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx
@@ -5,10 +5,10 @@
  */
 
 import React, { memo, MouseEventHandler } from 'react';
-import { EuiLink, EuiLinkProps } from '@elastic/eui';
+import { EuiLink, EuiLinkProps, EuiButton, EuiButtonProps } from '@elastic/eui';
 import { useNavigateToAppEventHandler } from '../../hooks/endpoint/use_navigate_to_app_event_handler';
 
-type LinkToAppProps = EuiLinkProps & {
+type LinkToAppProps = (EuiLinkProps | EuiButtonProps) & {
   /** the app id - normally the value of the `id` in that plugin's `kibana.json`  */
   appId: string;
   /** Any app specific path (route) */
@@ -16,6 +16,8 @@ type LinkToAppProps = EuiLinkProps & {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   appState?: any;
   onClick?: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
+  /** Uses an EuiButton element for styling */
+  asButton?: boolean;
 };
 
 /**
@@ -23,13 +25,22 @@ type LinkToAppProps = EuiLinkProps & {
  * a given app without causing a full browser refresh
  */
 export const LinkToApp = memo<LinkToAppProps>(
-  ({ appId, appPath: path, appState: state, onClick, children, ...otherProps }) => {
+  ({ appId, appPath: path, appState: state, onClick, asButton, children, ...otherProps }) => {
     const handleOnClick = useNavigateToAppEventHandler(appId, { path, state, onClick });
+
     return (
-      // eslint-disable-next-line @elastic/eui/href-or-on-click
-      <EuiLink {...otherProps} onClick={handleOnClick}>
-        {children}
-      </EuiLink>
+      <>
+        {asButton && asButton === true ? (
+          <EuiButton {...(otherProps as EuiButtonProps)} onClick={handleOnClick}>
+            {children}
+          </EuiButton>
+        ) : (
+          // eslint-disable-next-line @elastic/eui/href-or-on-click
+          <EuiLink {...otherProps} onClick={handleOnClick}>
+            {children}
+          </EuiLink>
+        )}
+      </>
     );
   }
 );
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx
index 7b4dc36def133..df1591bf78778 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx
@@ -6,8 +6,8 @@
 
 import React, { memo } from 'react';
 import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
-import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
+import { EuiCallOut, EuiText, EuiTitle, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
 import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app';
 import {
   CustomConfigureDatasourceContent,
@@ -21,43 +21,65 @@ import { getPolicyDetailPath } from '../../../../common/routing';
  */
 export const ConfigureEndpointDatasource = memo<CustomConfigureDatasourceContent>(
   ({ from, datasourceId }: CustomConfigureDatasourceProps) => {
-    const { services } = useKibana();
     let policyUrl = '';
     if (from === 'edit' && datasourceId) {
       policyUrl = getPolicyDetailPath(datasourceId);
     }
 
     return (
-      <EuiEmptyPrompt
-        data-test-subj={`endpointDatasourceConfig_${from === 'edit' ? 'edit' : 'create'}`}
-        body={
-          <EuiText>
+      <>
+        <EuiTitle size="xs">
+          <h4>
+            <FormattedMessage
+              id="xpack.securitySolution.endpoint.ingestManager.policyConfiguration"
+              defaultMessage="Policy Configuration"
+            />
+          </h4>
+        </EuiTitle>
+        <EuiSpacer size="m" />
+        <EuiCallOut
+          data-test-subj={`endpointDatasourceConfig_${from === 'edit' ? 'edit' : 'create'}`}
+          iconType="iInCircle"
+          title={i18n.translate(
+            'xpack.securitySolution.endpoint.ingestManager.policyConfiguration.calloutTitle',
+            {
+              defaultMessage: 'Manage Policy configuration in the Security app',
+            }
+          )}
+        >
+          <EuiText size="s">
             <p>
               {from === 'edit' ? (
-                <LinkToApp
-                  data-test-subj="editLinkToPolicyDetails"
-                  appId="securitySolution:management"
-                  appPath={policyUrl}
-                  // Cannot use formalUrl here since the code is called in Ingest, which does not use redux
-                  href={`${services.application.getUrlForApp(
-                    'securitySolution:management'
-                  )}${policyUrl}`}
-                >
+                <>
                   <FormattedMessage
-                    id="xpack.securitySolution.endpoint.ingestManager.editDatasource.stepConfigure"
-                    defaultMessage="View and configure Security Policy"
+                    id="xpack.securitySolution.endpoint.ingestManager.editDatasource.endpointConfiguration"
+                    defaultMessage="You can make changes to the Policy Configuration in the Security app. Fleet will deploy changes to your agents whenever your Policy changes."
                   />
-                </LinkToApp>
+                  <EuiSpacer />
+                  <LinkToApp
+                    data-test-subj="editLinkToPolicyDetails"
+                    asButton={true}
+                    appId="securitySolution:management"
+                    className="editLinkToPolicyDetails"
+                    appPath={policyUrl}
+                    // Cannot use formalUrl here since the code is called in Ingest, which does not use redux
+                  >
+                    <FormattedMessage
+                      id="xpack.securitySolution.endpoint.ingestManager.editDatasource.configurePolicyLink"
+                      defaultMessage="Configure Policy"
+                    />
+                  </LinkToApp>
+                </>
               ) : (
                 <FormattedMessage
-                  id="xpack.securitySolution.endpoint.ingestManager.createDatasource.stepConfigure"
-                  defaultMessage="The recommended Security Policy has been associated with this data source. The Security Policy can be edited in the Security application once your data source has been saved."
+                  id="xpack.securitySolution.endpoint.ingestManager.createDatasource.endpointConfiguration"
+                  defaultMessage="Any agents that use this agent configuration will use a basic policy. You can make changes to this policy in the Security app, and Fleet will deploy those changes to your agents."
                 />
               )}
             </p>
           </EuiText>
-        }
-      />
+        </EuiCallOut>
+      </>
     );
   }
 );

From f4e7f14ffeb78d3e5cc266d542087bb60a0a5ecb Mon Sep 17 00:00:00 2001
From: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Date: Sat, 27 Jun 2020 04:53:53 +0100
Subject: [PATCH 20/21] [SIEM] Import timeline fix (#65448)

* fix import timeline and clean up

fix unit tests

apply failure checker

clean up error message

fix update template

* add unit tests

* clean up common libs

* rename variables

* add unit tests

* fix types

* Fix imports

* rename file

* poc

* fix unit test

* review

* cleanup fallback values

* cleanup

* check if title exists

* fix unit test

* add unit test

* lint error

* put the flag for disableTemplate into common

* add immutiable

* fix unit

* check templateTimelineVersion only when update via import

* update template timeline via import with response

* add template filter

* add filter count

* add filter numbers

* rename

* enable pin events and note under active status

* disable comment and pinnedEvents for template timelines

* add timelineType for openTimeline

* enable note icon for template

* add timeline type for propertyLeft

* fix types

* duplicate elastic template

* update schema

* fix status check

* fix import

* add templateTimelineType

* disable note for immutable timeline

* fix unit

* fix error message

* fix update

* fix types

* rollback change

* rollback change

* fix create template timeline

* add i18n for error message

* fix unit test

* fix wording and disable delete btn for immutable timeline

* fix unit test provider

* fix types

* fix toaster

* fix notes and pins

* add i18n

* fix selected items

* set disableTemplateto true

* move templateInfo to helper

* review + imporvement

* fix review

* fix types

* fix types

Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
---
 .../security_solution/common/constants.ts     |   8 +-
 .../common/types/timeline/index.ts            |  32 +
 .../components/alerts_table/actions.test.tsx  |   4 +-
 .../index.test.tsx                            |  17 +-
 .../error_toast_dispatcher/index.test.tsx     |  17 +-
 .../common/components/inspect/index.test.tsx  |  25 +-
 .../components/stat_items/index.test.tsx      |   9 +-
 .../super_date_picker/index.test.tsx          |  17 +-
 .../common/components/top_n/index.test.tsx    |   9 +-
 .../common/lib/compose/kibana_compose.tsx     |   2 +-
 .../mock/endpoint/app_context_render.tsx      |  25 +-
 .../public/common/mock/kibana_react.ts        |  33 +
 .../public/common/mock/test_providers.tsx     |  19 +-
 .../public/common/store/store.ts              |   3 +
 .../public/common/store/types.ts              |  21 +-
 .../view/test_helpers/render_alert_page.tsx   |   9 +-
 .../public/graphql/introspection.json         |  78 +-
 .../security_solution/public/graphql/types.ts |  32 +
 .../authentications_table/index.test.tsx      |  17 +-
 .../components/hosts_table/index.test.tsx     |  17 +-
 .../public/hosts/pages/hosts.test.tsx         |   9 +-
 .../components/ip_overview/index.test.tsx     |  17 +-
 .../components/kpi_network/index.test.tsx     |  17 +-
 .../network_dns_table/index.test.tsx          |  17 +-
 .../network_http_table/index.test.tsx         |  17 +-
 .../index.test.tsx                            |  17 +-
 .../network_top_n_flow_table/index.test.tsx   |  17 +-
 .../components/tls_table/index.test.tsx       |  17 +-
 .../components/users_table/index.test.tsx     |  17 +-
 .../network/pages/ip_details/index.test.tsx   |  20 +-
 .../public/network/pages/network.test.tsx     |   9 +-
 .../components/overview_host/index.test.tsx   |  17 +-
 .../overview_network/index.test.tsx           |  17 +-
 .../components/recent_timelines/index.tsx     |  39 +-
 .../security_solution/public/plugin.tsx       |  16 +-
 .../components/flyout/header/index.tsx        |   4 +
 .../__snapshots__/index.test.tsx.snap         |   1 +
 .../header_with_close_button/index.test.tsx   |  23 +-
 .../components/flyout/index.test.tsx          |   5 +
 .../components/notes/add_note/index.test.tsx  | 180 ++--
 .../components/notes/add_note/index.tsx       |   1 -
 .../timelines/components/notes/index.tsx      |  21 +-
 .../notes/note_cards/index.test.tsx           |  65 +-
 .../components/notes/note_cards/index.tsx     |   3 +
 .../edit_timeline_batch_actions.tsx           |  11 +-
 .../export_timeline/export_timeline.test.tsx  |  27 -
 .../export_timeline/index.test.tsx            |  44 +-
 .../open_timeline/export_timeline/index.tsx   |  24 +-
 .../components/open_timeline/helpers.ts       | 192 +++--
 .../components/open_timeline/index.tsx        |  94 +-
 .../open_timeline/open_timeline.test.tsx      |   4 +-
 .../open_timeline/open_timeline.tsx           |  36 +-
 .../open_timeline_modal_body.test.tsx         |   4 +-
 .../open_timeline_modal_body.tsx              |  22 +-
 .../open_timeline/search_row/index.tsx        |  15 +-
 .../timelines_table/actions_columns.tsx       |   8 +-
 .../timelines_table/icon_header_columns.tsx   | 105 ++-
 .../open_timeline/timelines_table/index.tsx   |   8 +-
 .../open_timeline/timelines_table/mocks.ts    |   2 +
 .../components/open_timeline/translations.ts  |  23 +-
 .../components/open_timeline/types.ts         |  30 +-
 .../open_timeline/use_timeline_status.tsx     | 110 +++
 .../open_timeline/use_timeline_types.tsx      | 116 +--
 .../__snapshots__/timeline.test.tsx.snap      |   1 +
 .../timeline/body/actions/index.test.tsx      |  13 +-
 .../timeline/body/actions/index.tsx           | 168 ++--
 .../timeline/body/events/stateful_event.tsx   |   9 +-
 .../components/timeline/body/helpers.test.ts  |  29 +-
 .../components/timeline/body/helpers.ts       |  12 +-
 .../components/timeline/body/index.test.tsx   | 240 ++----
 .../components/timeline/body/translations.ts  |  14 +
 .../components/timeline/header/index.test.tsx |  82 +-
 .../components/timeline/header/index.tsx      |  19 +-
 .../timeline/header/translations.ts           |   8 +
 .../components/timeline/index.test.tsx        |   3 +
 .../timelines/components/timeline/index.tsx   |  10 +-
 .../components/timeline/pin/index.tsx         |  26 +-
 .../timeline/properties/helpers.tsx           |  44 +-
 .../timeline/properties/index.test.tsx        |  56 +-
 .../components/timeline/properties/index.tsx  |   9 +-
 .../properties/new_template_timeline.test.tsx |   9 +-
 .../timeline/properties/properties_left.tsx   |   9 +
 .../properties/properties_right.test.tsx      |   3 +-
 .../timeline/properties/properties_right.tsx  |   8 +-
 .../timeline/properties/translations.ts       |   2 +-
 .../selectable_timeline/index.test.tsx        |   8 +-
 .../timeline/selectable_timeline/index.tsx    |  45 +-
 .../components/timeline/timeline.test.tsx     |   2 +
 .../components/timeline/timeline.tsx          |   4 +
 .../containers/all/index.gql_query.ts         |   9 +
 .../public/timelines/containers/all/index.tsx |  61 +-
 .../public/timelines/containers/api.test.ts   |   7 +-
 .../public/timelines/containers/api.ts        |  65 +-
 .../public/timelines/pages/translations.ts    |  14 +
 .../timelines/store/timeline/actions.ts       |   2 +
 .../public/timelines/store/timeline/epic.ts   |  60 +-
 .../timeline/epic_local_storage.test.tsx      |  19 +-
 .../timelines/store/timeline/helpers.ts       |  57 +-
 .../store/timeline/manage_timeline_id.tsx     |  18 +
 .../public/timelines/store/timeline/model.ts  |   1 -
 .../timelines/store/timeline/reducer.ts       |  38 +-
 .../public/timelines/store/timeline/types.ts  |   3 +
 .../plugins/security_solution/public/types.ts |   5 +
 .../server/graphql/timeline/resolvers.ts      |   4 +-
 .../server/graphql/timeline/schema.gql.ts     |  13 +-
 .../security_solution/server/graphql/types.ts |  67 ++
 .../server/lib/detection_engine/README.md     |   4 +-
 .../lib/timeline/pick_saved_timeline.ts       |  20 +-
 .../routes/__mocks__/import_timelines.ts      |  56 +-
 .../routes/__mocks__/request_responses.ts     |  16 +-
 .../routes/clean_draft_timelines_route.ts     |  12 +-
 .../routes/create_timelines_route.test.ts     |  10 +-
 .../timeline/routes/create_timelines_route.ts |  85 +-
 .../routes/import_timelines_route.test.ts     | 517 ++++++++++-
 .../timeline/routes/import_timelines_route.ts | 157 ++--
 .../routes/update_timelines_route.test.ts     |   8 +-
 .../timeline/routes/update_timelines_route.ts |  85 +-
 .../lib/timeline/routes/utils/common.ts       |  21 +-
 .../utils/compare_timelines_status.test.ts    | 810 ++++++++++++++++++
 .../routes/utils/compare_timelines_status.ts  | 247 ++++++
 .../timeline/routes/utils/create_timelines.ts |  47 +-
 .../timeline/routes/utils/export_timelines.ts |   8 +-
 .../timeline/routes/utils/failure_cases.ts    | 377 ++++++++
 .../timeline/routes/utils/timeline_object.ts  |  86 ++
 .../timeline/routes/utils/update_timelines.ts |  80 --
 .../server/lib/timeline/saved_object.ts       | 144 +++-
 126 files changed, 4536 insertions(+), 1365 deletions(-)
 create mode 100644 x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx
 create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts
 create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts
 create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
 create mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/timeline_object.ts
 delete mode 100644 x-pack/plugins/security_solution/server/lib/timeline/routes/utils/update_timelines.ts

diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 58431e405ea8b..4aff1c81c40f7 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -158,6 +158,12 @@ export const showAllOthersBucket: string[] = [
 
 /**
  * CreateTemplateTimelineBtn
+ * https://github.com/elastic/kibana/pull/66613
  * Remove the comment here to enable template timeline
  */
-export const disableTemplate = true;
+export const disableTemplate = false;
+
+/*
+ * This should be set to true after https://github.com/elastic/kibana/pull/67496 is merged
+ */
+export const enableElasticFilter = false;
diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts
index 4f255bb6d6834..2cf5930a83bee 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -137,11 +137,13 @@ const SavedSortRuntimeType = runtimeTypes.partial({
 export enum TimelineStatus {
   active = 'active',
   draft = 'draft',
+  immutable = 'immutable',
 }
 
 export const TimelineStatusLiteralRt = runtimeTypes.union([
   runtimeTypes.literal(TimelineStatus.active),
   runtimeTypes.literal(TimelineStatus.draft),
+  runtimeTypes.literal(TimelineStatus.immutable),
 ]);
 
 const TimelineStatusLiteralWithNullRt = unionWithNullType(TimelineStatusLiteralRt);
@@ -151,6 +153,29 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf<
   typeof TimelineStatusLiteralWithNullRt
 >;
 
+/**
+ * Template timeline type
+ */
+
+export enum TemplateTimelineType {
+  elastic = 'elastic',
+  custom = 'custom',
+}
+
+export const TemplateTimelineTypeLiteralRt = runtimeTypes.union([
+  runtimeTypes.literal(TemplateTimelineType.elastic),
+  runtimeTypes.literal(TemplateTimelineType.custom),
+]);
+
+export const TemplateTimelineTypeLiteralWithNullRt = unionWithNullType(
+  TemplateTimelineTypeLiteralRt
+);
+
+export type TemplateTimelineTypeLiteral = runtimeTypes.TypeOf<typeof TemplateTimelineTypeLiteralRt>;
+export type TemplateTimelineTypeLiteralWithNull = runtimeTypes.TypeOf<
+  typeof TemplateTimelineTypeLiteralWithNullRt
+>;
+
 /*
  *  Timeline Types
  */
@@ -273,6 +298,13 @@ export const TimelineResponseType = runtimeTypes.type({
   }),
 });
 
+export const TimelineErrorResponseType = runtimeTypes.type({
+  status_code: runtimeTypes.number,
+  message: runtimeTypes.string,
+});
+
+export interface TimelineErrorResponse
+  extends runtimeTypes.TypeOf<typeof TimelineErrorResponseType> {}
 export interface TimelineResponse extends runtimeTypes.TypeOf<typeof TimelineResponseType> {}
 
 /**
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx
index 2fa7cfeedcd15..bd62b79a3c54e 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_table/actions.test.tsx
@@ -215,8 +215,8 @@ describe('alert actions', () => {
               columnId: '@timestamp',
               sortDirection: 'desc',
             },
-            status: TimelineStatus.draft,
-            title: '',
+            status: TimelineStatus.active,
+            title: 'Test rule - Duplicate',
             timelineType: TimelineType.default,
             templateTimelineId: null,
             templateTimelineVersion: null,
diff --git a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx
index c7015ed81701e..9c08e05ddfa39 100644
--- a/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/add_filter_to_global_search_bar/index.test.tsx
@@ -12,6 +12,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../mock';
 import { createStore, State } from '../../store';
@@ -35,10 +36,22 @@ jest.mock('../../lib/kibana', () => ({
 describe('AddFilterToGlobalSearchBar Component', () => {
   const state: State = mockGlobalState;
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
     mockAddFilters.mockClear();
   });
 
diff --git a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx
index 4bc77555f09bd..45b75d0f33ac9 100644
--- a/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/error_toast_dispatcher/index.test.tsx
@@ -12,6 +12,7 @@ import {
   apolloClientObservable,
   mockGlobalState,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../mock';
 import { createStore } from '../../store/store';
@@ -22,10 +23,22 @@ import { State } from '../../store/types';
 describe('Error Toast Dispatcher', () => {
   const state: State = mockGlobalState;
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx
index 45397921a6651..f2b7d45972625 100644
--- a/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx
@@ -14,6 +14,7 @@ import {
   mockGlobalState,
   apolloClientObservable,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../mock';
 import { createStore, State } from '../../store';
@@ -36,13 +37,25 @@ describe('Inspect Button', () => {
     state: state.inputs,
   };
 
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   describe('Render', () => {
     beforeEach(() => {
       const myState = cloneDeep(state);
       myState.inputs = upsertQuery(newQuery);
-      store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+      store = createStore(
+        myState,
+        SUB_PLUGINS_REDUCER,
+        apolloClientObservable,
+        kibanaObservable,
+        storage
+      );
     });
     test('Eui Empty Button', () => {
       const wrapper = mount(
@@ -146,7 +159,13 @@ describe('Inspect Button', () => {
         response: ['my response'],
       };
       myState.inputs = upsertQuery(myQuery);
-      store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+      store = createStore(
+        myState,
+        SUB_PLUGINS_REDUCER,
+        apolloClientObservable,
+        kibanaObservable,
+        storage
+      );
     });
     test('Open Inspect Modal', () => {
       const wrapper = mount(
diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx
index 50721ef3b26ad..f548275b36e70 100644
--- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx
@@ -34,6 +34,7 @@ import {
   mockGlobalState,
   apolloClientObservable,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../mock';
 import { State, createStore } from '../../store';
@@ -55,7 +56,13 @@ describe('Stat Items Component', () => {
   const theme = () => ({ eui: euiDarkVars, darkMode: true });
   const state: State = mockGlobalState;
   const { storage } = createSecuritySolutionStorageMock();
-  const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  const store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   describe.each([
     [
diff --git a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx
index 19321622d75fa..164ca177ee91a 100644
--- a/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/super_date_picker/index.test.tsx
@@ -14,6 +14,7 @@ import {
   apolloClientObservable,
   mockGlobalState,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../mock';
 import { createUseUiSetting$Mock } from '../../mock/kibana_react';
@@ -81,11 +82,23 @@ describe('SIEM Super Date Picker', () => {
   describe('#SuperDatePicker', () => {
     const state: State = mockGlobalState;
     const { storage } = createSecuritySolutionStorageMock();
-    let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    let store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
 
     beforeEach(() => {
       jest.clearAllMocks();
-      store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+      store = createStore(
+        state,
+        SUB_PLUGINS_REDUCER,
+        apolloClientObservable,
+        kibanaObservable,
+        storage
+      );
       mockUseUiSetting$.mockImplementation((key, defaultValue) => {
         const useUiSetting$Mock = createUseUiSetting$Mock();
 
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
index ae25e66b2af86..336f906b3bed0 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx
@@ -13,6 +13,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../mock';
 import { createKibanaCoreStartMock } from '../../mock/kibana_core';
@@ -156,7 +157,13 @@ const state: State = {
 };
 
 const { storage } = createSecuritySolutionStorageMock();
-const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+const store = createStore(
+  state,
+  SUB_PLUGINS_REDUCER,
+  apolloClientObservable,
+  kibanaObservable,
+  storage
+);
 
 describe('StatefulTopN', () => {
   // Suppress warnings about "react-beautiful-dnd"
diff --git a/x-pack/plugins/security_solution/public/common/lib/compose/kibana_compose.tsx b/x-pack/plugins/security_solution/public/common/lib/compose/kibana_compose.tsx
index 47834f148c910..342db7f43943d 100644
--- a/x-pack/plugins/security_solution/public/common/lib/compose/kibana_compose.tsx
+++ b/x-pack/plugins/security_solution/public/common/lib/compose/kibana_compose.tsx
@@ -9,10 +9,10 @@ import ApolloClient from 'apollo-client';
 import { ApolloLink } from 'apollo-link';
 
 // eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { CoreStart } from '../../../../../../../src/core/public';
 import introspectionQueryResultData from '../../../graphql/introspection.json';
 import { AppFrontendLibs } from '../lib';
 import { getLinks } from './helpers';
+import { CoreStart } from '../../../../../../../src/core/public';
 
 export function composeLibs(core: CoreStart): AppFrontendLibs {
   const cache = new InMemoryCache({
diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx
index 1db63897a8863..779d5eff0b971 100644
--- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx
+++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx
@@ -13,7 +13,7 @@ import { coreMock } from '../../../../../../../src/core/public/mocks';
 import { StartPlugins } from '../../../types';
 import { depsStartMock } from './dependencies_start_mock';
 import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../../store/test_utils';
-import { apolloClientObservable } from '../test_providers';
+import { apolloClientObservable, kibanaObservable } from '../test_providers';
 import { createStore, State, substateMiddlewareFactory } from '../../store';
 import { alertMiddlewareFactory } from '../../../endpoint_alerts/store/middleware';
 import { AppRootProvider } from './app_root_provider';
@@ -58,14 +58,21 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
   const middlewareSpy = createSpyMiddleware();
   const { storage } = createSecuritySolutionStorageMock();
 
-  const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage, [
-    substateMiddlewareFactory(
-      (globalState) => globalState.alertList,
-      alertMiddlewareFactory(coreStart, depsStart)
-    ),
-    ...managementMiddlewareFactory(coreStart, depsStart),
-    middlewareSpy.actionSpyMiddleware,
-  ]);
+  const store = createStore(
+    mockGlobalState,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage,
+    [
+      substateMiddlewareFactory(
+        (globalState) => globalState.alertList,
+        alertMiddlewareFactory(coreStart, depsStart)
+      ),
+      ...managementMiddlewareFactory(coreStart, depsStart),
+      middlewareSpy.actionSpyMiddleware,
+    ]
+  );
 
   const MockKibanaContextProvider = createKibanaContextProviderMock();
 
diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts
index 2b639bfdc14f5..c5d50e1379482 100644
--- a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts
@@ -26,6 +26,7 @@ import {
   DEFAULT_INDEX_PATTERN,
 } from '../../../common/constants';
 import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core';
+import { StartServices } from '../../types';
 import { createSecuritySolutionStorageMock } from './mock_local_storage';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -71,6 +72,8 @@ export const createUseUiSetting$Mock = () => {
   ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()];
 };
 
+export const createKibanaObservable$Mock = createKibanaCoreStartMock;
+
 export const createUseKibanaMock = () => {
   const core = createKibanaCoreStartMock();
   const plugins = createKibanaPluginsStartMock();
@@ -90,6 +93,36 @@ export const createUseKibanaMock = () => {
   return () => ({ services });
 };
 
+export const createStartServices = () => {
+  const core = createKibanaCoreStartMock();
+  const plugins = createKibanaPluginsStartMock();
+  const security = {
+    authc: {
+      getCurrentUser: jest.fn(),
+      areAPIKeysEnabled: jest.fn(),
+    },
+    sessionTimeout: {
+      start: jest.fn(),
+      stop: jest.fn(),
+      extend: jest.fn(),
+    },
+    license: {
+      isEnabled: jest.fn(),
+      getFeatures: jest.fn(),
+      features$: jest.fn(),
+    },
+    __legacyCompat: { logoutUrl: 'logoutUrl', tenant: 'tenant' },
+  };
+
+  const services = ({
+    ...core,
+    ...plugins,
+    security,
+  } as unknown) as StartServices;
+
+  return services;
+};
+
 export const createWithKibanaMock = () => {
   const kibana = createUseKibanaMock()();
 
diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
index 0573f049c35c5..297dc235a2a50 100644
--- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
+++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx
@@ -19,7 +19,7 @@ import { ThemeProvider } from 'styled-components';
 
 import { createStore, State } from '../store';
 import { mockGlobalState } from './global_state';
-import { createKibanaContextProviderMock } from './kibana_react';
+import { createKibanaContextProviderMock, createStartServices } from './kibana_react';
 import { FieldHook, useForm } from '../../shared_imports';
 import { SUB_PLUGINS_REDUCER } from './utils';
 import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage';
@@ -38,6 +38,7 @@ export const apolloClient = new ApolloClient({
 });
 
 export const apolloClientObservable = new BehaviorSubject(apolloClient);
+export const kibanaObservable = new BehaviorSubject(createStartServices());
 
 Object.defineProperty(window, 'localStorage', {
   value: localStorageMock(),
@@ -49,7 +50,13 @@ const { storage } = createSecuritySolutionStorageMock();
 /** A utility for wrapping children in the providers required to run most tests */
 const TestProvidersComponent: React.FC<Props> = ({
   children,
-  store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage),
+  store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  ),
   onDragEnd = jest.fn(),
 }) => (
   <I18nProvider>
@@ -69,7 +76,13 @@ export const TestProviders = React.memo(TestProvidersComponent);
 
 const TestProviderWithoutDragAndDropComponent: React.FC<Props> = ({
   children,
-  store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage),
+  store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  ),
 }) => (
   <I18nProvider>
     <ReduxStoreProvider store={store}>{children}</ReduxStoreProvider>
diff --git a/x-pack/plugins/security_solution/public/common/store/store.ts b/x-pack/plugins/security_solution/public/common/store/store.ts
index 5f53724b287df..a39c9f18bcdb8 100644
--- a/x-pack/plugins/security_solution/public/common/store/store.ts
+++ b/x-pack/plugins/security_solution/public/common/store/store.ts
@@ -29,6 +29,7 @@ import { AppAction } from './actions';
 import { Immutable } from '../../../common/endpoint/types';
 import { State } from './types';
 import { Storage } from '../../../../../../src/plugins/kibana_utils/public';
+import { CoreStart } from '../../../../../../src/core/public';
 
 type ComposeType = typeof compose;
 declare global {
@@ -49,6 +50,7 @@ export const createStore = (
   state: PreloadedState<State>,
   pluginsReducer: SubPluginsInitReducer,
   apolloClient: Observable<AppApolloClient>,
+  kibana: Observable<CoreStart>,
   storage: Storage,
   additionalMiddleware?: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>
 ): Store<State, Action> => {
@@ -56,6 +58,7 @@ export const createStore = (
 
   const middlewareDependencies = {
     apolloClient$: apolloClient,
+    kibana$: kibana,
     selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector,
     selectNotesByIdSelector: appSelectors.selectNotesByIdSelector,
     timelineByIdSelector: timelineSelectors.timelineByIdSelector,
diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts
index 2b92451e30119..d1e8df0f982c4 100644
--- a/x-pack/plugins/security_solution/public/common/store/types.ts
+++ b/x-pack/plugins/security_solution/public/common/store/types.ts
@@ -19,23 +19,22 @@ import { NetworkPluginState } from '../../network/store';
 import { EndpointAlertsPluginState } from '../../endpoint_alerts';
 import { ManagementPluginState } from '../../management';
 
+export type StoreState = HostsPluginState &
+  NetworkPluginState &
+  TimelinePluginState &
+  EndpointAlertsPluginState &
+  ManagementPluginState & {
+    app: AppState;
+    dragAndDrop: DragAndDropState;
+    inputs: InputsState;
+  };
 /**
  * The redux `State` type for the Security App.
  * We use `CombinedState` to wrap our shape because we create our reducer using `combineReducers`.
  * `combineReducers` returns a type wrapped in `CombinedState`.
  * `CombinedState` is required for redux to know what keys to make optional when preloaded state into a store.
  */
-export type State = CombinedState<
-  HostsPluginState &
-    NetworkPluginState &
-    TimelinePluginState &
-    EndpointAlertsPluginState &
-    ManagementPluginState & {
-      app: AppState;
-      dragAndDrop: DragAndDropState;
-      inputs: InputsState;
-    }
->;
+export type State = CombinedState<StoreState>;
 
 export type KueryFilterQueryKind = 'kuery' | 'lucene';
 
diff --git a/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx b/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx
index acfe3f228c21f..f03c72518305d 100644
--- a/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx
+++ b/x-pack/plugins/security_solution/public/endpoint_alerts/view/test_helpers/render_alert_page.tsx
@@ -19,6 +19,7 @@ import {
   SUB_PLUGINS_REDUCER,
   mockGlobalState,
   apolloClientObservable,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 
@@ -31,7 +32,13 @@ export const alertPageTestRender = () => {
    * Create a store, with the middleware disabled. We don't want side effects being created by our code in this test.
    */
   const { storage } = createSecuritySolutionStorageMock();
-  const store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  const store = createStore(
+    mockGlobalState,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   const depsStart = depsStartMock();
   depsStart.data.ui.SearchBar.mockImplementation(() => <div />);
diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json
index 48547212bb6c0..69356f8fc8aa7 100644
--- a/x-pack/plugins/security_solution/public/graphql/introspection.json
+++ b/x-pack/plugins/security_solution/public/graphql/introspection.json
@@ -255,6 +255,18 @@
                 "description": "",
                 "type": { "kind": "ENUM", "name": "TimelineType", "ofType": null },
                 "defaultValue": null
+              },
+              {
+                "name": "templateTimelineType",
+                "description": "",
+                "type": { "kind": "ENUM", "name": "TemplateTimelineType", "ofType": null },
+                "defaultValue": null
+              },
+              {
+                "name": "status",
+                "description": "",
+                "type": { "kind": "ENUM", "name": "TimelineStatus", "ofType": null },
+                "defaultValue": null
               }
             ],
             "type": {
@@ -10405,7 +10417,13 @@
         "interfaces": null,
         "enumValues": [
           { "name": "active", "description": "", "isDeprecated": false, "deprecationReason": null },
-          { "name": "draft", "description": "", "isDeprecated": false, "deprecationReason": null }
+          { "name": "draft", "description": "", "isDeprecated": false, "deprecationReason": null },
+          {
+            "name": "immutable",
+            "description": "",
+            "isDeprecated": false,
+            "deprecationReason": null
+          }
         ],
         "possibleTypes": null
       },
@@ -10529,6 +10547,24 @@
         ],
         "possibleTypes": null
       },
+      {
+        "kind": "ENUM",
+        "name": "TemplateTimelineType",
+        "description": "",
+        "fields": null,
+        "inputFields": null,
+        "interfaces": null,
+        "enumValues": [
+          {
+            "name": "elastic",
+            "description": "",
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null }
+        ],
+        "possibleTypes": null
+      },
       {
         "kind": "OBJECT",
         "name": "ResponseTimelines",
@@ -10557,6 +10593,46 @@
             "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
             "isDeprecated": false,
             "deprecationReason": null
+          },
+          {
+            "name": "defaultTimelineCount",
+            "description": "",
+            "args": [],
+            "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "templateTimelineCount",
+            "description": "",
+            "args": [],
+            "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "elasticTemplateTimelineCount",
+            "description": "",
+            "args": [],
+            "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "customTemplateTimelineCount",
+            "description": "",
+            "args": [],
+            "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "favoriteCount",
+            "description": "",
+            "args": [],
+            "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
+            "isDeprecated": false,
+            "deprecationReason": null
           }
         ],
         "inputFields": null,
diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts
index b5088fe51b446..1171e93793536 100644
--- a/x-pack/plugins/security_solution/public/graphql/types.ts
+++ b/x-pack/plugins/security_solution/public/graphql/types.ts
@@ -345,6 +345,7 @@ export enum TlsFields {
 export enum TimelineStatus {
   active = 'active',
   draft = 'draft',
+  immutable = 'immutable',
 }
 
 export enum TimelineType {
@@ -359,6 +360,11 @@ export enum SortFieldTimeline {
   created = 'created',
 }
 
+export enum TemplateTimelineType {
+  elastic = 'elastic',
+  custom = 'custom',
+}
+
 export enum NetworkDirectionEcs {
   inbound = 'inbound',
   outbound = 'outbound',
@@ -2117,6 +2123,16 @@ export interface ResponseTimelines {
   timeline: (Maybe<TimelineResult>)[];
 
   totalCount?: Maybe<number>;
+
+  defaultTimelineCount?: Maybe<number>;
+
+  templateTimelineCount?: Maybe<number>;
+
+  elasticTemplateTimelineCount?: Maybe<number>;
+
+  customTemplateTimelineCount?: Maybe<number>;
+
+  favoriteCount?: Maybe<number>;
 }
 
 export interface Mutation {
@@ -2254,6 +2270,10 @@ export interface GetAllTimelineQueryArgs {
   onlyUserFavorite?: Maybe<boolean>;
 
   timelineType?: Maybe<TimelineType>;
+
+  templateTimelineType?: Maybe<TemplateTimelineType>;
+
+  status?: Maybe<TimelineStatus>;
 }
 export interface AuthenticationsSourceArgs {
   timerange: TimerangeInput;
@@ -4315,6 +4335,8 @@ export namespace GetAllTimeline {
     sort?: Maybe<SortTimeline>;
     onlyUserFavorite?: Maybe<boolean>;
     timelineType?: Maybe<TimelineType>;
+    templateTimelineType?: Maybe<TemplateTimelineType>;
+    status?: Maybe<TimelineStatus>;
   };
 
   export type Query = {
@@ -4328,6 +4350,16 @@ export namespace GetAllTimeline {
 
     totalCount: Maybe<number>;
 
+    defaultTimelineCount: Maybe<number>;
+
+    templateTimelineCount: Maybe<number>;
+
+    elasticTemplateTimelineCount: Maybe<number>;
+
+    customTemplateTimelineCount: Maybe<number>;
+
+    favoriteCount: Maybe<number>;
+
     timeline: (Maybe<Timeline>)[];
   };
 
diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx
index 3809d848759cc..9603f30615a12 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx
@@ -13,6 +13,7 @@ import {
   apolloClientObservable,
   mockGlobalState,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { createStore, State } from '../../../common/store';
@@ -26,10 +27,22 @@ describe('Authentication Table Component', () => {
   const state: State = mockGlobalState;
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx
index 1231c35f21460..ab00e77a4fa43 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx
@@ -15,6 +15,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { useMountAppended } from '../../../common/utils/use_mount_appended';
@@ -40,11 +41,23 @@ describe('Hosts Table', () => {
   const state: State = mockGlobalState;
   const { storage } = createSecuritySolutionStorageMock();
 
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
   const mount = useMountAppended();
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx
index ea0b32137eb39..1ea3a3020a1d5 100644
--- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx
@@ -16,6 +16,7 @@ import {
   TestProviders,
   mockGlobalState,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../common/mock';
 import { SiemNavigation } from '../../common/components/navigation';
@@ -154,7 +155,13 @@ describe('Hosts - rendering', () => {
     });
     const myState: State = mockGlobalState;
     const { storage } = createSecuritySolutionStorageMock();
-    const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    const myStore = createStore(
+      myState,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
     const wrapper = mount(
       <TestProviders store={myStore}>
         <Router history={mockHistory}>
diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx
index 553cb8c63db98..b8d97f06bf85f 100644
--- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx
@@ -14,6 +14,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { createStore, State } from '../../../common/store';
@@ -28,10 +29,22 @@ describe('IP Overview Component', () => {
   const state: State = mockGlobalState;
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx
index 580a5420f1c34..8acd17d2ce767 100644
--- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx
@@ -12,6 +12,7 @@ import {
   apolloClientObservable,
   mockGlobalState,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { createStore, State } from '../../../common/store';
@@ -25,10 +26,22 @@ describe('KpiNetwork Component', () => {
   const narrowDateRange = jest.fn();
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
index 036ebedd6b88e..bbbe56715d345 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx
@@ -15,6 +15,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { State, createStore } from '../../../common/store';
@@ -28,11 +29,23 @@ describe('NetworkTopNFlow Table Component', () => {
   const loadPage = jest.fn();
   const state: State = mockGlobalState;
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
   const mount = useMountAppended();
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
index ac37aaf309155..72c932c575be3 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx
@@ -15,6 +15,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { useMountAppended } from '../../../common/utils/use_mount_appended';
@@ -31,11 +32,23 @@ describe('NetworkHttp Table Component', () => {
   const state: State = mockGlobalState;
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
   const mount = useMountAppended();
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
index 8b1dbc8c558b6..a1ee0574d8b05 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx
@@ -17,6 +17,7 @@ import {
   mockIndexPattern,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { useMountAppended } from '../../../common/utils/use_mount_appended';
@@ -32,10 +33,22 @@ describe('NetworkTopCountries Table Component', () => {
   const mount = useMountAppended();
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
index b14d411810dee..100ecaa51f4ae 100644
--- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx
@@ -16,6 +16,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { useMountAppended } from '../../../common/utils/use_mount_appended';
@@ -31,11 +32,23 @@ describe('NetworkTopNFlow Table Component', () => {
   const state: State = mockGlobalState;
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
   const mount = useMountAppended();
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
index acbe974f914d7..cd2dc926c03bc 100644
--- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx
@@ -15,6 +15,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { useMountAppended } from '../../../common/utils/use_mount_appended';
@@ -28,11 +29,23 @@ describe('Tls Table Component', () => {
   const state: State = mockGlobalState;
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
   const mount = useMountAppended();
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('Rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
index f0d4d7fbeefc6..3f1762cadd652 100644
--- a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx
@@ -16,6 +16,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { useMountAppended } from '../../../common/utils/use_mount_appended';
@@ -30,11 +31,23 @@ describe('Users Table Component', () => {
   const state: State = mockGlobalState;
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
   const mount = useMountAppended();
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   describe('Rendering', () => {
diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx
index a87eb3d057447..962a6269f8488 100644
--- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx
@@ -18,6 +18,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { useMountAppended } from '../../../common/utils/use_mount_appended';
@@ -90,7 +91,6 @@ const getMockProps = (ip: string) => ({
 
 describe('Ip Details', () => {
   const mount = useMountAppended();
-
   beforeAll(() => {
     (useWithSource as jest.Mock).mockReturnValue({
       indicesExist: false,
@@ -107,15 +107,27 @@ describe('Ip Details', () => {
   });
 
   afterAll(() => {
-    delete (global as GlobalWithFetch).fetch;
+    jest.resetAllMocks();
   });
 
   const state: State = mockGlobalState;
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   test('it renders', () => {
diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx
index 7cdfdbf0af69a..af84e1d42b45b 100644
--- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx
+++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx
@@ -16,6 +16,7 @@ import {
   mockGlobalState,
   apolloClientObservable,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../common/mock';
 import { State, createStore } from '../../common/store';
@@ -139,7 +140,13 @@ describe('rendering - rendering', () => {
     });
     const myState: State = mockGlobalState;
     const { storage } = createSecuritySolutionStorageMock();
-    const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    const myStore = createStore(
+      myState,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
     const wrapper = mount(
       <TestProviders store={myStore}>
         <Router history={mockHistory}>
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx
index d29efa2d44c15..2b21385004a73 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx
@@ -14,6 +14,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 
@@ -95,11 +96,23 @@ describe('OverviewHost', () => {
   const state: State = mockGlobalState;
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
     const myState = cloneDeep(state);
-    store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      myState,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   test('it renders the expected widget title', () => {
diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx
index b4b685465dbda..7a9834ee3ea9a 100644
--- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx
@@ -14,6 +14,7 @@ import {
   TestProviders,
   SUB_PLUGINS_REDUCER,
   createSecuritySolutionStorageMock,
+  kibanaObservable,
 } from '../../../common/mock';
 
 import { OverviewNetwork } from '.';
@@ -86,11 +87,23 @@ describe('OverviewNetwork', () => {
   const state: State = mockGlobalState;
 
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
     const myState = cloneDeep(state);
-    store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      myState,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
   });
 
   test('it renders the expected widget title', () => {
diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx
index 9c149a850bec9..8f2b3c7495f0d 100644
--- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx
+++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx
@@ -24,6 +24,7 @@ import { RecentTimelines } from './recent_timelines';
 import * as i18n from './translations';
 import { FilterMode } from './types';
 import { LoadingPlaceholders } from '../loading_placeholders';
+import { useTimelineStatus } from '../../../timelines/components/open_timeline/use_timeline_status';
 import { useKibana } from '../../../common/lib/kibana';
 import { SecurityPageName } from '../../../app/types';
 import { APP_ID } from '../../../../common/constants';
@@ -83,25 +84,25 @@ const StatefulRecentTimelinesComponent = React.memo<Props>(
     );
 
     const { fetchAllTimeline, timelines, loading } = useGetAllTimeline();
-
-    useEffect(
-      () =>
-        fetchAllTimeline({
-          pageInfo: {
-            pageIndex: 1,
-            pageSize: PAGE_SIZE,
-          },
-          search: '',
-          sort: {
-            sortField: SortFieldTimeline.updated,
-            sortOrder: Direction.desc,
-          },
-          onlyUserFavorite: filterBy === 'favorites',
-          timelineType: TimelineType.default,
-        }),
-      // eslint-disable-next-line react-hooks/exhaustive-deps
-      [filterBy]
-    );
+    const timelineType = TimelineType.default;
+    const { templateTimelineType, timelineStatus } = useTimelineStatus({ timelineType });
+    useEffect(() => {
+      fetchAllTimeline({
+        pageInfo: {
+          pageIndex: 1,
+          pageSize: PAGE_SIZE,
+        },
+        search: '',
+        sort: {
+          sortField: SortFieldTimeline.updated,
+          sortOrder: Direction.desc,
+        },
+        onlyUserFavorite: filterBy === 'favorites',
+        status: timelineStatus,
+        timelineType,
+        templateTimelineType,
+      });
+    }, [fetchAllTimeline, filterBy, timelineStatus, timelineType, templateTimelineType]);
 
     return (
       <>
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index b247170a4a5db..d7e29a466cbf2 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -23,7 +23,14 @@ import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public';
 import { initTelemetry } from './common/lib/telemetry';
 import { KibanaServices } from './common/lib/kibana/services';
 import { serviceNowActionType, jiraActionType } from './common/lib/connectors';
-import { PluginSetup, PluginStart, SetupPlugins, StartPlugins, StartServices } from './types';
+import {
+  PluginSetup,
+  PluginStart,
+  SetupPlugins,
+  StartPlugins,
+  StartServices,
+  AppObservableLibs,
+} from './types';
 import {
   APP_ID,
   APP_ICON,
@@ -120,6 +127,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
           this.downloadAssets(),
           this.downloadSubPlugins(),
         ]);
+
         return renderApp({
           ...composeLibs(coreStart),
           ...params,
@@ -396,8 +404,9 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
       endpointAlertsSubPlugin,
       managementSubPlugin,
     } = await this.downloadSubPlugins();
-
-    const libs$ = new BehaviorSubject(composeLibs(coreStart));
+    const { apolloClient } = composeLibs(coreStart);
+    const appLibs: AppObservableLibs = { apolloClient, kibana: coreStart };
+    const libs$ = new BehaviorSubject(appLibs);
 
     const alertsStart = alertsSubPlugin.start(storage);
     const hostsStart = hostsSubPlugin.start(storage);
@@ -434,6 +443,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
         ...managementSubPluginStart.store.reducer,
       },
       libs$.pipe(pluck('apolloClient')),
+      libs$.pipe(pluck('kibana')),
       storage,
       [
         ...(endpointAlertsStart.store.middleware ?? []),
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
index 9fe48cd2f0190..8e34e11e85729 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx
@@ -41,6 +41,7 @@ const StatefulFlyoutHeader = React.memo<Props>(
     notesById,
     status,
     timelineId,
+    timelineType,
     title,
     toggleLock,
     updateDescription,
@@ -66,6 +67,7 @@ const StatefulFlyoutHeader = React.memo<Props>(
         noteIds={noteIds}
         status={status}
         timelineId={timelineId}
+        timelineType={timelineType}
         title={title}
         toggleLock={toggleLock}
         updateDescription={updateDescription}
@@ -100,6 +102,7 @@ const makeMapStateToProps = () => {
       title = '',
       noteIds = emptyNotesId,
       status,
+      timelineType,
     } = timeline;
 
     const history = emptyHistory; // TODO: get history from store via selector
@@ -116,6 +119,7 @@ const makeMapStateToProps = () => {
       notesById: getNotesByIds(state),
       status,
       title,
+      timelineType,
     };
   };
   return mapStateToProps;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap
index df96f2a1f7eba..d0d7a1cd7f5d7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap
@@ -4,6 +4,7 @@ exports[`FlyoutHeaderWithCloseButton renders correctly against snapshot 1`] = `
 <FlyoutHeaderWithCloseButton
   onClose={[MockFunction]}
   timelineId="test"
+  timelineType="default"
   usersViewing={
     Array [
       "elastic",
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx
index 3ddc1efe9a47f..9b7d4c3266c56 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx
@@ -7,6 +7,7 @@
 import { mount, shallow } from 'enzyme';
 import React from 'react';
 
+import { TimelineType } from '../../../../../common/types/timeline';
 import { TestProviders } from '../../../../common/mock';
 import { FlyoutHeaderWithCloseButton } from '.';
 
@@ -40,14 +41,16 @@ jest.mock('../../../../common/lib/kibana', () => {
 });
 
 describe('FlyoutHeaderWithCloseButton', () => {
+  const props = {
+    onClose: jest.fn(),
+    timelineId: 'test',
+    timelineType: TimelineType.default,
+    usersViewing: ['elastic'],
+  };
   test('renders correctly against snapshot', () => {
     const EmptyComponent = shallow(
       <TestProviders>
-        <FlyoutHeaderWithCloseButton
-          onClose={jest.fn()}
-          timelineId={'test'}
-          usersViewing={['elastic']}
-        />
+        <FlyoutHeaderWithCloseButton {...props} />
       </TestProviders>
     );
     expect(EmptyComponent.find('FlyoutHeaderWithCloseButton')).toMatchSnapshot();
@@ -55,13 +58,13 @@ describe('FlyoutHeaderWithCloseButton', () => {
 
   test('it should invoke onClose when the close button is clicked', () => {
     const closeMock = jest.fn();
+    const testProps = {
+      ...props,
+      onClose: closeMock,
+    };
     const wrapper = mount(
       <TestProviders>
-        <FlyoutHeaderWithCloseButton
-          onClose={closeMock}
-          timelineId={'test'}
-          usersViewing={['elastic']}
-        />
+        <FlyoutHeaderWithCloseButton {...testProps} />
       </TestProviders>
     );
     wrapper.find('[data-test-subj="close-timeline"] button').first().simulate('click');
diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx
index 932cde32f3d43..50578ef0a8e42 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx
@@ -14,6 +14,7 @@ import {
   mockGlobalState,
   TestProviders,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../common/mock';
 import { createStore, State } from '../../../common/store';
@@ -62,6 +63,7 @@ describe('Flyout', () => {
         stateShowIsTrue,
         SUB_PLUGINS_REDUCER,
         apolloClientObservable,
+        kibanaObservable,
         storage
       );
 
@@ -86,6 +88,7 @@ describe('Flyout', () => {
         stateWithDataProviders,
         SUB_PLUGINS_REDUCER,
         apolloClientObservable,
+        kibanaObservable,
         storage
       );
 
@@ -108,6 +111,7 @@ describe('Flyout', () => {
         stateWithDataProviders,
         SUB_PLUGINS_REDUCER,
         apolloClientObservable,
+        kibanaObservable,
         storage
       );
 
@@ -142,6 +146,7 @@ describe('Flyout', () => {
         stateWithDataProviders,
         SUB_PLUGINS_REDUCER,
         apolloClientObservable,
+        kibanaObservable,
         storage
       );
 
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
index 1ddf298110a5d..570c0028e0f51 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.test.tsx
@@ -8,52 +8,39 @@ import { mount, shallow } from 'enzyme';
 import React from 'react';
 
 import { AddNote } from '.';
+import { TimelineStatus } from '../../../../../common/types/timeline';
 
 describe('AddNote', () => {
   const note = 'The contents of a new note';
+  const props = {
+    associateNote: jest.fn(),
+    getNewNoteId: jest.fn(),
+    newNote: note,
+    onCancelAddNote: jest.fn(),
+    updateNewNote: jest.fn(),
+    updateNote: jest.fn(),
+    status: TimelineStatus.active,
+  };
 
   test('renders correctly', () => {
-    const wrapper = shallow(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const wrapper = shallow(<AddNote {...props} />);
     expect(wrapper).toMatchSnapshot();
   });
 
   test('it renders the Cancel button when onCancelAddNote is provided', () => {
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const wrapper = mount(<AddNote {...props} />);
 
     expect(wrapper.find('[data-test-subj="cancel"]').exists()).toEqual(true);
   });
 
   test('it invokes onCancelAddNote when the Cancel button is clicked', () => {
     const onCancelAddNote = jest.fn();
+    const testProps = {
+      ...props,
+      onCancelAddNote,
+    };
 
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        onCancelAddNote={onCancelAddNote}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const wrapper = mount(<AddNote {...testProps} />);
 
     wrapper.find('[data-test-subj="cancel"]').first().simulate('click');
 
@@ -62,17 +49,12 @@ describe('AddNote', () => {
 
   test('it does NOT invoke associateNote when the Cancel button is clicked', () => {
     const associateNote = jest.fn();
+    const testProps = {
+      ...props,
+      associateNote,
+    };
 
-    const wrapper = mount(
-      <AddNote
-        associateNote={associateNote}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const wrapper = mount(<AddNote {...testProps} />);
 
     wrapper.find('[data-test-subj="cancel"]').first().simulate('click');
 
@@ -80,47 +62,29 @@ describe('AddNote', () => {
   });
 
   test('it does NOT render the Cancel button when onCancelAddNote is NOT provided', () => {
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const testProps = {
+      ...props,
+      onCancelAddNote: undefined,
+    };
+    const wrapper = mount(<AddNote {...testProps} />);
 
     expect(wrapper.find('[data-test-subj="cancel"]').exists()).toEqual(false);
   });
 
   test('it renders the contents of the note', () => {
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const wrapper = mount(<AddNote {...props} />);
 
     expect(wrapper.find('[data-test-subj="add-a-note"]').first().text()).toEqual(note);
   });
 
   test('it invokes associateNote when the Add Note button is clicked', () => {
     const associateNote = jest.fn();
-
-    const wrapper = mount(
-      <AddNote
-        associateNote={associateNote}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const testProps = {
+      ...props,
+      newNote: note,
+      associateNote,
+    };
+    const wrapper = mount(<AddNote {...testProps} />);
 
     wrapper.find('[data-test-subj="add-note"]').first().simulate('click');
 
@@ -129,17 +93,12 @@ describe('AddNote', () => {
 
   test('it invokes getNewNoteId when the Add Note button is clicked', () => {
     const getNewNoteId = jest.fn();
+    const testProps = {
+      ...props,
+      getNewNoteId,
+    };
 
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={getNewNoteId}
-        newNote={note}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const wrapper = mount(<AddNote {...testProps} />);
 
     wrapper.find('[data-test-subj="add-note"]').first().simulate('click');
 
@@ -148,17 +107,12 @@ describe('AddNote', () => {
 
   test('it invokes updateNewNote when the Add Note button is clicked', () => {
     const updateNewNote = jest.fn();
+    const testProps = {
+      ...props,
+      updateNewNote,
+    };
 
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={updateNewNote}
-        updateNote={jest.fn()}
-      />
-    );
+    const wrapper = mount(<AddNote {...testProps} />);
 
     wrapper.find('[data-test-subj="add-note"]').first().simulate('click');
 
@@ -167,17 +121,11 @@ describe('AddNote', () => {
 
   test('it invokes updateNote when the Add Note button is clicked', () => {
     const updateNote = jest.fn();
-
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={note}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={updateNote}
-      />
-    );
+    const testProps = {
+      ...props,
+      updateNote,
+    };
+    const wrapper = mount(<AddNote {...testProps} />);
 
     wrapper.find('[data-test-subj="add-note"]').first().simulate('click');
 
@@ -185,16 +133,11 @@ describe('AddNote', () => {
   });
 
   test('it does NOT display the markdown formatting hint when a note has NOT been entered', () => {
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={''}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const testProps = {
+      ...props,
+      newNote: '',
+    };
+    const wrapper = mount(<AddNote {...testProps} />);
 
     expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
       'visibility',
@@ -203,16 +146,11 @@ describe('AddNote', () => {
   });
 
   test('it displays the markdown formatting hint when a note has been entered', () => {
-    const wrapper = mount(
-      <AddNote
-        associateNote={jest.fn()}
-        getNewNoteId={jest.fn()}
-        newNote={'We should see a formatting hint now'}
-        onCancelAddNote={jest.fn()}
-        updateNewNote={jest.fn()}
-        updateNote={jest.fn()}
-      />
-    );
+    const testProps = {
+      ...props,
+      newNote: 'We should see a formatting hint now',
+    };
+    const wrapper = mount(<AddNote {...testProps} />);
 
     expect(wrapper.find('[data-test-subj="markdown-hint"]').first()).toHaveStyleRule(
       'visibility',
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
index d3db1a619600f..7c211aafdf8c6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/add_note/index.tsx
@@ -61,7 +61,6 @@ export const AddNote = React.memo<{
       }),
     [associateNote, getNewNoteId, newNote, updateNewNote, updateNote]
   );
-
   return (
     <AddNotesContainer alignItems="flexEnd" direction="column" gutterSize="none">
       <NewNote note={newNote} noteInputHeight={200} updateNewNote={updateNewNote} />
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx
index 42f28f0340679..957b37a0bd1c2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx
@@ -21,12 +21,14 @@ import { AddNote } from './add_note';
 import { columns } from './columns';
 import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers';
 import { NOTES_PANEL_WIDTH, NOTES_PANEL_HEIGHT } from '../timeline/properties/notes_size';
+import { TimelineStatusLiteral, TimelineStatus } from '../../../../common/types/timeline';
 
 interface Props {
   associateNote: AssociateNote;
   getNotesByIds: (noteIds: string[]) => Note[];
   getNewNoteId: GetNewNoteId;
   noteIds: string[];
+  status: TimelineStatusLiteral;
   updateNote: UpdateNote;
 }
 
@@ -53,8 +55,9 @@ InMemoryTable.displayName = 'InMemoryTable';
 
 /** A view for entering and reviewing notes */
 export const Notes = React.memo<Props>(
-  ({ associateNote, getNotesByIds, getNewNoteId, noteIds, updateNote }) => {
+  ({ associateNote, getNotesByIds, getNewNoteId, noteIds, status, updateNote }) => {
     const [newNote, setNewNote] = useState('');
+    const isImmutable = status === TimelineStatus.immutable;
 
     return (
       <NotesPanel>
@@ -63,13 +66,15 @@ export const Notes = React.memo<Props>(
         </EuiModalHeader>
 
         <EuiModalBody>
-          <AddNote
-            associateNote={associateNote}
-            getNewNoteId={getNewNoteId}
-            newNote={newNote}
-            updateNewNote={setNewNote}
-            updateNote={updateNote}
-          />
+          {!isImmutable && (
+            <AddNote
+              associateNote={associateNote}
+              getNewNoteId={getNewNoteId}
+              newNote={newNote}
+              updateNewNote={setNewNote}
+              updateNote={updateNote}
+            />
+          )}
           <EuiSpacer size="s" />
           <InMemoryTable
             data-test-subj="notes-table"
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx
index fa63eb625f283..952295d0858ee 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx
@@ -12,6 +12,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
 import { Note } from '../../../../common/lib/note';
 
 import { NoteCards } from '.';
+import { TimelineStatus } from '../../../../../common/types/timeline';
 
 describe('NoteCards', () => {
   const noteIds = ['abc', 'def'];
@@ -38,18 +39,21 @@ describe('NoteCards', () => {
     },
   ];
 
+  const props = {
+    associateNote: jest.fn(),
+    getNotesByIds,
+    getNewNoteId: jest.fn(),
+    noteIds,
+    showAddNote: true,
+    status: TimelineStatus.active,
+    toggleShowAddNote: jest.fn(),
+    updateNote: jest.fn(),
+  };
+
   test('it renders the notes column when noteIds are specified', () => {
     const wrapper = mountWithIntl(
       <ThemeProvider theme={theme}>
-        <NoteCards
-          associateNote={jest.fn()}
-          getNotesByIds={getNotesByIds}
-          getNewNoteId={jest.fn()}
-          noteIds={noteIds}
-          showAddNote={true}
-          toggleShowAddNote={jest.fn()}
-          updateNote={jest.fn()}
-        />
+        <NoteCards {...props} />
       </ThemeProvider>
     );
 
@@ -57,17 +61,10 @@ describe('NoteCards', () => {
   });
 
   test('it does NOT render the notes column when noteIds are NOT specified', () => {
+    const testProps = { ...props, noteIds: [] };
     const wrapper = mountWithIntl(
       <ThemeProvider theme={theme}>
-        <NoteCards
-          associateNote={jest.fn()}
-          getNotesByIds={getNotesByIds}
-          getNewNoteId={jest.fn()}
-          noteIds={[]}
-          showAddNote={true}
-          toggleShowAddNote={jest.fn()}
-          updateNote={jest.fn()}
-        />
+        <NoteCards {...testProps} />
       </ThemeProvider>
     );
 
@@ -77,15 +74,7 @@ describe('NoteCards', () => {
   test('renders note cards', () => {
     const wrapper = mountWithIntl(
       <ThemeProvider theme={theme}>
-        <NoteCards
-          associateNote={jest.fn()}
-          getNotesByIds={getNotesByIds}
-          getNewNoteId={jest.fn()}
-          noteIds={noteIds}
-          showAddNote={true}
-          toggleShowAddNote={jest.fn()}
-          updateNote={jest.fn()}
-        />
+        <NoteCards {...props} />
       </ThemeProvider>
     );
 
@@ -102,15 +91,7 @@ describe('NoteCards', () => {
   test('it shows controls for adding notes when showAddNote is true', () => {
     const wrapper = mountWithIntl(
       <ThemeProvider theme={theme}>
-        <NoteCards
-          associateNote={jest.fn()}
-          getNotesByIds={getNotesByIds}
-          getNewNoteId={jest.fn()}
-          noteIds={noteIds}
-          showAddNote={true}
-          toggleShowAddNote={jest.fn()}
-          updateNote={jest.fn()}
-        />
+        <NoteCards {...props} />
       </ThemeProvider>
     );
 
@@ -118,17 +99,11 @@ describe('NoteCards', () => {
   });
 
   test('it does NOT show controls for adding notes when showAddNote is false', () => {
+    const testProps = { ...props, showAddNote: false };
+
     const wrapper = mountWithIntl(
       <ThemeProvider theme={theme}>
-        <NoteCards
-          associateNote={jest.fn()}
-          getNotesByIds={getNotesByIds}
-          getNewNoteId={jest.fn()}
-          noteIds={noteIds}
-          showAddNote={false}
-          toggleShowAddNote={jest.fn()}
-          updateNote={jest.fn()}
-        />
+        <NoteCards {...testProps} />
       </ThemeProvider>
     );
 
diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
index 3c8fc50e93b89..9d9055e3ad748 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx
@@ -12,6 +12,7 @@ import { Note } from '../../../../common/lib/note';
 import { AddNote } from '../add_note';
 import { AssociateNote, GetNewNoteId, UpdateNote } from '../helpers';
 import { NoteCard } from '../note_card';
+import { TimelineStatusLiteral } from '../../../../../common/types/timeline';
 
 const AddNoteContainer = styled.div``;
 AddNoteContainer.displayName = 'AddNoteContainer';
@@ -49,6 +50,7 @@ interface Props {
   getNewNoteId: GetNewNoteId;
   noteIds: string[];
   showAddNote: boolean;
+  status: TimelineStatusLiteral;
   toggleShowAddNote: () => void;
   updateNote: UpdateNote;
 }
@@ -61,6 +63,7 @@ export const NoteCards = React.memo<Props>(
     getNewNoteId,
     noteIds,
     showAddNote,
+    status,
     toggleShowAddNote,
     updateNote,
   }) => {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx
index 4d45b74e9b1b4..15c078e175355 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx
@@ -6,7 +6,9 @@
 
 import { EuiContextMenuPanel, EuiContextMenuItem, EuiBasicTable } from '@elastic/eui';
 import React, { useCallback, useMemo } from 'react';
-import { isEmpty } from 'lodash/fp';
+
+import { TimelineStatus } from '../../../../common/types/timeline';
+
 import * as i18n from './translations';
 import { DeleteTimelines, OpenTimelineResult } from './types';
 import { EditTimelineActions } from './export_timeline';
@@ -63,7 +65,7 @@ export const useEditTimelineBatchActions = ({
 
   const getBatchItemsPopoverContent = useCallback(
     (closePopover: () => void) => {
-      const isDisabled = isEmpty(selectedItems);
+      const disabled = selectedItems?.some((item) => item.status === TimelineStatus.immutable);
       return (
         <>
           <EditTimelineActions
@@ -82,7 +84,7 @@ export const useEditTimelineBatchActions = ({
           <EuiContextMenuPanel
             items={[
               <EuiContextMenuItem
-                disabled={isDisabled}
+                disabled={disabled}
                 icon="exportAction"
                 key="ExportItemKey"
                 onClick={handleEnableExportTimelineDownloader}
@@ -90,7 +92,7 @@ export const useEditTimelineBatchActions = ({
                 {i18n.EXPORT_SELECTED}
               </EuiContextMenuItem>,
               <EuiContextMenuItem
-                disabled={isDisabled}
+                disabled={disabled}
                 icon="trash"
                 key="DeleteItemKey"
                 onClick={handleOnOpenDeleteTimelineModal}
@@ -102,6 +104,7 @@ export const useEditTimelineBatchActions = ({
         </>
       );
     },
+    // eslint-disable-next-line react-hooks/exhaustive-deps
     [
       deleteTimelines,
       isEnableDownloader,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx
index d377b10a55c21..b8a7cfd59d222 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx
@@ -8,7 +8,6 @@ import React from 'react';
 import { TimelineDownloader } from './export_timeline';
 import { mockSelectedTimeline } from './mocks';
 import { ReactWrapper, mount } from 'enzyme';
-import { useExportTimeline } from '.';
 
 jest.mock('../translations', () => {
   return {
@@ -32,19 +31,6 @@ describe('TimelineDownloader', () => {
     onComplete: jest.fn(),
   };
   describe('should not render a downloader', () => {
-    beforeAll(() => {
-      ((useExportTimeline as unknown) as jest.Mock).mockReturnValue({
-        enableDownloader: false,
-        setEnableDownloader: jest.fn(),
-        exportedIds: {},
-        getExportedData: jest.fn(),
-      });
-    });
-
-    afterAll(() => {
-      ((useExportTimeline as unknown) as jest.Mock).mockReset();
-    });
-
     test('Without exportedIds', () => {
       const testProps = {
         ...defaultTestProps,
@@ -65,19 +51,6 @@ describe('TimelineDownloader', () => {
   });
 
   describe('should render a downloader', () => {
-    beforeAll(() => {
-      ((useExportTimeline as unknown) as jest.Mock).mockReturnValue({
-        enableDownloader: false,
-        setEnableDownloader: jest.fn(),
-        exportedIds: {},
-        getExportedData: jest.fn(),
-      });
-    });
-
-    afterAll(() => {
-      ((useExportTimeline as unknown) as jest.Mock).mockReset();
-    });
-
     test('With selectedItems and exportedIds is given and isEnableDownloader is true', () => {
       const testProps = {
         ...defaultTestProps,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.test.tsx
index 674cd6dad5f76..72f149174253a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.test.tsx
@@ -5,31 +5,41 @@
  */
 
 import React from 'react';
-import { mount } from 'enzyme';
-import { useExportTimeline, ExportTimeline } from '.';
+import { shallow } from 'enzyme';
+import { EditTimelineActionsComponent } from '.';
 
-describe('useExportTimeline', () => {
-  describe('call with selected timelines', () => {
-    let exportTimelineRes: ExportTimeline;
-    const TestHook = () => {
-      exportTimelineRes = useExportTimeline();
-      return <div />;
+describe('EditTimelineActionsComponent', () => {
+  describe('render', () => {
+    const props = {
+      deleteTimelines: jest.fn(),
+      ids: ['id1'],
+      isEnableDownloader: false,
+      isDeleteTimelineModalOpen: false,
+      onComplete: jest.fn(),
+      title: 'mockTitle',
     };
 
-    beforeAll(() => {
-      mount(<TestHook />);
-    });
+    test('should render timelineDownloader', () => {
+      const wrapper = shallow(<EditTimelineActionsComponent {...props} />);
 
-    test('Downloader should be disabled by default', () => {
-      expect(exportTimelineRes.isEnableDownloader).toBeFalsy();
+      expect(wrapper.find('[data-test-subj="TimelineDownloader"]').exists()).toBeTruthy();
     });
 
-    test('Should include disableExportTimelineDownloader in return value', () => {
-      expect(exportTimelineRes).toHaveProperty('disableExportTimelineDownloader');
+    test('Should render DeleteTimelineModalOverlay if deleteTimelines is given', () => {
+      const wrapper = shallow(<EditTimelineActionsComponent {...props} />);
+
+      expect(wrapper.find('[data-test-subj="DeleteTimelineModalOverlay"]').exists()).toBeTruthy();
     });
 
-    test('Should include enableExportTimelineDownloader in return value', () => {
-      expect(exportTimelineRes).toHaveProperty('enableExportTimelineDownloader');
+    test('Should not render DeleteTimelineModalOverlay if deleteTimelines is not given', () => {
+      const newProps = {
+        ...props,
+        deleteTimelines: undefined,
+      };
+      const wrapper = shallow(<EditTimelineActionsComponent {...newProps} />);
+      expect(
+        wrapper.find('[data-test-subj="DeleteTimelineModalOverlay"]').exists()
+      ).not.toBeTruthy();
     });
   });
 });
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.tsx
index 7bac3229c8173..2ad4aa9d208cb 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/export_timeline/index.tsx
@@ -4,7 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import React, { useState, useCallback } from 'react';
+import React from 'react';
 import { DeleteTimelines } from '../types';
 
 import { TimelineDownloader } from './export_timeline';
@@ -17,25 +17,7 @@ export interface ExportTimeline {
   isEnableDownloader: boolean;
 }
 
-export const useExportTimeline = (): ExportTimeline => {
-  const [isEnableDownloader, setIsEnableDownloader] = useState(false);
-
-  const enableExportTimelineDownloader = useCallback(() => {
-    setIsEnableDownloader(true);
-  }, []);
-
-  const disableExportTimelineDownloader = useCallback(() => {
-    setIsEnableDownloader(false);
-  }, []);
-
-  return {
-    disableExportTimelineDownloader,
-    enableExportTimelineDownloader,
-    isEnableDownloader,
-  };
-};
-
-const EditTimelineActionsComponent: React.FC<{
+export const EditTimelineActionsComponent: React.FC<{
   deleteTimelines: DeleteTimelines | undefined;
   ids: string[];
   isEnableDownloader: boolean;
@@ -52,6 +34,7 @@ const EditTimelineActionsComponent: React.FC<{
 }) => (
   <>
     <TimelineDownloader
+      data-test-subj="TimelineDownloader"
       exportedIds={ids}
       getExportedData={exportSelectedTimeline}
       isEnableDownloader={isEnableDownloader}
@@ -59,6 +42,7 @@ const EditTimelineActionsComponent: React.FC<{
     />
     {deleteTimelines != null && (
       <DeleteTimelineModalOverlay
+        data-test-subj="DeleteTimelineModalOverlay"
         deleteTimelines={deleteTimelines}
         isModalOpen={isDeleteTimelineModalOpen}
         onComplete={onComplete}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
index 520215cde4862..e841718c8119b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts
@@ -10,7 +10,17 @@ import { Action } from 'typescript-fsa';
 import uuid from 'uuid';
 import { Dispatch } from 'redux';
 import { oneTimelineQuery } from '../../containers/one/index.gql_query';
-import { TimelineResult, GetOneTimeline, NoteResult } from '../../../graphql/types';
+import {
+  TimelineResult,
+  GetOneTimeline,
+  NoteResult,
+  FilterTimelineResult,
+  ColumnHeaderResult,
+  PinnedEvent,
+} from '../../../graphql/types';
+
+import { TimelineStatus, TimelineType } from '../../../../common/types/timeline';
+
 import {
   addNotes as dispatchAddNotes,
   updateNote as dispatchUpdateNote,
@@ -22,9 +32,9 @@ import {
   addTimeline as dispatchAddTimeline,
   addNote as dispatchAddGlobalTimelineNote,
 } from '../../../timelines/store/timeline/actions';
-
 import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
 import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
+
 import {
   defaultColumnHeaderType,
   defaultHeaders,
@@ -77,103 +87,115 @@ const parseString = (params: string) => {
   }
 };
 
+const setTimelineColumn = (col: ColumnHeaderResult) => {
+  const timelineCols: ColumnHeaderOptions = {
+    ...col,
+    columnHeaderType: defaultColumnHeaderType,
+    id: col.id != null ? col.id : 'unknown',
+    placeholder: col.placeholder != null ? col.placeholder : undefined,
+    category: col.category != null ? col.category : undefined,
+    description: col.description != null ? col.description : undefined,
+    example: col.example != null ? col.example : undefined,
+    type: col.type != null ? col.type : undefined,
+    aggregatable: col.aggregatable != null ? col.aggregatable : undefined,
+    width: col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH,
+  };
+  return timelineCols;
+};
+
+const setTimelineFilters = (filter: FilterTimelineResult) => ({
+  $state: {
+    store: 'appState',
+  },
+  meta: {
+    ...filter.meta,
+    ...(filter.meta && filter.meta.field != null ? { params: parseString(filter.meta.field) } : {}),
+    ...(filter.meta && filter.meta.params != null
+      ? { params: parseString(filter.meta.params) }
+      : {}),
+    ...(filter.meta && filter.meta.value != null ? { value: parseString(filter.meta.value) } : {}),
+  },
+  ...(filter.exists != null ? { exists: parseString(filter.exists) } : {}),
+  ...(filter.match_all != null ? { exists: parseString(filter.match_all) } : {}),
+  ...(filter.missing != null ? { exists: parseString(filter.missing) } : {}),
+  ...(filter.query != null ? { query: parseString(filter.query) } : {}),
+  ...(filter.range != null ? { range: parseString(filter.range) } : {}),
+  ...(filter.script != null ? { exists: parseString(filter.script) } : {}),
+});
+
+const setEventIdToNoteIds = (
+  duplicate: boolean,
+  eventIdToNoteIds: NoteResult[] | null | undefined
+) =>
+  duplicate
+    ? {}
+    : eventIdToNoteIds != null
+    ? eventIdToNoteIds.reduce((acc, note) => {
+        if (note.eventId != null) {
+          const eventNotes = getOr([], note.eventId, acc);
+          return { ...acc, [note.eventId]: [...eventNotes, note.noteId] };
+        }
+        return acc;
+      }, {})
+    : {};
+
+const setPinnedEventsSaveObject = (
+  duplicate: boolean,
+  pinnedEventsSaveObject: PinnedEvent[] | null | undefined
+) =>
+  duplicate
+    ? {}
+    : pinnedEventsSaveObject != null
+    ? pinnedEventsSaveObject.reduce(
+        (acc, pinnedEvent) => ({
+          ...acc,
+          ...(pinnedEvent.eventId != null ? { [pinnedEvent.eventId]: pinnedEvent } : {}),
+        }),
+        {}
+      )
+    : {};
+
+const setPinnedEventIds = (duplicate: boolean, pinnedEventIds: string[] | null | undefined) =>
+  duplicate
+    ? {}
+    : pinnedEventIds != null
+    ? pinnedEventIds.reduce((acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), {})
+    : {};
+
+// eslint-disable-next-line complexity
 export const defaultTimelineToTimelineModel = (
   timeline: TimelineResult,
   duplicate: boolean
 ): TimelineModel => {
-  return Object.entries({
+  const isTemplate = timeline.timelineType === TimelineType.template;
+  const timelineEntries = {
     ...timeline,
-    columns:
-      timeline.columns != null
-        ? timeline.columns.map((col) => {
-            const timelineCols: ColumnHeaderOptions = {
-              ...col,
-              columnHeaderType: defaultColumnHeaderType,
-              id: col.id != null ? col.id : 'unknown',
-              placeholder: col.placeholder != null ? col.placeholder : undefined,
-              category: col.category != null ? col.category : undefined,
-              description: col.description != null ? col.description : undefined,
-              example: col.example != null ? col.example : undefined,
-              type: col.type != null ? col.type : undefined,
-              aggregatable: col.aggregatable != null ? col.aggregatable : undefined,
-              width:
-                col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH,
-            };
-            return timelineCols;
-          })
-        : defaultHeaders,
-    eventIdToNoteIds: duplicate
-      ? {}
-      : timeline.eventIdToNoteIds != null
-      ? timeline.eventIdToNoteIds.reduce((acc, note) => {
-          if (note.eventId != null) {
-            const eventNotes = getOr([], note.eventId, acc);
-            return { ...acc, [note.eventId]: [...eventNotes, note.noteId] };
-          }
-          return acc;
-        }, {})
-      : {},
-    filters:
-      timeline.filters != null
-        ? timeline.filters.map((filter) => ({
-            $state: {
-              store: 'appState',
-            },
-            meta: {
-              ...filter.meta,
-              ...(filter.meta && filter.meta.field != null
-                ? { params: parseString(filter.meta.field) }
-                : {}),
-              ...(filter.meta && filter.meta.params != null
-                ? { params: parseString(filter.meta.params) }
-                : {}),
-              ...(filter.meta && filter.meta.value != null
-                ? { value: parseString(filter.meta.value) }
-                : {}),
-            },
-            ...(filter.exists != null ? { exists: parseString(filter.exists) } : {}),
-            ...(filter.match_all != null ? { exists: parseString(filter.match_all) } : {}),
-            ...(filter.missing != null ? { exists: parseString(filter.missing) } : {}),
-            ...(filter.query != null ? { query: parseString(filter.query) } : {}),
-            ...(filter.range != null ? { range: parseString(filter.range) } : {}),
-            ...(filter.script != null ? { exists: parseString(filter.script) } : {}),
-          }))
-        : [],
+    columns: timeline.columns != null ? timeline.columns.map(setTimelineColumn) : defaultHeaders,
+    eventIdToNoteIds: setEventIdToNoteIds(duplicate, timeline.eventIdToNoteIds),
+    filters: timeline.filters != null ? timeline.filters.map(setTimelineFilters) : [],
     isFavorite: duplicate
       ? false
       : timeline.favorite != null
       ? timeline.favorite.length > 0
       : false,
     noteIds: duplicate ? [] : timeline.noteIds != null ? timeline.noteIds : [],
-    pinnedEventIds: duplicate
-      ? {}
-      : timeline.pinnedEventIds != null
-      ? timeline.pinnedEventIds.reduce(
-          (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }),
-          {}
-        )
-      : {},
-    pinnedEventsSaveObject: duplicate
-      ? {}
-      : timeline.pinnedEventsSaveObject != null
-      ? timeline.pinnedEventsSaveObject.reduce(
-          (acc, pinnedEvent) => ({
-            ...acc,
-            ...(pinnedEvent.eventId != null ? { [pinnedEvent.eventId]: pinnedEvent } : {}),
-          }),
-          {}
-        )
-      : {},
+    pinnedEventIds: setPinnedEventIds(duplicate, timeline.pinnedEventIds),
+    pinnedEventsSaveObject: setPinnedEventsSaveObject(duplicate, timeline.pinnedEventsSaveObject),
     id: duplicate ? '' : timeline.savedObjectId,
+    status: duplicate ? TimelineStatus.active : timeline.status,
     savedObjectId: duplicate ? null : timeline.savedObjectId,
     version: duplicate ? null : timeline.version,
-    title: duplicate ? '' : timeline.title || '',
-    templateTimelineId: duplicate ? null : timeline.templateTimelineId,
-    templateTimelineVersion: duplicate ? null : timeline.templateTimelineVersion,
-  }).reduce((acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc), {
-    ...timelineDefaults,
-    id: '',
-  });
+    title: duplicate ? `${timeline.title} - Duplicate` : timeline.title || '',
+    templateTimelineId: duplicate && isTemplate ? uuid.v4() : timeline.templateTimelineId,
+    templateTimelineVersion: duplicate && isTemplate ? 1 : timeline.templateTimelineVersion,
+  };
+  return Object.entries(timelineEntries).reduce(
+    (acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc),
+    {
+      ...timelineDefaults,
+      id: '',
+    }
+  );
 };
 
 export const formatTimelineResultToModel = (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
index 24dee1460810f..ea63f2b7b0710 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx
@@ -9,9 +9,9 @@ import React, { useEffect, useState, useCallback } from 'react';
 import { connect, ConnectedProps } from 'react-redux';
 
 import { Dispatch } from 'redux';
-import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
-import { deleteTimelineMutation } from '../../containers/delete/persist.gql_query';
-import { useGetAllTimeline } from '../../containers/all';
+
+import { disableTemplate } from '../../../../common/constants';
+
 import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types';
 import { State } from '../../../common/store';
 import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model';
@@ -21,6 +21,12 @@ import {
   createTimeline as dispatchCreateNewTimeline,
   updateIsLoading as dispatchUpdateIsLoading,
 } from '../../../timelines/store/timeline/actions';
+
+import { deleteTimelineMutation } from '../../containers/delete/persist.gql_query';
+import { useGetAllTimeline } from '../../containers/all';
+
+import { defaultHeaders } from '../timeline/body/column_headers/default_headers';
+
 import { OpenTimeline } from './open_timeline';
 import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers';
 import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body';
@@ -42,7 +48,7 @@ import {
 } from './types';
 import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants';
 import { useTimelineTypes } from './use_timeline_types';
-import { disableTemplate } from '../../../../common/constants';
+import { useTimelineStatus } from './use_timeline_status';
 
 interface OwnProps<TCache = object> {
   apolloClient: ApolloClient<TCache>;
@@ -106,28 +112,54 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
     /** The requested field to sort on */
     const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD);
 
-    const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes();
-    const { fetchAllTimeline, timelines, loading, totalCount } = useGetAllTimeline();
-
-    const refetch = useCallback(
-      () =>
-        fetchAllTimeline({
-          pageInfo: {
-            pageIndex: pageIndex + 1,
-            pageSize,
-          },
-          search,
-          sort: {
-            sortField: sortField as SortFieldTimeline,
-            sortOrder: sortDirection as Direction,
-          },
-          onlyUserFavorite: onlyFavorites,
-          timelineType,
-        }),
-
-      // eslint-disable-next-line react-hooks/exhaustive-deps
-      [pageIndex, pageSize, search, sortField, sortDirection, timelineType, onlyFavorites]
-    );
+    const {
+      customTemplateTimelineCount,
+      defaultTimelineCount,
+      elasticTemplateTimelineCount,
+      favoriteCount,
+      fetchAllTimeline,
+      timelines,
+      loading,
+      totalCount,
+      templateTimelineCount,
+    } = useGetAllTimeline();
+    const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes({
+      defaultTimelineCount,
+      templateTimelineCount,
+    });
+    const { timelineStatus, templateTimelineType, templateTimelineFilter } = useTimelineStatus({
+      timelineType,
+      customTemplateTimelineCount,
+      elasticTemplateTimelineCount,
+    });
+    const refetch = useCallback(() => {
+      fetchAllTimeline({
+        pageInfo: {
+          pageIndex: pageIndex + 1,
+          pageSize,
+        },
+        search,
+        sort: {
+          sortField: sortField as SortFieldTimeline,
+          sortOrder: sortDirection as Direction,
+        },
+        onlyUserFavorite: onlyFavorites,
+        timelineType,
+        templateTimelineType,
+        status: timelineStatus,
+      });
+    }, [
+      fetchAllTimeline,
+      pageIndex,
+      pageSize,
+      search,
+      sortField,
+      sortDirection,
+      timelineType,
+      timelineStatus,
+      templateTimelineType,
+      onlyFavorites,
+    ]);
 
     /** Invoked when the user presses enters to submit the text in the search input */
     const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => {
@@ -264,6 +296,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
         data-test-subj={'open-timeline'}
         deleteTimelines={onDeleteOneTimeline}
         defaultPageSize={defaultPageSize}
+        favoriteCount={favoriteCount}
         isLoading={loading}
         itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
         importDataModalToggle={importDataModalToggle}
@@ -285,7 +318,9 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
         selectedItems={selectedItems}
         sortDirection={sortDirection}
         sortField={sortField}
-        tabs={!disableTemplate ? timelineTabs : undefined}
+        templateTimelineFilter={!disableTemplate ? templateTimelineFilter : null}
+        timelineType={timelineType}
+        timelineFilter={!disableTemplate ? timelineTabs : null}
         title={title}
         totalSearchResultsCount={totalCount}
       />
@@ -294,6 +329,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
         data-test-subj={'open-timeline-modal'}
         deleteTimelines={onDeleteOneTimeline}
         defaultPageSize={defaultPageSize}
+        favoriteCount={favoriteCount}
         hideActions={hideActions}
         isLoading={loading}
         itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap}
@@ -312,7 +348,9 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>(
         selectedItems={selectedItems}
         sortDirection={sortDirection}
         sortField={sortField}
-        tabs={!disableTemplate ? timelineFilters : undefined}
+        templateTimelineFilter={!disableTemplate ? templateTimelineFilter : null}
+        timelineType={timelineType}
+        timelineFilter={!disableTemplate ? timelineFilters : null}
         title={title}
         totalSearchResultsCount={totalCount}
       />
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx
index a331c62ec4754..f42914c86f46b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx
@@ -16,6 +16,7 @@ import { TimelinesTableProps } from './timelines_table';
 import { mockTimelineResults } from '../../../common/mock/timeline_results';
 import { OpenTimeline } from './open_timeline';
 import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants';
+import { TimelineType } from '../../../../common/types/timeline';
 
 jest.mock('../../../common/lib/kibana');
 
@@ -46,8 +47,9 @@ describe('OpenTimeline', () => {
     selectedItems: [],
     sortDirection: DEFAULT_SORT_DIRECTION,
     sortField: DEFAULT_SORT_FIELD,
-    tabs: <div />,
     title,
+    timelineType: TimelineType.default,
+    templateTimelineFilter: [<div />],
     totalSearchResultsCount: mockSearchResults.length,
   });
 
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
index 4894b1b2577a9..849143894efe0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx
@@ -4,17 +4,11 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import { EuiPanel, EuiBasicTable } from '@elastic/eui';
+import { EuiPanel, EuiBasicTable, EuiCallOut, EuiSpacer } from '@elastic/eui';
 import React, { useCallback, useMemo, useRef } from 'react';
 import { FormattedMessage } from '@kbn/i18n/react';
-import { OPEN_TIMELINE_CLASS_NAME } from './helpers';
-import { OpenTimelineProps, OpenTimelineResult, ActionTimelineToShow } from './types';
-import { SearchRow } from './search_row';
-import { TimelinesTable } from './timelines_table';
-import { ImportDataModal } from '../../../common/components/import_data_modal';
-import * as i18n from './translations';
-import { importTimelines } from '../../containers/api';
 
+import { ImportDataModal } from '../../../common/components/import_data_modal';
 import {
   UtilityBarGroup,
   UtilityBarText,
@@ -22,14 +16,23 @@ import {
   UtilityBarSection,
   UtilityBarAction,
 } from '../../../common/components/utility_bar';
+
+import { importTimelines } from '../../containers/api';
+
 import { useEditTimelineBatchActions } from './edit_timeline_batch_actions';
 import { useEditTimelineActions } from './edit_timeline_actions';
 import { EditOneTimelineAction } from './export_timeline';
+import { SearchRow } from './search_row';
+import { TimelinesTable } from './timelines_table';
+import * as i18n from './translations';
+import { OPEN_TIMELINE_CLASS_NAME } from './helpers';
+import { OpenTimelineProps, OpenTimelineResult, ActionTimelineToShow } from './types';
 
 export const OpenTimeline = React.memo<OpenTimelineProps>(
   ({
     deleteTimelines,
     defaultPageSize,
+    favoriteCount,
     isLoading,
     itemIdToExpandedNotesRowMap,
     importDataModalToggle,
@@ -51,11 +54,12 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
     sortDirection,
     setImportDataModalToggle,
     sortField,
-    tabs,
+    timelineType,
+    timelineFilter,
+    templateTimelineFilter,
     totalSearchResultsCount,
   }) => {
     const tableRef = useRef<EuiBasicTable<OpenTimelineResult>>();
-
     const {
       actionItem,
       enableExportTimelineDownloader,
@@ -124,6 +128,8 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
       [onDeleteSelected, deleteTimelines]
     );
 
+    const SearchRowContent = useMemo(() => <>{templateTimelineFilter}</>, [templateTimelineFilter]);
+
     return (
       <>
         <EditOneTimelineAction
@@ -151,15 +157,20 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
         />
 
         <EuiPanel className={OPEN_TIMELINE_CLASS_NAME}>
-          {!!tabs && tabs}
+          <EuiCallOut size="s" title={i18n.TEMPLATE_CALL_OUT_MESSAGE} />
+          <EuiSpacer size="m" />
+          {!!timelineFilter && timelineFilter}
           <SearchRow
             data-test-subj="search-row"
+            favoriteCount={favoriteCount}
             onlyFavorites={onlyFavorites}
             onQueryChange={onQueryChange}
             onToggleOnlyFavorites={onToggleOnlyFavorites}
             query={query}
             totalSearchResultsCount={totalSearchResultsCount}
-          />
+          >
+            {SearchRowContent}
+          </SearchRow>
 
           <UtilityBar border>
             <UtilityBarSection>
@@ -206,6 +217,7 @@ export const OpenTimeline = React.memo<OpenTimelineProps>(
             showExtendedColumns={true}
             sortDirection={sortDirection}
             sortField={sortField}
+            timelineType={timelineType}
             tableRef={tableRef}
             totalSearchResultsCount={totalSearchResultsCount}
           />
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx
index 42a3f9a44d4b6..1d08f0296ce0d 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx
@@ -16,6 +16,7 @@ import { TimelinesTableProps } from '../timelines_table';
 import { mockTimelineResults } from '../../../../common/mock/timeline_results';
 import { OpenTimelineModalBody } from './open_timeline_modal_body';
 import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
+import { TimelineType } from '../../../../../common/types/timeline';
 
 jest.mock('../../../../common/lib/kibana');
 
@@ -45,7 +46,8 @@ describe('OpenTimelineModal', () => {
     selectedItems: [],
     sortDirection: DEFAULT_SORT_DIRECTION,
     sortField: DEFAULT_SORT_FIELD,
-    tabs: <div />,
+    timelineType: TimelineType.default,
+    templateTimelineFilter: [<div />],
     title,
     totalSearchResultsCount: mockSearchResults.length,
   });
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx
index 9eab64d6fcf52..bf66d9a52ff2f 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx
@@ -23,6 +23,7 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
   ({
     deleteTimelines,
     defaultPageSize,
+    favoriteCount,
     hideActions = [],
     isLoading,
     itemIdToExpandedNotesRowMap,
@@ -42,7 +43,9 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
     selectedItems,
     sortDirection,
     sortField,
-    tabs,
+    timelineFilter,
+    timelineType,
+    templateTimelineFilter,
     title,
     totalSearchResultsCount,
   }) => {
@@ -54,6 +57,16 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
       return actions.filter((action) => !hideActions.includes(action));
     }, [onDeleteSelected, deleteTimelines, hideActions]);
 
+    const SearchRowContent = useMemo(
+      () => (
+        <>
+          {!!timelineFilter && timelineFilter}
+          {!!templateTimelineFilter && templateTimelineFilter}
+        </>
+      ),
+      [timelineFilter, templateTimelineFilter]
+    );
+
     return (
       <>
         <EuiModalHeader>
@@ -67,13 +80,15 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
             <>
               <SearchRow
                 data-test-subj="search-row"
+                favoriteCount={favoriteCount}
                 onlyFavorites={onlyFavorites}
                 onQueryChange={onQueryChange}
                 onToggleOnlyFavorites={onToggleOnlyFavorites}
                 query={query}
-                tabs={tabs}
                 totalSearchResultsCount={totalSearchResultsCount}
-              />
+              >
+                {SearchRowContent}
+              </SearchRow>
             </>
           </HeaderContainer>
         </EuiModalHeader>
@@ -96,6 +111,7 @@ export const OpenTimelineModalBody = memo<OpenTimelineProps>(
             showExtendedColumns={false}
             sortDirection={sortDirection}
             sortField={sortField}
+            timelineType={timelineType}
             totalSearchResultsCount={totalSearchResultsCount}
           />
         </EuiModalBody>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx
index 557649aa3aa43..6f9178664ccf0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/search_row/index.tsx
@@ -34,8 +34,13 @@ SearchRowFlexGroup.displayName = 'SearchRowFlexGroup';
 
 type Props = Pick<
   OpenTimelineProps,
-  'onlyFavorites' | 'onQueryChange' | 'onToggleOnlyFavorites' | 'query' | 'totalSearchResultsCount'
-> & { tabs?: JSX.Element };
+  | 'favoriteCount'
+  | 'onlyFavorites'
+  | 'onQueryChange'
+  | 'onToggleOnlyFavorites'
+  | 'query'
+  | 'totalSearchResultsCount'
+> & { children?: JSX.Element | null };
 
 const searchBox = {
   placeholder: i18n.SEARCH_PLACEHOLDER,
@@ -47,12 +52,13 @@ const searchBox = {
  */
 export const SearchRow = React.memo<Props>(
   ({
+    favoriteCount,
     onlyFavorites,
     onQueryChange,
     onToggleOnlyFavorites,
     query,
     totalSearchResultsCount,
-    tabs,
+    children,
   }) => {
     return (
       <SearchRowContainer>
@@ -68,10 +74,11 @@ export const SearchRow = React.memo<Props>(
                   data-test-subj="only-favorites-toggle"
                   hasActiveFilters={onlyFavorites}
                   onClick={onToggleOnlyFavorites}
+                  numFilters={favoriteCount ?? undefined}
                 >
                   {i18n.ONLY_FAVORITES}
                 </EuiFilterButton>
-                {tabs}
+                {!!children && children}
               </>
             </EuiFilterGroup>
           </EuiFlexItem>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx
index c92e241c0fe79..5b8eb8fd0365c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx
@@ -16,6 +16,7 @@ import {
   TimelineActionsOverflowColumns,
 } from '../types';
 import * as i18n from '../translations';
+import { TimelineStatus } from '../../../../../common/types/timeline';
 
 /**
  * Returns the action columns (e.g. delete, open duplicate timeline)
@@ -54,7 +55,9 @@ export const getActionsColumns = ({
     onClick: (selectedTimeline: OpenTimelineResult) => {
       if (enableExportTimelineDownloader != null) enableExportTimelineDownloader(selectedTimeline);
     },
-    enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
+    enabled: (timeline: OpenTimelineResult) => {
+      return timeline.savedObjectId != null && timeline.status !== TimelineStatus.immutable;
+    },
     description: i18n.EXPORT_SELECTED,
     'data-test-subj': 'export-timeline',
   };
@@ -65,7 +68,8 @@ export const getActionsColumns = ({
     onClick: (selectedTimeline: OpenTimelineResult) => {
       if (onOpenDeleteTimelineModal != null) onOpenDeleteTimelineModal(selectedTimeline);
     },
-    enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null,
+    enabled: ({ savedObjectId, status }: OpenTimelineResult) =>
+      savedObjectId != null && status !== TimelineStatus.immutable,
     description: i18n.DELETE_SELECTED,
     'data-test-subj': 'delete-timeline',
   };
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.tsx
index 5b0f3ded7d71b..e07c6b6b46149 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.tsx
@@ -13,55 +13,68 @@ import { ACTION_COLUMN_WIDTH } from './common_styles';
 import { getNotesCount, getPinnedEventCount } from '../helpers';
 import * as i18n from '../translations';
 import { FavoriteTimelineResult, OpenTimelineResult } from '../types';
+import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../../common/types/timeline';
 
 /**
  * Returns the columns that have icon headers
  */
-export const getIconHeaderColumns = () => [
-  {
-    align: 'center',
-    field: 'pinnedEventIds',
-    name: (
-      <EuiToolTip content={i18n.PINNED_EVENTS}>
-        <EuiIcon data-test-subj="pinned-event-header-icon" size="m" type="pin" />
-      </EuiToolTip>
-    ),
-    render: (_: Record<string, boolean> | null | undefined, timelineResult: OpenTimelineResult) => (
-      <span data-test-subj="pinned-event-count">{`${getPinnedEventCount(timelineResult)}`}</span>
-    ),
-    sortable: false,
-    width: ACTION_COLUMN_WIDTH,
-  },
-  {
-    align: 'center',
-    field: 'eventIdToNoteIds',
-    name: (
-      <EuiToolTip content={i18n.NOTES}>
-        <EuiIcon data-test-subj="notes-count-header-icon" size="m" type="editorComment" />
-      </EuiToolTip>
-    ),
-    render: (
-      _: Record<string, string[]> | null | undefined,
-      timelineResult: OpenTimelineResult
-    ) => <span data-test-subj="notes-count">{getNotesCount(timelineResult)}</span>,
-    sortable: false,
-    width: ACTION_COLUMN_WIDTH,
-  },
-  {
-    align: 'center',
-    field: 'favorite',
-    name: (
-      <EuiToolTip content={i18n.FAVORITES}>
-        <EuiIcon data-test-subj="favorites-header-icon" size="m" type="starEmpty" />
-      </EuiToolTip>
-    ),
-    render: (favorite: FavoriteTimelineResult[] | null | undefined) => {
-      const isFavorite = favorite != null && favorite.length > 0;
-      const fill = isFavorite ? 'starFilled' : 'starEmpty';
+export const getIconHeaderColumns = ({
+  timelineType,
+}: {
+  timelineType: TimelineTypeLiteralWithNull;
+}) => {
+  const columns = {
+    note: {
+      align: 'center',
+      field: 'eventIdToNoteIds',
+      name: (
+        <EuiToolTip content={i18n.NOTES}>
+          <EuiIcon data-test-subj="notes-count-header-icon" size="m" type="editorComment" />
+        </EuiToolTip>
+      ),
+      render: (
+        _: Record<string, string[]> | null | undefined,
+        timelineResult: OpenTimelineResult
+      ) => <span data-test-subj="notes-count">{getNotesCount(timelineResult)}</span>,
+      sortable: false,
+      width: ACTION_COLUMN_WIDTH,
+    },
+    pinnedEvent: {
+      align: 'center',
+      field: 'pinnedEventIds',
+      name: (
+        <EuiToolTip content={i18n.PINNED_EVENTS}>
+          <EuiIcon data-test-subj="pinned-event-header-icon" size="m" type="pin" />
+        </EuiToolTip>
+      ),
+      render: (
+        _: Record<string, boolean> | null | undefined,
+        timelineResult: OpenTimelineResult
+      ) => (
+        <span data-test-subj="pinned-event-count">{`${getPinnedEventCount(timelineResult)}`}</span>
+      ),
+      sortable: false,
+      width: ACTION_COLUMN_WIDTH,
+    },
+    favorite: {
+      align: 'center',
+      field: 'favorite',
+      name: (
+        <EuiToolTip content={i18n.FAVORITES}>
+          <EuiIcon data-test-subj="favorites-header-icon" size="m" type="starEmpty" />
+        </EuiToolTip>
+      ),
+      render: (favorite: FavoriteTimelineResult[] | null | undefined) => {
+        const isFavorite = favorite != null && favorite.length > 0;
+        const fill = isFavorite ? 'starFilled' : 'starEmpty';
 
-      return <EuiIcon data-test-subj={`favorite-${fill}-star`} type={fill} size="m" />;
+        return <EuiIcon data-test-subj={`favorite-${fill}-star`} type={fill} size="m" />;
+      },
+      sortable: false,
+      width: ACTION_COLUMN_WIDTH,
     },
-    sortable: false,
-    width: ACTION_COLUMN_WIDTH,
-  },
-];
+  };
+  const templateColumns = [columns.note, columns.favorite];
+  const defaultColumns = [columns.pinnedEvent, columns.note, columns.favorite];
+  return timelineType === TimelineType.template ? templateColumns : defaultColumns;
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx
index 7091ef1f0a1f9..fdba3247afb38 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.tsx
@@ -24,6 +24,7 @@ import { getActionsColumns } from './actions_columns';
 import { getCommonColumns } from './common_columns';
 import { getExtendedColumns } from './extended_columns';
 import { getIconHeaderColumns } from './icon_header_columns';
+import { TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline';
 
 // there are a number of type mismatches across this file
 const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any
@@ -58,6 +59,7 @@ export const getTimelinesTableColumns = ({
   onOpenTimeline,
   onToggleShowNotes,
   showExtendedColumns,
+  timelineType,
 }: {
   actionTimelineToShow: ActionTimelineToShow[];
   deleteTimelines?: DeleteTimelines;
@@ -68,6 +70,7 @@ export const getTimelinesTableColumns = ({
   onSelectionChange: OnSelectionChange;
   onToggleShowNotes: OnToggleShowNotes;
   showExtendedColumns: boolean;
+  timelineType: TimelineTypeLiteralWithNull;
 }) => {
   return [
     ...getCommonColumns({
@@ -76,7 +79,7 @@ export const getTimelinesTableColumns = ({
       onToggleShowNotes,
     }),
     ...getExtendedColumnsIfEnabled(showExtendedColumns),
-    ...getIconHeaderColumns(),
+    ...getIconHeaderColumns({ timelineType }),
     ...getActionsColumns({
       actionTimelineToShow,
       deleteTimelines,
@@ -105,6 +108,7 @@ export interface TimelinesTableProps {
   showExtendedColumns: boolean;
   sortDirection: 'asc' | 'desc';
   sortField: string;
+  timelineType: TimelineTypeLiteralWithNull;
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   tableRef?: React.MutableRefObject<_EuiBasicTable<any> | undefined>;
   totalSearchResultsCount: number;
@@ -134,6 +138,7 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
     sortField,
     sortDirection,
     tableRef,
+    timelineType,
     totalSearchResultsCount,
   }) => {
     const pagination = {
@@ -174,6 +179,7 @@ export const TimelinesTable = React.memo<TimelinesTableProps>(
           onSelectionChange,
           onToggleShowNotes,
           showExtendedColumns,
+          timelineType,
         })}
         compressed
         data-test-subj="timelines-table"
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/mocks.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/mocks.ts
index 78ca898cc407e..0770f460794a6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/mocks.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/mocks.ts
@@ -7,6 +7,7 @@ import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines_page';
 import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants';
 import { OpenTimelineResult } from '../types';
 import { TimelinesTableProps } from '.';
+import { TimelineType } from '../../../../../common/types/timeline';
 
 export const getMockTimelinesTableProps = (
   mockOpenTimelineResults: OpenTimelineResult[]
@@ -28,5 +29,6 @@ export const getMockTimelinesTableProps = (
   showExtendedColumns: true,
   sortDirection: DEFAULT_SORT_DIRECTION,
   sortField: DEFAULT_SORT_FIELD,
+  timelineType: TimelineType.default,
   totalSearchResultsCount: mockOpenTimelineResults.length,
 });
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
index edd77330f5084..7b07548af67ae 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts
@@ -220,6 +220,20 @@ export const TAB_TEMPLATES = i18n.translate(
   }
 );
 
+export const FILTER_ELASTIC_TIMELINES = i18n.translate(
+  'xpack.securitySolution.timelines.components.templateFilter.elasticTitle',
+  {
+    defaultMessage: 'Elastic templates',
+  }
+);
+
+export const FILTER_CUSTOM_TIMELINES = i18n.translate(
+  'xpack.securitySolution.timelines.components.templateFilter.customizedTitle',
+  {
+    defaultMessage: 'Custom templates',
+  }
+);
+
 export const IMPORT_TIMELINE_BTN_TITLE = i18n.translate(
   'xpack.securitySolution.timelines.components.importTimelineModal.importTimelineTitle',
   {
@@ -230,7 +244,7 @@ export const IMPORT_TIMELINE_BTN_TITLE = i18n.translate(
 export const SELECT_TIMELINE = i18n.translate(
   'xpack.securitySolution.timelines.components.importTimelineModal.selectTimelineDescription',
   {
-    defaultMessage: 'Select a SIEM timeline (as exported from the Timeline view) to import',
+    defaultMessage: 'Select a Security timeline (as exported from the Timeline view) to import',
   }
 );
 
@@ -280,3 +294,10 @@ export const IMPORT_FAILED_DETAILED = (id: string, statusCode: number, message:
       defaultMessage: 'Timeline ID: {id}\n Status Code: {statusCode}\n Message: {message}',
     }
   );
+
+export const TEMPLATE_CALL_OUT_MESSAGE = i18n.translate(
+  'xpack.securitySolution.timelines.components.templateCallOutMessageTitle',
+  {
+    defaultMessage: 'Now you can add timeline templates and link it to rules.',
+  }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
index e1515a3a79254..8811d5452e039 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts
@@ -8,7 +8,12 @@ import { SetStateAction, Dispatch } from 'react';
 import { AllTimelinesVariables } from '../../containers/all';
 import { TimelineModel } from '../../store/timeline/model';
 import { NoteResult } from '../../../graphql/types';
-import { TimelineTypeLiteral } from '../../../../common/types/timeline';
+import {
+  TimelineTypeLiteral,
+  TimelineTypeLiteralWithNull,
+  TimelineStatus,
+  TemplateTimelineTypeLiteral,
+} from '../../../../common/types/timeline';
 
 /** The users who added a timeline to favorites */
 export interface FavoriteTimelineResult {
@@ -46,6 +51,7 @@ export interface OpenTimelineResult {
   notes?: TimelineResultNote[] | null;
   pinnedEventIds?: Readonly<Record<string, boolean>> | null;
   savedObjectId?: string | null;
+  status?: TimelineStatus | null;
   title?: string | null;
   templateTimelineId?: string | null;
   type?: TimelineTypeLiteral;
@@ -118,6 +124,8 @@ export interface OpenTimelineProps {
   deleteTimelines?: DeleteTimelines;
   /** The default requested size of each page of search results */
   defaultPageSize: number;
+  /** The number of favorite timeline*/
+  favoriteCount?: number | null | undefined;
   /** Displays an indicator that data is loading when true */
   isLoading: boolean;
   /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */
@@ -160,8 +168,12 @@ export interface OpenTimelineProps {
   sortDirection: 'asc' | 'desc';
   /** the requested field to sort on */
   sortField: string;
+  /** this affects timeline's behaviour like editable / duplicatible */
+  timelineType: TimelineTypeLiteralWithNull;
+  /** when timelineType === template, templatetimelineFilter is a JSX.Element */
+  templateTimelineFilter: JSX.Element[] | null;
   /** timeline / template timeline */
-  tabs?: JSX.Element;
+  timelineFilter?: JSX.Element | JSX.Element[] | null;
   /** The title of the Open Timeline component  */
   title: string;
   /** The total (server-side) count of the search results */
@@ -196,9 +208,19 @@ export enum TimelineTabsStyle {
 }
 
 export interface TimelineTab {
-  id: TimelineTypeLiteral;
-  name: string;
+  count: number | undefined;
   disabled: boolean;
   href: string;
+  id: TimelineTypeLiteral;
+  name: string;
   onClick: (ev: { preventDefault: () => void }) => void;
+  withNext: boolean;
+}
+
+export interface TemplateTimelineFilter {
+  id: TemplateTimelineTypeLiteral;
+  name: string;
+  disabled: boolean;
+  withNext: boolean;
+  count: number | undefined;
 }
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx
new file mode 100644
index 0000000000000..f17f6aebaddf6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_status.tsx
@@ -0,0 +1,110 @@
+/*
+ * 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, useMemo } from 'react';
+import { EuiFilterButton } from '@elastic/eui';
+
+import {
+  TimelineStatus,
+  TimelineType,
+  TimelineTypeLiteralWithNull,
+  TemplateTimelineType,
+  TemplateTimelineTypeLiteralWithNull,
+  TimelineStatusLiteralWithNull,
+} from '../../../../common/types/timeline';
+
+import * as i18n from './translations';
+import { TemplateTimelineFilter } from './types';
+import { disableTemplate } from '../../../../common/constants';
+
+export const useTimelineStatus = ({
+  timelineType,
+  elasticTemplateTimelineCount,
+  customTemplateTimelineCount,
+}: {
+  timelineType: TimelineTypeLiteralWithNull;
+  elasticTemplateTimelineCount?: number | null;
+  customTemplateTimelineCount?: number | null;
+}): {
+  timelineStatus: TimelineStatusLiteralWithNull;
+  templateTimelineType: TemplateTimelineTypeLiteralWithNull;
+  templateTimelineFilter: JSX.Element[] | null;
+} => {
+  const [selectedTab, setSelectedTab] = useState<TemplateTimelineTypeLiteralWithNull>(
+    disableTemplate ? null : TemplateTimelineType.elastic
+  );
+  const isTemplateFilterEnabled = useMemo(() => timelineType === TimelineType.template, [
+    timelineType,
+  ]);
+
+  const templateTimelineType = useMemo(
+    () => (disableTemplate || !isTemplateFilterEnabled ? null : selectedTab),
+    [selectedTab, isTemplateFilterEnabled]
+  );
+
+  const timelineStatus = useMemo(
+    () =>
+      templateTimelineType == null
+        ? null
+        : templateTimelineType === TemplateTimelineType.elastic
+        ? TimelineStatus.immutable
+        : TimelineStatus.active,
+    [templateTimelineType]
+  );
+
+  const filters = useMemo(
+    () => [
+      {
+        id: TemplateTimelineType.elastic,
+        name: i18n.FILTER_ELASTIC_TIMELINES,
+        disabled: !isTemplateFilterEnabled,
+        withNext: true,
+        count: elasticTemplateTimelineCount ?? undefined,
+      },
+      {
+        id: TemplateTimelineType.custom,
+        name: i18n.FILTER_CUSTOM_TIMELINES,
+        disabled: !isTemplateFilterEnabled,
+        withNext: false,
+        count: customTemplateTimelineCount ?? undefined,
+      },
+    ],
+    [customTemplateTimelineCount, elasticTemplateTimelineCount, isTemplateFilterEnabled]
+  );
+
+  const onFilterClicked = useCallback(
+    (tabId) => {
+      if (selectedTab === tabId) {
+        setSelectedTab(null);
+      } else {
+        setSelectedTab(tabId);
+      }
+    },
+    [setSelectedTab, selectedTab]
+  );
+
+  const templateTimelineFilter = useMemo(() => {
+    return isTemplateFilterEnabled
+      ? filters.map((tab: TemplateTimelineFilter) => (
+          <EuiFilterButton
+            hasActiveFilters={tab.id === templateTimelineType}
+            key={`template-timeline-filter-${tab.id}`}
+            numFilters={tab.count}
+            onClick={onFilterClicked.bind(null, tab.id)}
+            withNext={tab.withNext}
+            isDisabled={tab.disabled}
+          >
+            {tab.name}
+          </EuiFilterButton>
+        ))
+      : null;
+  }, [templateTimelineType, filters, isTemplateFilterEnabled, onFilterClicked]);
+
+  return {
+    timelineStatus,
+    templateTimelineType,
+    templateTimelineFilter,
+  };
+};
diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
index 56c67b0c294a2..bee94db348872 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx
@@ -13,10 +13,16 @@ import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/lin
 import * as i18n from './translations';
 import { TimelineTabsStyle, TimelineTab } from './types';
 
-export const useTimelineTypes = (): {
+export const useTimelineTypes = ({
+  defaultTimelineCount,
+  templateTimelineCount,
+}: {
+  defaultTimelineCount?: number | null;
+  templateTimelineCount?: number | null;
+}): {
   timelineType: TimelineTypeLiteralWithNull;
   timelineTabs: JSX.Element;
-  timelineFilters: JSX.Element;
+  timelineFilters: JSX.Element[];
 } => {
   const history = useHistory();
   const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines);
@@ -40,35 +46,52 @@ export const useTimelineTypes = (): {
     },
     [history, urlSearch]
   );
-
-  const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = (
-    timelineTabsStyle: TimelineTabsStyle
-  ) => [
-    {
-      id: TimelineType.default,
-      name:
-        timelineTabsStyle === TimelineTabsStyle.filter
-          ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES)
-          : i18n.TAB_TIMELINES,
-      href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)),
-      disabled: false,
-      onClick: goToTimeline,
-    },
-    {
-      id: TimelineType.template,
-      name:
-        timelineTabsStyle === TimelineTabsStyle.filter
-          ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES)
-          : i18n.TAB_TEMPLATES,
-      href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)),
-      disabled: false,
-      onClick: goToTemplateTimeline,
-    },
-  ];
+  const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = useCallback(
+    (timelineTabsStyle: TimelineTabsStyle) => [
+      {
+        id: TimelineType.default,
+        name:
+          timelineTabsStyle === TimelineTabsStyle.filter
+            ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES)
+            : i18n.TAB_TIMELINES,
+        href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)),
+        disabled: false,
+        withNext: true,
+        count:
+          timelineTabsStyle === TimelineTabsStyle.filter
+            ? defaultTimelineCount ?? undefined
+            : undefined,
+        onClick: goToTimeline,
+      },
+      {
+        id: TimelineType.template,
+        name:
+          timelineTabsStyle === TimelineTabsStyle.filter
+            ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES)
+            : i18n.TAB_TEMPLATES,
+        href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)),
+        disabled: false,
+        withNext: false,
+        count:
+          timelineTabsStyle === TimelineTabsStyle.filter
+            ? templateTimelineCount ?? undefined
+            : undefined,
+        onClick: goToTemplateTimeline,
+      },
+    ],
+    [
+      defaultTimelineCount,
+      templateTimelineCount,
+      urlSearch,
+      formatUrl,
+      goToTimeline,
+      goToTemplateTimeline,
+    ]
+  );
 
   const onFilterClicked = useCallback(
-    (timelineTabsStyle, tabId) => {
-      if (timelineTabsStyle === TimelineTabsStyle.filter && tabId === timelineType) {
+    (tabId) => {
+      if (tabId === timelineType) {
         setTimelineTypes(null);
       } else {
         setTimelineTypes(tabId);
@@ -89,7 +112,7 @@ export const useTimelineTypes = (): {
               href={tab.href}
               onClick={(ev) => {
                 tab.onClick(ev);
-                onFilterClicked(TimelineTabsStyle.tab, tab.id);
+                onFilterClicked(tab.id);
               }}
             >
               {tab.name}
@@ -103,24 +126,21 @@ export const useTimelineTypes = (): {
   }, [tabName]);
 
   const timelineFilters = useMemo(() => {
-    return (
-      <>
-        {getFilterOrTabs(TimelineTabsStyle.tab).map((tab: TimelineTab) => (
-          <EuiFilterButton
-            hasActiveFilters={tab.id === timelineType}
-            key={`timeline-${TimelineTabsStyle.filter}-${tab.id}`}
-            onClick={(ev: { preventDefault: () => void }) => {
-              tab.onClick(ev);
-              onFilterClicked.bind(null, TimelineTabsStyle.filter, tab.id);
-            }}
-          >
-            {tab.name}
-          </EuiFilterButton>
-        ))}
-      </>
-    );
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [timelineType]);
+    return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => (
+      <EuiFilterButton
+        hasActiveFilters={tab.id === timelineType}
+        key={`timeline-${TimelineTabsStyle.filter}-${tab.id}`}
+        numFilters={tab.count}
+        onClick={(ev: { preventDefault: () => void }) => {
+          tab.onClick(ev);
+          onFilterClicked(tab.id);
+        }}
+        withNext={tab.withNext}
+      >
+        {tab.name}
+      </EuiFilterButton>
+    ));
+  }, [timelineType, getFilterOrTabs, onFilterClicked]);
 
   return {
     timelineType,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
index 9278225271930..012cfd66317de 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap
@@ -926,6 +926,7 @@ In other use cases the message field can be used to concatenate different values
                 }
               }
               start={1521830963132}
+              status="active"
               toggleColumn={[MockFunction]}
               usersViewing={
                 Array [
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
index a50e7e56661f2..53b018fb00adf 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx
@@ -5,13 +5,24 @@
  */
 import { mount } from 'enzyme';
 import React from 'react';
+import { useSelector } from 'react-redux';
 
-import { TestProviders } from '../../../../../common/mock';
+import { TestProviders, mockTimelineModel } from '../../../../../common/mock';
 import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
 
 import { Actions } from '.';
 
+jest.mock('react-redux', () => {
+  const origin = jest.requireActual('react-redux');
+  return {
+    ...origin,
+    useSelector: jest.fn(),
+  };
+});
+
 describe('Actions', () => {
+  (useSelector as jest.Mock).mockReturnValue(mockTimelineModel);
+
   test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => {
     const wrapper = mount(
       <TestProviders>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
index b478070b31578..d343c3db04da6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
@@ -3,10 +3,15 @@
  * or more contributor license agreements. Licensed under the Elastic License;
  * you may not use this file except in compliance with the Elastic License.
  */
-import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
 import React from 'react';
+import { useSelector } from 'react-redux';
+import { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
 
 import { Note } from '../../../../../common/lib/note';
+import { StoreState } from '../../../../../common/store/types';
+
+import { TimelineModel } from '../../../../store/timeline/model';
+
 import { AssociateNote, UpdateNote } from '../../../notes/helpers';
 import { Pin } from '../../pin';
 import { NotesButton } from '../../properties/helpers';
@@ -79,92 +84,101 @@ export const Actions = React.memo<Props>(
     showNotes,
     toggleShowNotes,
     updateNote,
-  }) => (
-    <EventsTdGroupActions
-      actionsColumnWidth={actionsColumnWidth}
-      data-test-subj="event-actions-container"
-    >
-      {showCheckboxes && (
-        <EventsTd data-test-subj="select-event-container">
+  }) => {
+    const timeline = useSelector<StoreState, TimelineModel>((state) => {
+      return state.timeline.timelineById['timeline-1'];
+    });
+    return (
+      <EventsTdGroupActions
+        actionsColumnWidth={actionsColumnWidth}
+        data-test-subj="event-actions-container"
+      >
+        {showCheckboxes && (
+          <EventsTd data-test-subj="select-event-container">
+            <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
+              {loadingEventIds.includes(eventId) ? (
+                <EuiLoadingSpinner size="m" data-test-subj="event-loader" />
+              ) : (
+                <EuiCheckbox
+                  data-test-subj="select-event"
+                  id={eventId}
+                  checked={checked}
+                  onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
+                    onRowSelected({
+                      eventIds: [eventId],
+                      isSelected: event.currentTarget.checked,
+                    });
+                  }}
+                />
+              )}
+            </EventsTdContent>
+          </EventsTd>
+        )}
+
+        <EventsTd>
           <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
-            {loadingEventIds.includes(eventId) ? (
-              <EuiLoadingSpinner size="m" data-test-subj="event-loader" />
-            ) : (
-              <EuiCheckbox
-                data-test-subj="select-event"
+            {loading && <EventsLoading />}
+
+            {!loading && (
+              <EuiButtonIcon
+                aria-label={expanded ? i18n.COLLAPSE : i18n.EXPAND}
+                data-test-subj="expand-event"
+                iconType={expanded ? 'arrowDown' : 'arrowRight'}
                 id={eventId}
-                checked={checked}
-                onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
-                  onRowSelected({
-                    eventIds: [eventId],
-                    isSelected: event.currentTarget.checked,
-                  });
-                }}
+                onClick={onEventToggled}
               />
             )}
           </EventsTdContent>
         </EventsTd>
-      )}
 
-      <EventsTd>
-        <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
-          {loading && <EventsLoading />}
+        <>{additionalActions}</>
 
-          {!loading && (
-            <EuiButtonIcon
-              aria-label={expanded ? i18n.COLLAPSE : i18n.EXPAND}
-              data-test-subj="expand-event"
-              iconType={expanded ? 'arrowDown' : 'arrowRight'}
-              id={eventId}
-              onClick={onEventToggled}
-            />
-          )}
-        </EventsTdContent>
-      </EventsTd>
+        {!isEventViewer && (
+          <>
+            <EventsTd>
+              <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
+                <EuiToolTip
+                  data-test-subj="timeline-action-pin-tool-tip"
+                  content={getPinTooltip({
+                    isPinned: eventIsPinned,
+                    eventHasNotes: eventHasNotes(noteIds),
+                    timelineType: timeline.timelineType,
+                  })}
+                >
+                  <Pin
+                    allowUnpinning={!eventHasNotes(noteIds)}
+                    data-test-subj="pin-event"
+                    onClick={onPinClicked}
+                    pinned={eventIsPinned}
+                    timelineType={timeline.timelineType}
+                  />
+                </EuiToolTip>
+              </EventsTdContent>
+            </EventsTd>
 
-      <>{additionalActions}</>
-
-      {!isEventViewer && (
-        <>
-          <EventsTd>
-            <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
-              <EuiToolTip
-                data-test-subj="timeline-action-pin-tool-tip"
-                content={getPinTooltip({
-                  isPinned: eventIsPinned,
-                  eventHasNotes: eventHasNotes(noteIds),
-                })}
-              >
-                <Pin
-                  allowUnpinning={!eventHasNotes(noteIds)}
-                  data-test-subj="pin-event"
-                  onClick={onPinClicked}
-                  pinned={eventIsPinned}
+            <EventsTd>
+              <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
+                <NotesButton
+                  animate={false}
+                  associateNote={associateNote}
+                  data-test-subj="add-note"
+                  getNotesByIds={getNotesByIds}
+                  noteIds={noteIds || emptyNotes}
+                  showNotes={showNotes}
+                  size="s"
+                  status={timeline.status}
+                  timelineType={timeline.timelineType}
+                  toggleShowNotes={toggleShowNotes}
+                  toolTip={timeline.timelineType ? i18n.NOTES_DISABLE_TOOLTIP : i18n.NOTES_TOOLTIP}
+                  updateNote={updateNote}
                 />
-              </EuiToolTip>
-            </EventsTdContent>
-          </EventsTd>
-
-          <EventsTd>
-            <EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
-              <NotesButton
-                animate={false}
-                associateNote={associateNote}
-                data-test-subj="add-note"
-                getNotesByIds={getNotesByIds}
-                noteIds={noteIds || emptyNotes}
-                showNotes={showNotes}
-                size="s"
-                toggleShowNotes={toggleShowNotes}
-                toolTip={i18n.NOTES_TOOLTIP}
-                updateNote={updateNote}
-              />
-            </EventsTdContent>
-          </EventsTd>
-        </>
-      )}
-    </EventsTdGroupActions>
-  ),
+              </EventsTdContent>
+            </EventsTd>
+          </>
+        )}
+      </EventsTdGroupActions>
+    );
+  },
   (nextProps, prevProps) => {
     return (
       prevProps.actionsColumnWidth === nextProps.actionsColumnWidth &&
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
index cf76cd3ddb8d4..d2175c728aa2a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx
@@ -5,6 +5,7 @@
  */
 
 import React, { useEffect, useRef, useState, useCallback } from 'react';
+import { useSelector } from 'react-redux';
 import uuid from 'uuid';
 import VisibilitySensor from 'react-visibility-sensor';
 
@@ -13,7 +14,7 @@ import { TimelineDetailsQuery } from '../../../../containers/details';
 import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types';
 import { requestIdleCallbackViaScheduler } from '../../../../../common/lib/helpers/scheduler';
 import { Note } from '../../../../../common/lib/note';
-import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
+import { ColumnHeaderOptions, TimelineModel } from '../../../../../timelines/store/timeline/model';
 import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers';
 import { SkeletonRow } from '../../skeleton_row';
 import {
@@ -33,6 +34,7 @@ import { getEventType } from '../helpers';
 import { NoteCards } from '../../../notes/note_cards';
 import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
 import { EventColumnView } from './event_column_view';
+import { StoreState } from '../../../../../common/store';
 
 interface Props {
   actionsColumnWidth: number;
@@ -128,7 +130,9 @@ const StatefulEventComponent: React.FC<Props> = ({
   const [expanded, setExpanded] = useState<{ [eventId: string]: boolean }>({});
   const [initialRender, setInitialRender] = useState(false);
   const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({});
-
+  const timeline = useSelector<StoreState, TimelineModel>((state) => {
+    return state.timeline.timelineById['timeline-1'];
+  });
   const divElement = useRef<HTMLDivElement | null>(null);
 
   const onToggleShowNotes = useCallback(() => {
@@ -251,6 +255,7 @@ const StatefulEventComponent: React.FC<Props> = ({
                         getNotesByIds={getNotesByIds}
                         noteIds={eventIdToNoteIds[event._id] || emptyNotes}
                         showAddNote={!!showNotes[event._id]}
+                        status={timeline.status}
                         toggleShowAddNote={onToggleShowNotes}
                         updateNote={updateNote}
                       />
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts
index e237e99df9ada..7ecd7ec5ed35c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.test.ts
@@ -7,6 +7,7 @@
 import { Ecs } from '../../../../graphql/types';
 
 import { eventHasNotes, eventIsPinned, getPinTooltip, stringifyEvent } from './helpers';
+import { TimelineType } from '../../../../../common/types/timeline';
 
 describe('helpers', () => {
   describe('stringifyEvent', () => {
@@ -192,21 +193,37 @@ describe('helpers', () => {
 
   describe('getPinTooltip', () => {
     test('it indicates the event may NOT be unpinned when `isPinned` is `true` and the event has notes', () => {
-      expect(getPinTooltip({ isPinned: true, eventHasNotes: true })).toEqual(
-        'This event cannot be unpinned because it has notes'
-      );
+      expect(
+        getPinTooltip({ isPinned: true, eventHasNotes: true, timelineType: TimelineType.default })
+      ).toEqual('This event cannot be unpinned because it has notes');
     });
 
     test('it indicates the event is pinned when `isPinned` is `true` and the event does NOT have notes', () => {
-      expect(getPinTooltip({ isPinned: true, eventHasNotes: false })).toEqual('Pinned event');
+      expect(
+        getPinTooltip({ isPinned: true, eventHasNotes: false, timelineType: TimelineType.default })
+      ).toEqual('Pinned event');
     });
 
     test('it indicates the event is NOT pinned when `isPinned` is `false` and the event has notes', () => {
-      expect(getPinTooltip({ isPinned: false, eventHasNotes: true })).toEqual('Unpinned event');
+      expect(
+        getPinTooltip({ isPinned: false, eventHasNotes: true, timelineType: TimelineType.default })
+      ).toEqual('Unpinned event');
     });
 
     test('it indicates the event is NOT pinned when `isPinned` is `false` and the event does NOT have notes', () => {
-      expect(getPinTooltip({ isPinned: false, eventHasNotes: false })).toEqual('Unpinned event');
+      expect(
+        getPinTooltip({ isPinned: false, eventHasNotes: false, timelineType: TimelineType.default })
+      ).toEqual('Unpinned event');
+    });
+
+    test('it indicates the event is disabled if timelineType is template', () => {
+      expect(
+        getPinTooltip({
+          isPinned: false,
+          eventHasNotes: false,
+          timelineType: TimelineType.template,
+        })
+      ).toEqual('This event cannot be pinned because it is filtered by a timeline template');
     });
   });
 
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
index bdc8c66ec3aa6..52bbccbba58e7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts
@@ -15,6 +15,7 @@ import { OnPinEvent, OnUnPinEvent } from '../events';
 import { TimelineRowAction, TimelineRowActionOnClick } from './actions';
 
 import * as i18n from './translations';
+import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export const omitTypenameAndEmpty = (k: string, v: any): any | undefined =>
@@ -28,10 +29,19 @@ export const getPinTooltip = ({
   isPinned,
   // eslint-disable-next-line no-shadow
   eventHasNotes,
+  timelineType,
 }: {
   isPinned: boolean;
   eventHasNotes: boolean;
-}) => (isPinned && eventHasNotes ? i18n.PINNED_WITH_NOTES : isPinned ? i18n.PINNED : i18n.UNPINNED);
+  timelineType: TimelineTypeLiteral;
+}) =>
+  timelineType === TimelineType.template
+    ? i18n.DISABLE_PIN
+    : isPinned && eventHasNotes
+    ? i18n.PINNED_WITH_NOTES
+    : isPinned
+    ? i18n.PINNED
+    : i18n.UNPINNED;
 
 export interface IsPinnedParams {
   eventId: string;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
index 9b96e0c49c73d..51bf883ed2d61 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx
@@ -5,10 +5,11 @@
  */
 
 import React from 'react';
+import { useSelector } from 'react-redux';
 
 import { mockBrowserFields } from '../../../../common/containers/source/mock';
 import { Direction } from '../../../../graphql/types';
-import { defaultHeaders, mockTimelineData } from '../../../../common/mock';
+import { defaultHeaders, mockTimelineData, mockTimelineModel } from '../../../../common/mock';
 import { TestProviders } from '../../../../common/mock/test_providers';
 
 import { Body, BodyProps } from '.';
@@ -24,6 +25,13 @@ const mockSort: Sort = {
   sortDirection: Direction.desc,
 };
 
+jest.mock('react-redux', () => {
+  const origin = jest.requireActual('react-redux');
+  return {
+    ...origin,
+    useSelector: jest.fn(),
+  };
+});
 jest.mock('../../../../common/components/link_to');
 
 jest.mock(
@@ -41,41 +49,43 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({
 
 describe('Body', () => {
   const mount = useMountAppended();
+  const props: BodyProps = {
+    addNoteToEvent: jest.fn(),
+    browserFields: mockBrowserFields,
+    columnHeaders: defaultHeaders,
+    columnRenderers,
+    data: mockTimelineData,
+    eventIdToNoteIds: {},
+    height: testBodyHeight,
+    id: 'timeline-test',
+    isSelectAllChecked: false,
+    getNotesByIds: mockGetNotesByIds,
+    loadingEventIds: [],
+    onColumnRemoved: jest.fn(),
+    onColumnResized: jest.fn(),
+    onColumnSorted: jest.fn(),
+    onFilterChange: jest.fn(),
+    onPinEvent: jest.fn(),
+    onRowSelected: jest.fn(),
+    onSelectAll: jest.fn(),
+    onUnPinEvent: jest.fn(),
+    onUpdateColumns: jest.fn(),
+    pinnedEventIds: {},
+    rowRenderers,
+    selectedEventIds: {},
+    show: true,
+    sort: mockSort,
+    showCheckboxes: false,
+    toggleColumn: jest.fn(),
+    updateNote: jest.fn(),
+  };
+  (useSelector as jest.Mock).mockReturnValue(mockTimelineModel);
 
   describe('rendering', () => {
     test('it renders the column headers', () => {
       const wrapper = mount(
         <TestProviders>
-          <Body
-            addNoteToEvent={jest.fn()}
-            browserFields={mockBrowserFields}
-            columnHeaders={defaultHeaders}
-            columnRenderers={columnRenderers}
-            data={mockTimelineData}
-            eventIdToNoteIds={{}}
-            height={testBodyHeight}
-            id={'timeline-test'}
-            isSelectAllChecked={false}
-            getNotesByIds={mockGetNotesByIds}
-            loadingEventIds={[]}
-            onColumnRemoved={jest.fn()}
-            onColumnResized={jest.fn()}
-            onColumnSorted={jest.fn()}
-            onFilterChange={jest.fn()}
-            onPinEvent={jest.fn()}
-            onRowSelected={jest.fn()}
-            onSelectAll={jest.fn()}
-            onUnPinEvent={jest.fn()}
-            onUpdateColumns={jest.fn()}
-            pinnedEventIds={{}}
-            rowRenderers={rowRenderers}
-            selectedEventIds={{}}
-            show={true}
-            sort={mockSort}
-            showCheckboxes={false}
-            toggleColumn={jest.fn()}
-            updateNote={jest.fn()}
-          />
+          <Body {...props} />
         </TestProviders>
       );
 
@@ -85,36 +95,7 @@ describe('Body', () => {
     test('it renders the scroll container', () => {
       const wrapper = mount(
         <TestProviders>
-          <Body
-            addNoteToEvent={jest.fn()}
-            browserFields={mockBrowserFields}
-            columnHeaders={defaultHeaders}
-            columnRenderers={columnRenderers}
-            data={mockTimelineData}
-            eventIdToNoteIds={{}}
-            height={testBodyHeight}
-            id={'timeline-test'}
-            isSelectAllChecked={false}
-            getNotesByIds={mockGetNotesByIds}
-            loadingEventIds={[]}
-            onColumnRemoved={jest.fn()}
-            onColumnResized={jest.fn()}
-            onColumnSorted={jest.fn()}
-            onFilterChange={jest.fn()}
-            onPinEvent={jest.fn()}
-            onRowSelected={jest.fn()}
-            onSelectAll={jest.fn()}
-            onUnPinEvent={jest.fn()}
-            onUpdateColumns={jest.fn()}
-            pinnedEventIds={{}}
-            rowRenderers={rowRenderers}
-            selectedEventIds={{}}
-            show={true}
-            sort={mockSort}
-            showCheckboxes={false}
-            toggleColumn={jest.fn()}
-            updateNote={jest.fn()}
-          />
+          <Body {...props} />
         </TestProviders>
       );
 
@@ -124,36 +105,7 @@ describe('Body', () => {
     test('it renders events', () => {
       const wrapper = mount(
         <TestProviders>
-          <Body
-            addNoteToEvent={jest.fn()}
-            browserFields={mockBrowserFields}
-            columnHeaders={defaultHeaders}
-            columnRenderers={columnRenderers}
-            data={mockTimelineData}
-            eventIdToNoteIds={{}}
-            height={testBodyHeight}
-            id={'timeline-test'}
-            isSelectAllChecked={false}
-            getNotesByIds={mockGetNotesByIds}
-            loadingEventIds={[]}
-            onColumnRemoved={jest.fn()}
-            onColumnResized={jest.fn()}
-            onColumnSorted={jest.fn()}
-            onFilterChange={jest.fn()}
-            onPinEvent={jest.fn()}
-            onRowSelected={jest.fn()}
-            onSelectAll={jest.fn()}
-            onUnPinEvent={jest.fn()}
-            onUpdateColumns={jest.fn()}
-            pinnedEventIds={{}}
-            rowRenderers={rowRenderers}
-            selectedEventIds={{}}
-            show={true}
-            sort={mockSort}
-            showCheckboxes={false}
-            toggleColumn={jest.fn()}
-            updateNote={jest.fn()}
-          />
+          <Body {...props} />
         </TestProviders>
       );
 
@@ -162,39 +114,10 @@ describe('Body', () => {
 
     test('it renders a tooltip for timestamp', async () => {
       const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp');
-
+      const testProps = { ...props, columnHeaders: headersJustTimestamp };
       const wrapper = mount(
         <TestProviders>
-          <Body
-            addNoteToEvent={jest.fn()}
-            browserFields={mockBrowserFields}
-            columnHeaders={headersJustTimestamp}
-            columnRenderers={columnRenderers}
-            data={mockTimelineData}
-            eventIdToNoteIds={{}}
-            height={testBodyHeight}
-            id={'timeline-test'}
-            isSelectAllChecked={false}
-            getNotesByIds={mockGetNotesByIds}
-            loadingEventIds={[]}
-            onColumnRemoved={jest.fn()}
-            onColumnResized={jest.fn()}
-            onColumnSorted={jest.fn()}
-            onFilterChange={jest.fn()}
-            onPinEvent={jest.fn()}
-            onRowSelected={jest.fn()}
-            onSelectAll={jest.fn()}
-            onUnPinEvent={jest.fn()}
-            onUpdateColumns={jest.fn()}
-            pinnedEventIds={{}}
-            rowRenderers={rowRenderers}
-            selectedEventIds={{}}
-            show={true}
-            sort={mockSort}
-            showCheckboxes={false}
-            toggleColumn={jest.fn()}
-            updateNote={jest.fn()}
-          />
+          <Body {...testProps} />
         </TestProviders>
       );
       wrapper.update();
@@ -215,6 +138,11 @@ describe('Body', () => {
   describe('action on event', () => {
     const dispatchAddNoteToEvent = jest.fn();
     const dispatchOnPinEvent = jest.fn();
+    const testProps = {
+      ...props,
+      addNoteToEvent: dispatchAddNoteToEvent,
+      onPinEvent: dispatchOnPinEvent,
+    };
 
     const addaNoteToEvent = (wrapper: ReturnType<typeof mount>, note: string) => {
       wrapper.find('[data-test-subj="add-note"]').first().find('button').simulate('click');
@@ -251,36 +179,7 @@ describe('Body', () => {
     test('Add a Note to an event', () => {
       const wrapper = mount(
         <TestProviders>
-          <Body
-            addNoteToEvent={dispatchAddNoteToEvent}
-            browserFields={mockBrowserFields}
-            columnHeaders={defaultHeaders}
-            columnRenderers={columnRenderers}
-            data={mockTimelineData}
-            eventIdToNoteIds={{}}
-            height={testBodyHeight}
-            id={'timeline-test'}
-            isSelectAllChecked={false}
-            getNotesByIds={mockGetNotesByIds}
-            loadingEventIds={[]}
-            onColumnRemoved={jest.fn()}
-            onColumnResized={jest.fn()}
-            onColumnSorted={jest.fn()}
-            onFilterChange={jest.fn()}
-            onPinEvent={dispatchOnPinEvent}
-            onRowSelected={jest.fn()}
-            onSelectAll={jest.fn()}
-            onUnPinEvent={jest.fn()}
-            onUpdateColumns={jest.fn()}
-            pinnedEventIds={{}}
-            rowRenderers={rowRenderers}
-            selectedEventIds={{}}
-            show={true}
-            sort={mockSort}
-            showCheckboxes={false}
-            toggleColumn={jest.fn()}
-            updateNote={jest.fn()}
-          />
+          <Body {...testProps} />
         </TestProviders>
       );
       addaNoteToEvent(wrapper, 'hello world');
@@ -290,44 +189,13 @@ describe('Body', () => {
     });
 
     test('Add two Note to an event', () => {
-      const Proxy = (props: BodyProps) => (
+      const Proxy = (proxyProps: BodyProps) => (
         <TestProviders>
-          <Body {...props} />
+          <Body {...proxyProps} />
         </TestProviders>
       );
 
-      const wrapper = mount(
-        <Proxy
-          addNoteToEvent={dispatchAddNoteToEvent}
-          browserFields={mockBrowserFields}
-          columnHeaders={defaultHeaders}
-          columnRenderers={columnRenderers}
-          data={mockTimelineData}
-          eventIdToNoteIds={{}}
-          height={testBodyHeight}
-          id={'timeline-test'}
-          isSelectAllChecked={false}
-          getNotesByIds={mockGetNotesByIds}
-          loadingEventIds={[]}
-          onColumnRemoved={jest.fn()}
-          onColumnResized={jest.fn()}
-          onColumnSorted={jest.fn()}
-          onFilterChange={jest.fn()}
-          onPinEvent={dispatchOnPinEvent}
-          onRowSelected={jest.fn()}
-          onSelectAll={jest.fn()}
-          onUnPinEvent={jest.fn()}
-          onUpdateColumns={jest.fn()}
-          pinnedEventIds={{}}
-          rowRenderers={rowRenderers}
-          selectedEventIds={{}}
-          show={true}
-          sort={mockSort}
-          showCheckboxes={false}
-          toggleColumn={jest.fn()}
-          updateNote={jest.fn()}
-        />
-      );
+      const wrapper = mount(<Proxy {...testProps} />);
       addaNoteToEvent(wrapper, 'hello world');
       dispatchAddNoteToEvent.mockClear();
       dispatchOnPinEvent.mockClear();
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
index 63b92d6b316cc..ef7ee26cd3ecf 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
@@ -13,6 +13,13 @@ export const NOTES_TOOLTIP = i18n.translate(
   }
 );
 
+export const NOTES_DISABLE_TOOLTIP = i18n.translate(
+  'xpack.securitySolution.timeline.body.notes.disableEventTooltip',
+  {
+    defaultMessage: 'Add notes for event filtered by a timeline template is not allowed',
+  }
+);
+
 export const COPY_TO_CLIPBOARD = i18n.translate(
   'xpack.securitySolution.timeline.body.copyToClipboardButtonLabel',
   {
@@ -38,6 +45,13 @@ export const PINNED_WITH_NOTES = i18n.translate(
   }
 );
 
+export const DISABLE_PIN = i18n.translate(
+  'xpack.securitySolution.timeline.body.pinning.disablePinnnedTooltip',
+  {
+    defaultMessage: 'This event cannot be pinned because it is filtered by a timeline template',
+  }
+);
+
 export const EXPAND = i18n.translate(
   'xpack.securitySolution.timeline.body.actions.expandAriaLabel',
   {
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx
index 6fb2443486f81..922148535d126 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.test.tsx
@@ -15,6 +15,7 @@ import { mockDataProviders } from '../data_providers/mock/mock_data_providers';
 import { useMountAppended } from '../../../../common/utils/use_mount_appended';
 
 import { TimelineHeader } from '.';
+import { TimelineStatus } from '../../../../../common/types/timeline';
 
 const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings;
 
@@ -23,43 +24,32 @@ jest.mock('../../../../common/lib/kibana');
 describe('Header', () => {
   const indexPattern = mockIndexPattern;
   const mount = useMountAppended();
+  const props = {
+    browserFields: {},
+    dataProviders: mockDataProviders,
+    filterManager: new FilterManager(mockUiSettingsForFilterManager),
+    id: 'foo',
+    indexPattern,
+    onDataProviderEdited: jest.fn(),
+    onDataProviderRemoved: jest.fn(),
+    onToggleDataProviderEnabled: jest.fn(),
+    onToggleDataProviderExcluded: jest.fn(),
+    show: true,
+    showCallOutUnauthorizedMsg: false,
+    status: TimelineStatus.active,
+  };
 
   describe('rendering', () => {
     test('renders correctly against snapshot', () => {
-      const wrapper = shallow(
-        <TimelineHeader
-          browserFields={{}}
-          dataProviders={mockDataProviders}
-          filterManager={new FilterManager(mockUiSettingsForFilterManager)}
-          id="foo"
-          indexPattern={indexPattern}
-          onDataProviderEdited={jest.fn()}
-          onDataProviderRemoved={jest.fn()}
-          onToggleDataProviderEnabled={jest.fn()}
-          onToggleDataProviderExcluded={jest.fn()}
-          show={true}
-          showCallOutUnauthorizedMsg={false}
-        />
-      );
+      const wrapper = shallow(<TimelineHeader {...props} />);
       expect(wrapper).toMatchSnapshot();
     });
 
     test('it renders the data providers when show is true', () => {
+      const testProps = { ...props, show: true };
       const wrapper = mount(
         <TestProviders>
-          <TimelineHeader
-            browserFields={{}}
-            dataProviders={mockDataProviders}
-            filterManager={new FilterManager(mockUiSettingsForFilterManager)}
-            id="foo"
-            indexPattern={indexPattern}
-            onDataProviderEdited={jest.fn()}
-            onDataProviderRemoved={jest.fn()}
-            onToggleDataProviderEnabled={jest.fn()}
-            onToggleDataProviderExcluded={jest.fn()}
-            show={true}
-            showCallOutUnauthorizedMsg={false}
-          />
+          <TimelineHeader {...testProps} />
         </TestProviders>
       );
 
@@ -67,21 +57,11 @@ describe('Header', () => {
     });
 
     test('it does NOT render the data providers when show is false', () => {
+      const testProps = { ...props, show: false };
+
       const wrapper = mount(
         <TestProviders>
-          <TimelineHeader
-            browserFields={{}}
-            dataProviders={mockDataProviders}
-            filterManager={new FilterManager(mockUiSettingsForFilterManager)}
-            id="foo"
-            indexPattern={indexPattern}
-            onDataProviderEdited={jest.fn()}
-            onDataProviderRemoved={jest.fn()}
-            onToggleDataProviderEnabled={jest.fn()}
-            onToggleDataProviderExcluded={jest.fn()}
-            show={false}
-            showCallOutUnauthorizedMsg={false}
-          />
+          <TimelineHeader {...testProps} />
         </TestProviders>
       );
 
@@ -89,21 +69,15 @@ describe('Header', () => {
     });
 
     test('it renders the unauthorized call out providers', () => {
+      const testProps = {
+        ...props,
+        filterManager: new FilterManager(mockUiSettingsForFilterManager),
+        showCallOutUnauthorizedMsg: true,
+      };
+
       const wrapper = mount(
         <TestProviders>
-          <TimelineHeader
-            browserFields={{}}
-            dataProviders={mockDataProviders}
-            filterManager={new FilterManager(mockUiSettingsForFilterManager)}
-            id="foo"
-            indexPattern={indexPattern}
-            onDataProviderEdited={jest.fn()}
-            onDataProviderRemoved={jest.fn()}
-            onToggleDataProviderEnabled={jest.fn()}
-            onToggleDataProviderExcluded={jest.fn()}
-            show={true}
-            showCallOutUnauthorizedMsg={true}
-          />
+          <TimelineHeader {...testProps} />
         </TestProviders>
       );
 
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
index e8f1e73719234..0541dee4b1e52 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/index.tsx
@@ -22,6 +22,10 @@ import { StatefulSearchOrFilter } from '../search_or_filter';
 import { BrowserFields } from '../../../../common/containers/source';
 
 import * as i18n from './translations';
+import {
+  TimelineStatus,
+  TimelineStatusLiteralWithNull,
+} from '../../../../../common/types/timeline';
 
 interface Props {
   browserFields: BrowserFields;
@@ -36,6 +40,7 @@ interface Props {
   onToggleDataProviderExcluded: OnToggleDataProviderExcluded;
   show: boolean;
   showCallOutUnauthorizedMsg: boolean;
+  status: TimelineStatusLiteralWithNull;
 }
 
 const TimelineHeaderComponent: React.FC<Props> = ({
@@ -51,6 +56,7 @@ const TimelineHeaderComponent: React.FC<Props> = ({
   onToggleDataProviderExcluded,
   show,
   showCallOutUnauthorizedMsg,
+  status,
 }) => (
   <>
     {showCallOutUnauthorizedMsg && (
@@ -62,7 +68,15 @@ const TimelineHeaderComponent: React.FC<Props> = ({
         size="s"
       />
     )}
-
+    {status === TimelineStatus.immutable && (
+      <EuiCallOut
+        data-test-subj="timelineImmutableCallOut"
+        title={i18n.CALL_OUT_IMMUTIABLE}
+        color="primary"
+        iconType="info"
+        size="s"
+      />
+    )}
     {show && !showGraphView(graphEventId) && (
       <>
         <DataProviders
@@ -100,5 +114,6 @@ export const TimelineHeader = React.memo(
     prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled &&
     prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded &&
     prevProps.show === nextProps.show &&
-    prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg
+    prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
+    prevProps.status === nextProps.status
 );
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
index c3c11289037a2..dd945d345aad8 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/translations.ts
@@ -13,3 +13,11 @@ export const CALL_OUT_UNAUTHORIZED_MSG = i18n.translate(
       'You can use Timeline to investigate events, but you do not have the required permissions to save timelines for future use. If you need to save timelines, contact your Kibana administrator.',
   }
 );
+
+export const CALL_OUT_IMMUTIABLE = i18n.translate(
+  'xpack.securitySolution.timeline.callOut.immutable.message.description',
+  {
+    defaultMessage:
+      'This timeline is immutable, therefore not allowed to save it within the security application, though you may continue to use the timeline to search and filter security events',
+  }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
index 83ac1a421958b..296b24cff43ad 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx
@@ -25,6 +25,7 @@ import { Sort } from './body/sort';
 import { mockDataProviders } from './data_providers/mock/mock_data_providers';
 import { StatefulTimeline, Props as StatefulTimelineProps } from './index';
 import { Timeline } from './timeline';
+import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
 
 jest.mock('../../../common/lib/kibana', () => {
   const originalModule = jest.requireActual('../../../common/lib/kibana');
@@ -88,6 +89,8 @@ describe('StatefulTimeline', () => {
       showCallOutUnauthorizedMsg: false,
       sort,
       start: startDate,
+      status: TimelineStatus.active,
+      timelineType: TimelineType.default,
       updateColumns: timelineActions.updateColumns,
       updateDataProviderEnabled: timelineActions.updateDataProviderEnabled,
       updateDataProviderExcluded: timelineActions.updateDataProviderExcluded,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
index a66c01d0b5d0b..35622eddc359c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx
@@ -57,6 +57,8 @@ const StatefulTimelineComponent = React.memo<Props>(
     showCallOutUnauthorizedMsg,
     sort,
     start,
+    status,
+    timelineType,
     updateDataProviderEnabled,
     updateDataProviderExcluded,
     updateItemsPerPage,
@@ -189,6 +191,7 @@ const StatefulTimelineComponent = React.memo<Props>(
         showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg}
         sort={sort!}
         start={start}
+        status={status}
         toggleColumn={toggleColumn}
         usersViewing={usersViewing}
       />
@@ -207,6 +210,8 @@ const StatefulTimelineComponent = React.memo<Props>(
       prevProps.show === nextProps.show &&
       prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
       prevProps.start === nextProps.start &&
+      prevProps.timelineType === nextProps.timelineType &&
+      prevProps.status === nextProps.status &&
       deepEqual(prevProps.columns, nextProps.columns) &&
       deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
       deepEqual(prevProps.filters, nextProps.filters) &&
@@ -238,11 +243,12 @@ const makeMapStateToProps = () => {
       kqlMode,
       show,
       sort,
+      status,
+      timelineType,
     } = timeline;
     const kqlQueryExpression = getKqlQueryTimeline(state, id)!;
 
     const timelineFilter = kqlMode === 'filter' ? filters || [] : [];
-
     return {
       columns,
       dataProviders,
@@ -261,6 +267,8 @@ const makeMapStateToProps = () => {
       showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state),
       sort,
       start: input.timerange.from,
+      status,
+      timelineType,
     };
   };
   return mapStateToProps;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx
index 800ea814fdd50..30fe8ae0ca1f6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/pin/index.tsx
@@ -8,6 +8,8 @@ import { EuiButtonIcon, IconSize } from '@elastic/eui';
 import { noop } from 'lodash/fp';
 import React from 'react';
 
+import { TimelineType, TimelineTypeLiteral } from '../../../../../common/types/timeline';
+
 import * as i18n from '../body/translations';
 
 export type PinIcon = 'pin' | 'pinFilled';
@@ -17,21 +19,25 @@ export const getPinIcon = (pinned: boolean): PinIcon => (pinned ? 'pinFilled' :
 interface Props {
   allowUnpinning: boolean;
   iconSize?: IconSize;
+  timelineType?: TimelineTypeLiteral;
   onClick?: () => void;
   pinned: boolean;
 }
 
 export const Pin = React.memo<Props>(
-  ({ allowUnpinning, iconSize = 'm', onClick = noop, pinned }) => (
-    <EuiButtonIcon
-      aria-label={pinned ? i18n.PINNED : i18n.UNPINNED}
-      data-test-subj="pin"
-      iconSize={iconSize}
-      iconType={getPinIcon(pinned)}
-      isDisabled={allowUnpinning ? false : true}
-      onClick={onClick}
-    />
-  )
+  ({ allowUnpinning, iconSize = 'm', onClick = noop, pinned, timelineType }) => {
+    const isTemplate = timelineType === TimelineType.template;
+    return (
+      <EuiButtonIcon
+        aria-label={pinned ? i18n.PINNED : i18n.UNPINNED}
+        data-test-subj="pin"
+        iconSize={iconSize}
+        iconType={getPinIcon(pinned)}
+        onClick={onClick}
+        isDisabled={isTemplate}
+      />
+    );
+  }
 );
 
 Pin.displayName = 'Pin';
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
index 528af23191ee9..21140d668d716 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx
@@ -27,6 +27,7 @@ import {
   TimelineTypeLiteral,
   TimelineStatus,
   TimelineType,
+  TimelineStatusLiteral,
   TimelineId,
 } from '../../../../../common/types/timeline';
 import { SecurityPageName } from '../../../../app/types';
@@ -262,11 +263,13 @@ interface NotesButtonProps {
   getNotesByIds: (noteIds: string[]) => Note[];
   noteIds: string[];
   size: 's' | 'l';
+  status: TimelineStatusLiteral;
   showNotes: boolean;
   toggleShowNotes: () => void;
   text?: string;
   toolTip?: string;
   updateNote: UpdateNote;
+  timelineType: TimelineTypeLiteral;
 }
 
 const getNewNoteId = (): string => uuid.v4();
@@ -303,16 +306,24 @@ LargeNotesButton.displayName = 'LargeNotesButton';
 interface SmallNotesButtonProps {
   noteIds: string[];
   toggleShowNotes: () => void;
+  timelineType: TimelineTypeLiteral;
 }
 
-const SmallNotesButton = React.memo<SmallNotesButtonProps>(({ noteIds, toggleShowNotes }) => (
-  <EuiButtonIcon
-    aria-label={i18n.NOTES}
-    data-test-subj="timeline-notes-button-small"
-    iconType="editorComment"
-    onClick={() => toggleShowNotes()}
-  />
-));
+const SmallNotesButton = React.memo<SmallNotesButtonProps>(
+  ({ noteIds, toggleShowNotes, timelineType }) => {
+    const isTemplate = timelineType === TimelineType.template;
+
+    return (
+      <EuiButtonIcon
+        aria-label={i18n.NOTES}
+        data-test-subj="timeline-notes-button-small"
+        iconType="editorComment"
+        onClick={() => toggleShowNotes()}
+        isDisabled={isTemplate}
+      />
+    );
+  }
+);
 SmallNotesButton.displayName = 'SmallNotesButton';
 
 /**
@@ -326,25 +337,32 @@ const NotesButtonComponent = React.memo<NotesButtonProps>(
     noteIds,
     showNotes,
     size,
+    status,
     toggleShowNotes,
     text,
     updateNote,
+    timelineType,
   }) => (
     <ButtonContainer animate={animate} data-test-subj="timeline-notes-button-container">
       <>
         {size === 'l' ? (
           <LargeNotesButton noteIds={noteIds} text={text} toggleShowNotes={toggleShowNotes} />
         ) : (
-          <SmallNotesButton noteIds={noteIds} toggleShowNotes={toggleShowNotes} />
+          <SmallNotesButton
+            noteIds={noteIds}
+            toggleShowNotes={toggleShowNotes}
+            timelineType={timelineType}
+          />
         )}
         {size === 'l' && showNotes ? (
           <EuiOverlayMask>
             <EuiModal maxWidth={NOTES_PANEL_WIDTH} onClose={toggleShowNotes}>
               <Notes
                 associateNote={associateNote}
+                getNewNoteId={getNewNoteId}
                 getNotesByIds={getNotesByIds}
+                status={status}
                 noteIds={noteIds}
-                getNewNoteId={getNewNoteId}
                 updateNote={updateNote}
               />
             </EuiModal>
@@ -364,6 +382,8 @@ export const NotesButton = React.memo<NotesButtonProps>(
     noteIds,
     showNotes,
     size,
+    status,
+    timelineType,
     toggleShowNotes,
     toolTip,
     text,
@@ -377,9 +397,11 @@ export const NotesButton = React.memo<NotesButtonProps>(
         noteIds={noteIds}
         showNotes={showNotes}
         size={size}
+        status={status}
         toggleShowNotes={toggleShowNotes}
         text={text}
         updateNote={updateNote}
+        timelineType={timelineType}
       />
     ) : (
       <EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip">
@@ -390,9 +412,11 @@ export const NotesButton = React.memo<NotesButtonProps>(
           noteIds={noteIds}
           showNotes={showNotes}
           size={size}
+          status={status}
           toggleShowNotes={toggleShowNotes}
           text={text}
           updateNote={updateNote}
+          timelineType={timelineType}
         />
       </EuiToolTip>
     )
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
index 1b76db409484f..cd089d10d5d4c 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx
@@ -6,13 +6,14 @@
 
 import { mount } from 'enzyme';
 import React from 'react';
-import { TimelineStatus } from '../../../../../common/types/timeline';
+import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
 import {
   mockGlobalState,
   apolloClientObservable,
   SUB_PLUGINS_REDUCER,
   createSecuritySolutionStorageMock,
   TestProviders,
+  kibanaObservable,
 } from '../../../../common/mock';
 import { createStore, State } from '../../../../common/store';
 import { useThrottledResizeObserver } from '../../../../common/components/utils';
@@ -86,6 +87,7 @@ const defaultProps = {
   isDatepickerLocked: false,
   isFavorite: false,
   title: '',
+  timelineType: TimelineType.default,
   description: '',
   getNotesByIds: jest.fn(),
   noteIds: [],
@@ -103,11 +105,23 @@ describe('Properties', () => {
   const { storage } = createSecuritySolutionStorageMock();
   let mockedWidth = 1000;
 
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   beforeEach(() => {
     jest.clearAllMocks();
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
     (useThrottledResizeObserver as jest.Mock).mockReturnValue({ width: mockedWidth });
   });
 
@@ -130,9 +144,10 @@ describe('Properties', () => {
   });
 
   test('renders correctly draft timeline', () => {
+    const testProps = { ...defaultProps, status: TimelineStatus.draft };
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, status: TimelineStatus.draft }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
 
@@ -157,9 +172,11 @@ describe('Properties', () => {
   });
 
   test('it renders a filled star icon when it is a favorite', () => {
+    const testProps = { ...defaultProps, isFavorite: true };
+
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, isFavorite: true }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
 
@@ -168,10 +185,10 @@ describe('Properties', () => {
 
   test('it renders the title of the timeline', () => {
     const title = 'foozle';
-
+    const testProps = { ...defaultProps, title };
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, title }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
 
@@ -194,9 +211,11 @@ describe('Properties', () => {
   });
 
   test('it renders the lock icon when isDatepickerLocked is true', () => {
+    const testProps = { ...defaultProps, isDatepickerLocked: true };
+
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, isDatepickerLocked: true }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
     expect(
@@ -223,13 +242,16 @@ describe('Properties', () => {
 
   test('it renders a description on the left when the width is at least as wide as the threshold', () => {
     const description = 'strange';
+    const testProps = { ...defaultProps, description };
+
+    // mockedWidth = showDescriptionThreshold;
 
     (useThrottledResizeObserver as jest.Mock).mockReset();
     (useThrottledResizeObserver as jest.Mock).mockReturnValue({ width: showDescriptionThreshold });
 
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, description }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
 
@@ -244,6 +266,9 @@ describe('Properties', () => {
 
   test('it does NOT render a description on the left when the width is less than the threshold', () => {
     const description = 'strange';
+    const testProps = { ...defaultProps, description };
+
+    // mockedWidth = showDescriptionThreshold - 1;
 
     (useThrottledResizeObserver as jest.Mock).mockReset();
     (useThrottledResizeObserver as jest.Mock).mockReturnValue({
@@ -252,7 +277,7 @@ describe('Properties', () => {
 
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, description }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
 
@@ -313,10 +338,11 @@ describe('Properties', () => {
 
   test('it renders an avatar for the current user viewing the timeline when it has a title', () => {
     const title = 'port scan';
+    const testProps = { ...defaultProps, title };
 
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, title }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
 
@@ -334,9 +360,11 @@ describe('Properties', () => {
   });
 
   test('insert timeline - new case', async () => {
+    const testProps = { ...defaultProps, title: 'coolness' };
+
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, title: 'coolness' }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
     wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click');
@@ -352,9 +380,11 @@ describe('Properties', () => {
   });
 
   test('insert timeline - existing case', async () => {
+    const testProps = { ...defaultProps, title: 'coolness' };
+
     const wrapper = mount(
       <TestProviders store={store}>
-        <Properties {...{ ...defaultProps, title: 'coolness' }} />
+        <Properties {...testProps} />
       </TestProviders>
     );
     wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click');
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
index 8029d166a688a..40462fa0d09da 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx
@@ -7,7 +7,7 @@
 import React, { useState, useCallback, useMemo } from 'react';
 
 import { useDispatch, useSelector } from 'react-redux';
-import { TimelineStatus, TimelineTypeLiteral } from '../../../../../common/types/timeline';
+import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline';
 import { useThrottledResizeObserver } from '../../../../common/components/utils';
 import { Note } from '../../../../common/lib/note';
 import { InputsModelId } from '../../../../common/store/inputs/constants';
@@ -52,7 +52,8 @@ interface Props {
   isFavorite: boolean;
   noteIds: string[];
   timelineId: string;
-  status: TimelineStatus;
+  timelineType: TimelineTypeLiteral;
+  status: TimelineStatusLiteral;
   title: string;
   toggleLock: ToggleLock;
   updateDescription: UpdateDescription;
@@ -87,6 +88,7 @@ export const Properties = React.memo<Props>(
     noteIds,
     status,
     timelineId,
+    timelineType,
     title,
     toggleLock,
     updateDescription,
@@ -164,10 +166,12 @@ export const Properties = React.memo<Props>(
           isFavorite={isFavorite}
           noteIds={noteIds}
           onToggleShowNotes={onToggleShowNotes}
+          status={status}
           showDescription={width >= showDescriptionThreshold}
           showNotes={showNotes}
           showNotesFromWidth={width >= showNotesThreshold}
           timelineId={timelineId}
+          timelineType={timelineType}
           title={title}
           toggleLock={onToggleLock}
           updateDescription={updateDescription}
@@ -196,6 +200,7 @@ export const Properties = React.memo<Props>(
           showUsersView={title.length > 0}
           status={status}
           timelineId={timelineId}
+          timelineType={timelineType}
           title={title}
           updateDescription={updateDescription}
           updateNote={updateNote}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx
index cd6233334c5de..b142484872813 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/new_template_timeline.test.tsx
@@ -11,6 +11,7 @@ import {
   mockGlobalState,
   apolloClientObservable,
   SUB_PLUGINS_REDUCER,
+  kibanaObservable,
   createSecuritySolutionStorageMock,
 } from '../../../../common/mock';
 import { createStore, State } from '../../../../common/store';
@@ -26,7 +27,13 @@ jest.mock('../../../../common/lib/kibana', () => {
 describe('NewTemplateTimeline', () => {
   const state: State = mockGlobalState;
   const { storage } = createSecuritySolutionStorageMock();
-  const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  const store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
   const mockClosePopover = jest.fn();
   const mockTitle = 'NEW_TIMELINE';
   let wrapper: ReactWrapper;
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
index 52766422e49c3..4673ba662b2e9 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_left.tsx
@@ -10,9 +10,12 @@ import React from 'react';
 import styled from 'styled-components';
 import { Description, Name, NotesButton, StarIcon } from './helpers';
 import { AssociateNote, UpdateNote } from '../../notes/helpers';
+
 import { Note } from '../../../../common/lib/note';
 import { SuperDatePicker } from '../../../../common/components/super_date_picker';
 
+import { TimelineTypeLiteral, TimelineStatusLiteral } from '../../../../../common/types/timeline';
+
 import * as i18n from './translations';
 
 type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void;
@@ -22,6 +25,7 @@ type UpdateDescription = ({ id, description }: { id: string; description: string
 interface Props {
   isFavorite: boolean;
   timelineId: string;
+  timelineType: TimelineTypeLiteral;
   updateIsFavorite: UpdateIsFavorite;
   showDescription: boolean;
   description: string;
@@ -29,6 +33,7 @@ interface Props {
   updateTitle: UpdateTitle;
   updateDescription: UpdateDescription;
   showNotes: boolean;
+  status: TimelineStatusLiteral;
   associateNote: AssociateNote;
   showNotesFromWidth: boolean;
   getNotesByIds: (noteIds: string[]) => Note[];
@@ -77,8 +82,10 @@ export const PropertiesLeft = React.memo<Props>(
     showDescription,
     description,
     title,
+    timelineType,
     updateTitle,
     updateDescription,
+    status,
     showNotes,
     showNotesFromWidth,
     associateNote,
@@ -120,10 +127,12 @@ export const PropertiesLeft = React.memo<Props>(
             noteIds={noteIds}
             showNotes={showNotes}
             size="l"
+            status={status}
             text={i18n.NOTES}
             toggleShowNotes={onToggleShowNotes}
             toolTip={i18n.NOTES_TOOL_TIP}
             updateNote={updateNote}
+            timelineType={timelineType}
           />
         </EuiFlexItem>
       ) : null}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx
index ae167515495f7..a36e841f3f871 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.test.tsx
@@ -9,7 +9,7 @@ import React from 'react';
 
 import { PropertiesRight } from './properties_right';
 import { useKibana } from '../../../../common/lib/kibana';
-import { TimelineStatus } from '../../../../../common/types/timeline';
+import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline';
 import { disableTemplate } from '../../../../../common/constants';
 
 jest.mock('../../../../common/lib/kibana', () => {
@@ -67,6 +67,7 @@ describe('Properties Right', () => {
     onOpenTimelineModal: jest.fn(),
     status: TimelineStatus.active,
     showTimelineModal: false,
+    timelineType: TimelineType.default,
     title: 'title',
     updateNote: jest.fn(),
   };
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
index e20a3db80d881..7a9fe85ae402b 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/properties_right.tsx
@@ -17,7 +17,7 @@ import {
 import { NewTimeline, Description, NotesButton, NewCase, ExistingCase } from './helpers';
 
 import { disableTemplate } from '../../../../../common/constants';
-import { TimelineStatus } from '../../../../../common/types/timeline';
+import { TimelineStatusLiteral, TimelineTypeLiteral } from '../../../../../common/types/timeline';
 
 import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect';
 import { useKibana } from '../../../../common/lib/kibana';
@@ -83,9 +83,10 @@ interface PropertiesRightComponentProps {
   showNotesFromWidth: boolean;
   showTimelineModal: boolean;
   showUsersView: boolean;
-  status: TimelineStatus;
+  status: TimelineStatusLiteral;
   timelineId: string;
   title: string;
+  timelineType: TimelineTypeLiteral;
   updateDescription: UpdateDescription;
   updateNote: UpdateNote;
   usersViewing: string[];
@@ -111,6 +112,7 @@ const PropertiesRightComponent: React.FC<PropertiesRightComponentProps> = ({
   showTimelineModal,
   showUsersView,
   status,
+  timelineType,
   timelineId,
   title,
   updateDescription,
@@ -203,6 +205,8 @@ const PropertiesRightComponent: React.FC<PropertiesRightComponentProps> = ({
                     noteIds={noteIds}
                     showNotes={showNotes}
                     size="l"
+                    status={status}
+                    timelineType={timelineType}
                     text={i18n.NOTES}
                     toggleShowNotes={onToggleShowNotes}
                     toolTip={i18n.NOTES_TOOL_TIP}
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
index 2568f41275401..561f8e513aa09 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/translations.ts
@@ -112,7 +112,7 @@ export const NEW_TIMELINE = i18n.translate(
 export const NEW_TEMPLATE_TIMELINE = i18n.translate(
   'xpack.securitySolution.timeline.properties.newTemplateTimelineButtonLabel',
   {
-    defaultMessage: 'Create template timeline',
+    defaultMessage: 'Create new timeline template',
   }
 );
 
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx
index 2b67cf75dcff9..0ff4c0a70fff2 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.test.tsx
@@ -30,11 +30,7 @@ describe('SelectableTimeline', () => {
     };
   });
 
-  const {
-    SelectableTimeline,
-
-    ORIGINAL_PAGE_SIZE,
-  } = jest.requireActual('./');
+  const { SelectableTimeline, ORIGINAL_PAGE_SIZE } = jest.requireActual('./');
 
   const props = {
     hideUntitled: false,
@@ -94,8 +90,10 @@ describe('SelectableTimeline', () => {
         sortField: SortFieldTimeline.updated,
         sortOrder: Direction.desc,
       },
+      status: null,
       onlyUserFavorite: false,
       timelineType: TimelineType.default,
+      templateTimelineType: null,
     };
     beforeAll(() => {
       mount(<SelectableTimeline {...props} />);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
index 56c7c3dcfeb76..dacaf325130d7 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx
@@ -33,6 +33,7 @@ import * as i18nTimeline from '../../open_timeline/translations';
 import { OpenTimelineResult } from '../../open_timeline/types';
 import { getEmptyTagValue } from '../../../../common/components/empty_value';
 import * as i18n from '../translations';
+import { useTimelineStatus } from '../../open_timeline/use_timeline_status';
 
 const MyEuiFlexItem = styled(EuiFlexItem)`
   display: inline-block;
@@ -118,6 +119,7 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
   const [onlyFavorites, setOnlyFavorites] = useState(false);
   const [searchRef, setSearchRef] = useState<HTMLElement | null>(null);
   const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline();
+  const { timelineStatus, templateTimelineType } = useTimelineStatus({ timelineType });
 
   const onSearchTimeline = useCallback((val) => {
     setSearchTimelineValue(val);
@@ -249,24 +251,31 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({
     },
   };
 
-  useEffect(
-    () =>
-      fetchAllTimeline({
-        pageInfo: {
-          pageIndex: 1,
-          pageSize,
-        },
-        search: searchTimelineValue,
-        sort: {
-          sortField: SortFieldTimeline.updated,
-          sortOrder: Direction.desc,
-        },
-        onlyUserFavorite: onlyFavorites,
-        timelineType,
-      }),
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-    [onlyFavorites, pageSize, searchTimelineValue, timelineType]
-  );
+  useEffect(() => {
+    fetchAllTimeline({
+      pageInfo: {
+        pageIndex: 1,
+        pageSize,
+      },
+      search: searchTimelineValue,
+      sort: {
+        sortField: SortFieldTimeline.updated,
+        sortOrder: Direction.desc,
+      },
+      onlyUserFavorite: onlyFavorites,
+      status: timelineStatus,
+      timelineType,
+      templateTimelineType,
+    });
+  }, [
+    fetchAllTimeline,
+    onlyFavorites,
+    pageSize,
+    searchTimelineValue,
+    timelineType,
+    timelineStatus,
+    templateTimelineType,
+  ]);
 
   return (
     <EuiSelectableContainer isLoading={loading}>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
index 79ec58711e06c..b58505546c341 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx
@@ -24,6 +24,7 @@ import { TimelineComponent, Props as TimelineComponentProps } from './timeline';
 import { Sort } from './body/sort';
 import { mockDataProviders } from './data_providers/mock/mock_data_providers';
 import { useMountAppended } from '../../../common/utils/use_mount_appended';
+import { TimelineStatus } from '../../../../common/types/timeline';
 
 jest.mock('../../../common/lib/kibana');
 jest.mock('./properties/properties_right');
@@ -96,6 +97,7 @@ describe('Timeline', () => {
       showCallOutUnauthorizedMsg: false,
       start: startDate,
       sort,
+      status: TimelineStatus.active,
       toggleColumn: jest.fn(),
       usersViewing: ['elastic'],
     };
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
index 85e3d5d9478b6..07d4b004d2eda 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx
@@ -40,6 +40,7 @@ import {
   IIndexPattern,
 } from '../../../../../../../src/plugins/data/public';
 import { useManageTimeline } from '../manage_timeline';
+import { TimelineStatusLiteral } from '../../../../common/types/timeline';
 
 const TimelineContainer = styled.div`
   height: 100%;
@@ -110,6 +111,7 @@ export interface Props {
   showCallOutUnauthorizedMsg: boolean;
   start: number;
   sort: Sort;
+  status: TimelineStatusLiteral;
   toggleColumn: (column: ColumnHeaderOptions) => void;
   usersViewing: string[];
 }
@@ -141,6 +143,7 @@ export const TimelineComponent: React.FC<Props> = ({
   show,
   showCallOutUnauthorizedMsg,
   start,
+  status,
   sort,
   toggleColumn,
   usersViewing,
@@ -214,6 +217,7 @@ export const TimelineComponent: React.FC<Props> = ({
             onToggleDataProviderExcluded={onToggleDataProviderExcluded}
             show={show}
             showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg}
+            status={status}
           />
         </TimelineHeaderContainer>
       </StyledEuiFlyoutHeader>
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts
index 60d000fe78184..5cbc922f09c9a 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.gql_query.ts
@@ -13,6 +13,8 @@ export const allTimelinesQuery = gql`
     $sort: SortTimeline
     $onlyUserFavorite: Boolean
     $timelineType: TimelineType
+    $templateTimelineType: TemplateTimelineType
+    $status: TimelineStatus
   ) {
     getAllTimeline(
       pageInfo: $pageInfo
@@ -20,8 +22,15 @@ export const allTimelinesQuery = gql`
       sort: $sort
       onlyUserFavorite: $onlyUserFavorite
       timelineType: $timelineType
+      templateTimelineType: $templateTimelineType
+      status: $status
     ) {
       totalCount
+      defaultTimelineCount
+      templateTimelineCount
+      elasticTemplateTimelineCount
+      customTemplateTimelineCount
+      favoriteCount
       timeline {
         savedObjectId
         description
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
index f025cf15181c3..17cc0f64de039 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx
@@ -22,7 +22,11 @@ import { useApolloClient } from '../../../common/utils/apollo_context';
 
 import { allTimelinesQuery } from './index.gql_query';
 import * as i18n from '../../pages/translations';
-import { TimelineTypeLiteralWithNull } from '../../../../common/types/timeline';
+import {
+  TimelineTypeLiteralWithNull,
+  TimelineStatusLiteralWithNull,
+  TemplateTimelineTypeLiteralWithNull,
+} from '../../../../common/types/timeline';
 
 export interface AllTimelinesArgs {
   fetchAllTimeline: ({
@@ -30,11 +34,17 @@ export interface AllTimelinesArgs {
     pageInfo,
     search,
     sort,
+    status,
     timelineType,
   }: AllTimelinesVariables) => void;
   timelines: OpenTimelineResult[];
   loading: boolean;
   totalCount: number;
+  customTemplateTimelineCount: number;
+  defaultTimelineCount: number;
+  elasticTemplateTimelineCount: number;
+  templateTimelineCount: number;
+  favoriteCount: number;
 }
 
 export interface AllTimelinesVariables {
@@ -42,7 +52,9 @@ export interface AllTimelinesVariables {
   pageInfo: PageInfoTimeline;
   search: string;
   sort: SortTimeline;
+  status: TimelineStatusLiteralWithNull;
   timelineType: TimelineTypeLiteralWithNull;
+  templateTimelineType: TemplateTimelineTypeLiteralWithNull;
 }
 
 export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES';
@@ -76,6 +88,7 @@ export const getAllTimeline = memoizeOne(
             )
           : null,
       savedObjectId: timeline.savedObjectId,
+      status: timeline.status,
       title: timeline.title,
       updated: timeline.updated,
       updatedBy: timeline.updatedBy,
@@ -90,27 +103,39 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
     loading: false,
     totalCount: 0,
     timelines: [],
+    customTemplateTimelineCount: 0,
+    defaultTimelineCount: 0,
+    elasticTemplateTimelineCount: 0,
+    templateTimelineCount: 0,
+    favoriteCount: 0,
   });
 
   const fetchAllTimeline = useCallback(
-    ({ onlyUserFavorite, pageInfo, search, sort, timelineType }: AllTimelinesVariables) => {
+    async ({
+      onlyUserFavorite,
+      pageInfo,
+      search,
+      sort,
+      status,
+      timelineType,
+      templateTimelineType,
+    }: AllTimelinesVariables) => {
       let didCancel = false;
       const abortCtrl = new AbortController();
 
       const fetchData = async () => {
         try {
           if (apolloClient != null) {
-            setAllTimelines({
-              ...allTimelines,
-              loading: true,
-            });
+            setAllTimelines((prevState) => ({ ...prevState, loading: true }));
 
             const variables: GetAllTimeline.Variables = {
               onlyUserFavorite,
               pageInfo,
               search,
               sort,
+              status,
               timelineType,
+              templateTimelineType,
             };
             const response = await apolloClient.query<
               GetAllTimeline.Query,
@@ -125,8 +150,16 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
                 },
               },
             });
-            const totalCount = response?.data?.getAllTimeline?.totalCount ?? 0;
-            const timelines = response?.data?.getAllTimeline?.timeline ?? [];
+            const getAllTimelineResponse = response?.data?.getAllTimeline;
+            const totalCount = getAllTimelineResponse?.totalCount ?? 0;
+            const timelines = getAllTimelineResponse?.timeline ?? [];
+            const customTemplateTimelineCount =
+              getAllTimelineResponse?.customTemplateTimelineCount ?? 0;
+            const defaultTimelineCount = getAllTimelineResponse?.defaultTimelineCount ?? 0;
+            const elasticTemplateTimelineCount =
+              getAllTimelineResponse?.elasticTemplateTimelineCount ?? 0;
+            const templateTimelineCount = getAllTimelineResponse?.templateTimelineCount ?? 0;
+            const favoriteCount = getAllTimelineResponse?.favoriteCount ?? 0;
             if (!didCancel) {
               dispatch(
                 inputsActions.setQuery({
@@ -141,6 +174,11 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
                 loading: false,
                 totalCount,
                 timelines: getAllTimeline(JSON.stringify(variables), timelines as TimelineResult[]),
+                customTemplateTimelineCount,
+                defaultTimelineCount,
+                elasticTemplateTimelineCount,
+                templateTimelineCount,
+                favoriteCount,
               });
             }
           }
@@ -155,6 +193,11 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
               loading: false,
               totalCount: 0,
               timelines: [],
+              customTemplateTimelineCount: 0,
+              defaultTimelineCount: 0,
+              elasticTemplateTimelineCount: 0,
+              templateTimelineCount: 0,
+              favoriteCount: 0,
             });
           }
         }
@@ -165,7 +208,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => {
         abortCtrl.abort();
       };
     },
-    [apolloClient, allTimelines, dispatch, dispatchToaster]
+    [apolloClient, dispatch, dispatchToaster]
   );
 
   useEffect(() => {
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts
index 26373fa1a825d..8a2f91d7171f7 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts
@@ -165,6 +165,7 @@ describe('persistTimeline', () => {
         },
       },
     };
+
     const version = null;
     const fetchMock = jest.fn();
     const postMock = jest.fn();
@@ -180,7 +181,11 @@ describe('persistTimeline', () => {
           patch: patchMock.mockReturnValue(mockPatchTimelineResponse),
         },
       });
-      api.persistTimeline({ timelineId, timeline: initialDraftTimeline, version });
+      api.persistTimeline({
+        timelineId,
+        timeline: initialDraftTimeline,
+        version,
+      });
     });
 
     afterAll(() => {
diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts
index a2277897e99bf..fbd89268880db 100644
--- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts
+++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts
@@ -12,6 +12,8 @@ import {
   TimelineResponse,
   TimelineResponseType,
   TimelineStatus,
+  TimelineErrorResponseType,
+  TimelineErrorResponse,
 } from '../../../common/types/timeline';
 import { TimelineInput, TimelineType } from '../../graphql/types';
 import {
@@ -48,6 +50,12 @@ const decodeTimelineResponse = (respTimeline?: TimelineResponse) =>
     fold(throwErrors(createToasterPlainError), identity)
   );
 
+const decodeTimelineErrorResponse = (respTimeline?: TimelineErrorResponse) =>
+  pipe(
+    TimelineErrorResponseType.decode(respTimeline),
+    fold(throwErrors(createToasterPlainError), identity)
+  );
+
 const postTimeline = async ({ timeline }: RequestPostTimeline): Promise<TimelineResponse> => {
   const response = await KibanaServices.get().http.post<TimelineResponse>(TIMELINE_URL, {
     method: 'POST',
@@ -61,12 +69,19 @@ const patchTimeline = async ({
   timelineId,
   timeline,
   version,
-}: RequestPatchTimeline): Promise<TimelineResponse> => {
-  const response = await KibanaServices.get().http.patch<TimelineResponse>(TIMELINE_URL, {
-    method: 'PATCH',
-    body: JSON.stringify({ timeline, timelineId, version }),
-  });
-
+}: RequestPatchTimeline): Promise<TimelineResponse | TimelineErrorResponse> => {
+  let response = null;
+  try {
+    response = await KibanaServices.get().http.patch<TimelineResponse>(TIMELINE_URL, {
+      method: 'PATCH',
+      body: JSON.stringify({ timeline, timelineId, version }),
+    });
+  } catch (err) {
+    // For Future developer
+    // We are not rejecting our promise here because we had issue with our RXJS epic
+    // the issue we were not able to pass the right object to it so we did manage the error in the success
+    return Promise.resolve(decodeTimelineErrorResponse(err.body));
+  }
   return decodeTimelineResponse(response);
 };
 
@@ -74,17 +89,31 @@ export const persistTimeline = async ({
   timelineId,
   timeline,
   version,
-}: RequestPersistTimeline): Promise<TimelineResponse> => {
-  if (timelineId == null && timeline.status === TimelineStatus.draft) {
-    const draftTimeline = await cleanDraftTimeline({ timelineType: timeline.timelineType! });
+}: RequestPersistTimeline): Promise<TimelineResponse | TimelineErrorResponse> => {
+  if (timelineId == null && timeline.status === TimelineStatus.draft && timeline) {
+    const draftTimeline = await cleanDraftTimeline({
+      timelineType: timeline.timelineType!,
+      templateTimelineId: timeline.templateTimelineId ?? undefined,
+      templateTimelineVersion: timeline.templateTimelineVersion ?? undefined,
+    });
+
+    const templateTimelineInfo =
+      timeline.timelineType! === TimelineType.template
+        ? {
+            templateTimelineId:
+              draftTimeline.data.persistTimeline.timeline.templateTimelineId ??
+              timeline.templateTimelineId,
+            templateTimelineVersion:
+              draftTimeline.data.persistTimeline.timeline.templateTimelineVersion ??
+              timeline.templateTimelineVersion,
+          }
+        : {};
 
     return patchTimeline({
       timelineId: draftTimeline.data.persistTimeline.timeline.savedObjectId,
       timeline: {
         ...timeline,
-        templateTimelineId: draftTimeline.data.persistTimeline.timeline.templateTimelineId,
-        templateTimelineVersion:
-          draftTimeline.data.persistTimeline.timeline.templateTimelineVersion,
+        ...templateTimelineInfo,
       },
       version: draftTimeline.data.persistTimeline.timeline.version ?? '',
     });
@@ -147,12 +176,24 @@ export const getDraftTimeline = async ({
 
 export const cleanDraftTimeline = async ({
   timelineType,
+  templateTimelineId,
+  templateTimelineVersion,
 }: {
   timelineType: TimelineType;
+  templateTimelineId?: string;
+  templateTimelineVersion?: number;
 }): Promise<TimelineResponse> => {
+  const templateTimelineInfo =
+    timelineType === TimelineType.template
+      ? {
+          templateTimelineId,
+          templateTimelineVersion,
+        }
+      : {};
   const response = await KibanaServices.get().http.post<TimelineResponse>(TIMELINE_DRAFT_URL, {
     body: JSON.stringify({
       timelineType,
+      ...templateTimelineInfo,
     }),
   });
 
diff --git a/x-pack/plugins/security_solution/public/timelines/pages/translations.ts b/x-pack/plugins/security_solution/public/timelines/pages/translations.ts
index 3ec98d47c67ea..5a9f80013a3ed 100644
--- a/x-pack/plugins/security_solution/public/timelines/pages/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/pages/translations.ts
@@ -30,3 +30,17 @@ export const ERROR_FETCHING_TIMELINES_TITLE = i18n.translate(
     defaultMessage: 'Failed to query all timelines data',
   }
 );
+
+export const UPDATE_TIMELINE_ERROR_TITLE = i18n.translate(
+  'xpack.securitySolution.timelines.updateTimelineErrorTitle',
+  {
+    defaultMessage: 'Timeline error',
+  }
+);
+
+export const UPDATE_TIMELINE_ERROR_TEXT = i18n.translate(
+  'xpack.securitySolution.timelines.updateTimelineErrorText',
+  {
+    defaultMessage: 'Something went wrong',
+  }
+);
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
index 55e6849fdb6c4..8fd75547cc539 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts
@@ -70,6 +70,8 @@ export const createTimeline = actionCreator<{
   showCheckboxes?: boolean;
   showRowRenderers?: boolean;
   timelineType?: TimelineTypeLiteral;
+  templateTimelineId?: string;
+  templateTimelineVersion?: number;
 }>('CREATE_TIMELINE');
 
 export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT');
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
index 2155dc804aa7e..94acb9d92075b 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts
@@ -33,7 +33,7 @@ import {
   Filter,
   MatchAllFilter,
 } from '../../../../../../.../../../src/plugins/data/public';
-import { TimelineStatus } from '../../../../common/types/timeline';
+import { TimelineStatus, TimelineErrorResponse } from '../../../../common/types/timeline';
 import { inputsModel } from '../../../common/store/inputs';
 import {
   TimelineType,
@@ -43,6 +43,10 @@ import {
 } from '../../../graphql/types';
 import { addError } from '../../../common/store/app/actions';
 
+import { persistTimeline } from '../../containers/api';
+import { ALL_TIMELINE_QUERY_ID } from '../../containers/all';
+import * as i18n from '../../pages/translations';
+
 import {
   applyKqlFilterQuery,
   addProvider,
@@ -79,8 +83,6 @@ import { isNotNull } from './helpers';
 import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue';
 import { myEpicTimelineId } from './my_epic_timeline_id';
 import { ActionTimeline, TimelineEpicDependencies } from './types';
-import { persistTimeline } from '../../containers/api';
-import { ALL_TIMELINE_QUERY_ID } from '../../containers/all';
 
 const timelineActionsType = [
   applyKqlFilterQuery.type,
@@ -121,6 +123,7 @@ export const createTimelineEpic = <State>(): Epic<
     timelineByIdSelector,
     timelineTimeRangeSelector,
     apolloClient$,
+    kibana$,
   }
 ) => {
   const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull));
@@ -146,13 +149,24 @@ export const createTimelineEpic = <State>(): Epic<
         if (action.type === addError.type) {
           return true;
         }
-        if (action.type === createTimeline.type && isItAtimelineAction(timelineId)) {
+        if (
+          isItAtimelineAction(timelineId) &&
+          timelineObj != null &&
+          timelineObj.status != null &&
+          TimelineStatus.immutable === timelineObj.status
+        ) {
+          return false;
+        } else if (action.type === createTimeline.type && isItAtimelineAction(timelineId)) {
           myEpicTimelineId.setTimelineVersion(null);
           myEpicTimelineId.setTimelineId(null);
+          myEpicTimelineId.setTemplateTimelineId(null);
+          myEpicTimelineId.setTemplateTimelineVersion(null);
         } else if (action.type === addTimeline.type && isItAtimelineAction(timelineId)) {
           const addNewTimeline: TimelineModel = get('payload.timeline', action);
           myEpicTimelineId.setTimelineId(addNewTimeline.savedObjectId);
           myEpicTimelineId.setTimelineVersion(addNewTimeline.version);
+          myEpicTimelineId.setTemplateTimelineId(addNewTimeline.templateTimelineId);
+          myEpicTimelineId.setTemplateTimelineVersion(addNewTimeline.templateTimelineVersion);
           return true;
         } else if (
           timelineActionsType.includes(action.type) &&
@@ -176,6 +190,8 @@ export const createTimelineEpic = <State>(): Epic<
         const action: ActionTimeline = get('action', objAction);
         const timelineId = myEpicTimelineId.getTimelineId();
         const version = myEpicTimelineId.getTimelineVersion();
+        const templateTimelineId = myEpicTimelineId.getTemplateTimelineId();
+        const templateTimelineVersion = myEpicTimelineId.getTemplateTimelineVersion();
 
         if (timelineNoteActionsType.includes(action.type)) {
           return epicPersistNote(
@@ -211,13 +227,37 @@ export const createTimelineEpic = <State>(): Epic<
             persistTimeline({
               timelineId,
               version,
-              timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange),
+              timeline: {
+                ...convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange),
+                templateTimelineId,
+                templateTimelineVersion,
+              },
             })
           ).pipe(
-            withLatestFrom(timeline$, allTimelineQuery$),
-            mergeMap(([result, recentTimeline, allTimelineQuery]) => {
+            withLatestFrom(timeline$, allTimelineQuery$, kibana$),
+            mergeMap(([result, recentTimeline, allTimelineQuery, kibana]) => {
+              const error = result as TimelineErrorResponse;
+              if (error.status_code != null && error.status_code === 405) {
+                kibana.notifications!.toasts.addDanger({
+                  title: i18n.UPDATE_TIMELINE_ERROR_TITLE,
+                  text: error.message ?? i18n.UPDATE_TIMELINE_ERROR_TEXT,
+                });
+                return [
+                  endTimelineSaving({
+                    id: action.payload.id,
+                  }),
+                ];
+              }
+
               const savedTimeline = recentTimeline[action.payload.id];
               const response: ResponseTimeline = get('data.persistTimeline', result);
+              if (response == null) {
+                return [
+                  endTimelineSaving({
+                    id: action.payload.id,
+                  }),
+                ];
+              }
               const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : [];
 
               if (allTimelineQuery.refetch != null) {
@@ -264,6 +304,12 @@ export const createTimelineEpic = <State>(): Epic<
                     myEpicTimelineId.setTimelineVersion(
                       updatedTimeline[get('payload.id', checkAction)].version
                     );
+                    myEpicTimelineId.setTemplateTimelineId(
+                      updatedTimeline[get('payload.id', checkAction)].templateTimelineId
+                    );
+                    myEpicTimelineId.setTemplateTimelineVersion(
+                      updatedTimeline[get('payload.id', checkAction)].templateTimelineVersion
+                    );
                     return true;
                   }
                   return false;
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
index 34778aba7873c..388869194085c 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx
@@ -15,6 +15,7 @@ import {
   defaultHeaders,
   createSecuritySolutionStorageMock,
   mockIndexPattern,
+  kibanaObservable,
 } from '../../../common/mock';
 
 import { createStore, State } from '../../../common/store';
@@ -38,6 +39,7 @@ import { Direction } from '../../../graphql/types';
 
 import { addTimelineInStorage } from '../../containers/local_storage';
 import { isPageTimeline } from './epic_local_storage';
+import { TimelineStatus } from '../../../../common/types/timeline';
 
 jest.mock('../../containers/local_storage');
 
@@ -50,7 +52,13 @@ const addTimelineInStorageMock = addTimelineInStorage as jest.Mock;
 describe('epicLocalStorage', () => {
   const state: State = mockGlobalState;
   const { storage } = createSecuritySolutionStorageMock();
-  let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+  let store = createStore(
+    state,
+    SUB_PLUGINS_REDUCER,
+    apolloClientObservable,
+    kibanaObservable,
+    storage
+  );
 
   let props = {} as TimelineComponentProps;
   const sort: Sort = {
@@ -63,7 +71,13 @@ describe('epicLocalStorage', () => {
   const indexPattern = mockIndexPattern;
 
   beforeEach(() => {
-    store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable, storage);
+    store = createStore(
+      state,
+      SUB_PLUGINS_REDUCER,
+      apolloClientObservable,
+      kibanaObservable,
+      storage
+    );
     props = {
       browserFields: mockBrowserFields,
       columns: defaultHeaders,
@@ -89,6 +103,7 @@ describe('epicLocalStorage', () => {
       show: true,
       showCallOutUnauthorizedMsg: false,
       start: startDate,
+      status: TimelineStatus.active,
       sort,
       toggleColumn: jest.fn(),
       usersViewing: ['elastic'],
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
index c0615d36f7a2e..33770aacde6bb 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts
@@ -6,6 +6,7 @@
 
 import { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp';
 
+import uuid from 'uuid';
 import { Filter } from '../../../../../../../src/plugins/data/public';
 
 import { disableTemplate } from '../../../../common/constants';
@@ -19,7 +20,7 @@ import {
 } from '../../../timelines/components/timeline/data_providers/data_provider';
 import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model';
 import { TimelineNonEcsData } from '../../../graphql/types';
-import { TimelineTypeLiteral } from '../../../../common/types/timeline';
+import { TimelineTypeLiteral, TimelineType } from '../../../../common/types/timeline';
 
 import { timelineDefaults } from './defaults';
 import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model';
@@ -158,28 +159,38 @@ export const addNewTimeline = ({
   showRowRenderers = true,
   timelineById,
   timelineType,
-}: AddNewTimelineParams): TimelineById => ({
-  ...timelineById,
-  [id]: {
-    id,
-    ...timelineDefaults,
-    columns,
-    dataProviders,
-    dateRange,
-    filters,
-    itemsPerPage,
-    kqlQuery,
-    sort,
-    show,
-    savedObjectId: null,
-    version: null,
-    isSaving: false,
-    isLoading: false,
-    showCheckboxes,
-    showRowRenderers,
-    timelineType: !disableTemplate ? timelineType : timelineDefaults.timelineType,
-  },
-});
+}: AddNewTimelineParams): TimelineById => {
+  const templateTimelineInfo =
+    !disableTemplate && timelineType === TimelineType.template
+      ? {
+          templateTimelineId: uuid.v4(),
+          templateTimelineVersion: 1,
+        }
+      : {};
+  return {
+    ...timelineById,
+    [id]: {
+      id,
+      ...timelineDefaults,
+      columns,
+      dataProviders,
+      dateRange,
+      filters,
+      itemsPerPage,
+      kqlQuery,
+      sort,
+      show,
+      savedObjectId: null,
+      version: null,
+      isSaving: false,
+      isLoading: false,
+      showCheckboxes,
+      showRowRenderers,
+      timelineType: !disableTemplate ? timelineType : timelineDefaults.timelineType,
+      ...templateTimelineInfo,
+    },
+  };
+};
 
 interface PinTimelineEventParams {
   id: string;
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/manage_timeline_id.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/manage_timeline_id.tsx
index d68c9bd42d974..6f8666a349d78 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/manage_timeline_id.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/manage_timeline_id.tsx
@@ -7,6 +7,8 @@
 export class ManageEpicTimelineId {
   private timelineId: string | null = null;
   private version: string | null = null;
+  private templateTimelineId: string | null = null;
+  private templateVersion: number | null = null;
 
   public getTimelineId(): string | null {
     return this.timelineId;
@@ -16,6 +18,14 @@ export class ManageEpicTimelineId {
     return this.version;
   }
 
+  public getTemplateTimelineId(): string | null {
+    return this.templateTimelineId;
+  }
+
+  public getTemplateTimelineVersion(): number | null {
+    return this.templateVersion;
+  }
+
   public setTimelineId(timelineId: string | null) {
     this.timelineId = timelineId;
   }
@@ -23,4 +33,12 @@ export class ManageEpicTimelineId {
   public setTimelineVersion(version: string | null) {
     this.version = version;
   }
+
+  public setTemplateTimelineId(templateTimelineId: string | null) {
+    this.templateTimelineId = templateTimelineId;
+  }
+
+  public setTemplateTimelineVersion(templateVersion: number | null) {
+    this.templateVersion = templateVersion;
+  }
 }
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
index e8ea3c8d16e3a..57895fea8f8ff 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts
@@ -160,7 +160,6 @@ export type SubsetTimelineModel = Readonly<
     | 'isLoading'
     | 'savedObjectId'
     | 'version'
-    | 'timelineType'
     | 'status'
   >
 >;
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
index 30b7f73c839d1..4072b4ac2f78b 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts
@@ -137,24 +137,26 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
         timelineType = TimelineType.default,
         filters,
       }
-    ) => ({
-      ...state,
-      timelineById: addNewTimeline({
-        columns,
-        dataProviders,
-        dateRange,
-        filters,
-        id,
-        itemsPerPage,
-        kqlQuery,
-        sort,
-        show,
-        showCheckboxes,
-        showRowRenderers,
-        timelineById: state.timelineById,
-        timelineType,
-      }),
-    })
+    ) => {
+      return {
+        ...state,
+        timelineById: addNewTimeline({
+          columns,
+          dataProviders,
+          dateRange,
+          filters,
+          id,
+          itemsPerPage,
+          kqlQuery,
+          sort,
+          show,
+          showCheckboxes,
+          showRowRenderers,
+          timelineById: state.timelineById,
+          timelineType,
+        }),
+      };
+    }
   )
   .case(upsertColumn, (state, { column, id, index }) => ({
     ...state,
diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
index 65798648f92c6..c64ed608339b6 100644
--- a/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
+++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/types.ts
@@ -10,6 +10,8 @@ import { Storage } from '../../../../../../../src/plugins/kibana_utils/public';
 import { AppApolloClient } from '../../../common/lib/lib';
 import { inputsModel } from '../../../common/store/inputs';
 import { NotesById } from '../../../common/store/app/model';
+import { StartServices } from '../../../types';
+
 import { TimelineModel } from './model';
 
 export interface AutoSavedWarningMsg {
@@ -53,5 +55,6 @@ export interface TimelineEpicDependencies<State> {
   selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery;
   selectNotesByIdSelector: (state: State) => NotesById;
   apolloClient$: Observable<AppApolloClient>;
+  kibana$: Observable<StartServices>;
   storage: Storage;
 }
diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts
index 6d59824702cfa..e212289458ed1 100644
--- a/x-pack/plugins/security_solution/public/types.ts
+++ b/x-pack/plugins/security_solution/public/types.ts
@@ -19,6 +19,7 @@ import {
   TriggersAndActionsUIPublicPluginStart as TriggersActionsStart,
 } from '../../triggers_actions_ui/public';
 import { SecurityPluginSetup } from '../../security/public';
+import { AppFrontendLibs } from './common/lib/lib';
 
 export interface SetupPlugins {
   home: HomePublicPluginSetup;
@@ -47,3 +48,7 @@ export type StartServices = CoreStart &
 export interface PluginSetup {}
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
 export interface PluginStart {}
+
+export interface AppObservableLibs extends AppFrontendLibs {
+  kibana: CoreStart;
+}
diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts
index a40ef5466c780..ab729bae6474d 100644
--- a/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts
+++ b/x-pack/plugins/security_solution/server/graphql/timeline/resolvers.ts
@@ -53,7 +53,9 @@ export const createTimelineResolvers = (
         args.pageInfo || null,
         args.search || null,
         args.sort || null,
-        args.timelineType || null
+        args.status || null,
+        args.timelineType || null,
+        args.templateTimelineType || null
       );
     },
   },
diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
index b9aa8534ab0e9..a9d07389797db 100644
--- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
+++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts
@@ -133,6 +133,12 @@ export const timelineSchema = gql`
   enum TimelineStatus {
     active
     draft
+    immutable
+  }
+
+  enum TemplateTimelineType {
+    elastic
+    custom
   }
 
   input TimelineInput {
@@ -277,6 +283,11 @@ export const timelineSchema = gql`
   type ResponseTimelines {
     timeline: [TimelineResult]!
     totalCount: Float
+    defaultTimelineCount: Float
+    templateTimelineCount: Float
+    elasticTemplateTimelineCount: Float
+    customTemplateTimelineCount: Float
+    favoriteCount: Float
   }
 
   #########################
@@ -285,7 +296,7 @@ export const timelineSchema = gql`
 
   extend type Query {
     getOneTimeline(id: ID!): TimelineResult!
-    getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType): ResponseTimelines!
+    getAllTimeline(pageInfo: PageInfoTimeline, search: String, sort: SortTimeline, onlyUserFavorite: Boolean, timelineType: TimelineType, templateTimelineType: TemplateTimelineType, status: TimelineStatus): ResponseTimelines!
   }
 
   extend type Mutation {
diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts
index 40666b6193928..2db3052bae66f 100644
--- a/x-pack/plugins/security_solution/server/graphql/types.ts
+++ b/x-pack/plugins/security_solution/server/graphql/types.ts
@@ -347,6 +347,7 @@ export enum TlsFields {
 export enum TimelineStatus {
   active = 'active',
   draft = 'draft',
+  immutable = 'immutable',
 }
 
 export enum TimelineType {
@@ -361,6 +362,11 @@ export enum SortFieldTimeline {
   created = 'created',
 }
 
+export enum TemplateTimelineType {
+  elastic = 'elastic',
+  custom = 'custom',
+}
+
 export enum NetworkDirectionEcs {
   inbound = 'inbound',
   outbound = 'outbound',
@@ -2119,6 +2125,16 @@ export interface ResponseTimelines {
   timeline: (Maybe<TimelineResult>)[];
 
   totalCount?: Maybe<number>;
+
+  defaultTimelineCount?: Maybe<number>;
+
+  templateTimelineCount?: Maybe<number>;
+
+  elasticTemplateTimelineCount?: Maybe<number>;
+
+  customTemplateTimelineCount?: Maybe<number>;
+
+  favoriteCount?: Maybe<number>;
 }
 
 export interface Mutation {
@@ -2256,6 +2272,10 @@ export interface GetAllTimelineQueryArgs {
   onlyUserFavorite?: Maybe<boolean>;
 
   timelineType?: Maybe<TimelineType>;
+
+  templateTimelineType?: Maybe<TemplateTimelineType>;
+
+  status?: Maybe<TimelineStatus>;
 }
 export interface AuthenticationsSourceArgs {
   timerange: TimerangeInput;
@@ -2714,6 +2734,10 @@ export namespace QueryResolvers {
     onlyUserFavorite?: Maybe<boolean>;
 
     timelineType?: Maybe<TimelineType>;
+
+    templateTimelineType?: Maybe<TemplateTimelineType>;
+
+    status?: Maybe<TimelineStatus>;
   }
 }
 
@@ -8670,6 +8694,24 @@ export namespace ResponseTimelinesResolvers {
     timeline?: TimelineResolver<(Maybe<TimelineResult>)[], TypeParent, TContext>;
 
     totalCount?: TotalCountResolver<Maybe<number>, TypeParent, TContext>;
+
+    defaultTimelineCount?: DefaultTimelineCountResolver<Maybe<number>, TypeParent, TContext>;
+
+    templateTimelineCount?: TemplateTimelineCountResolver<Maybe<number>, TypeParent, TContext>;
+
+    elasticTemplateTimelineCount?: ElasticTemplateTimelineCountResolver<
+      Maybe<number>,
+      TypeParent,
+      TContext
+    >;
+
+    customTemplateTimelineCount?: CustomTemplateTimelineCountResolver<
+      Maybe<number>,
+      TypeParent,
+      TContext
+    >;
+
+    favoriteCount?: FavoriteCountResolver<Maybe<number>, TypeParent, TContext>;
   }
 
   export type TimelineResolver<
@@ -8682,6 +8724,31 @@ export namespace ResponseTimelinesResolvers {
     Parent = ResponseTimelines,
     TContext = SiemContext
   > = Resolver<R, Parent, TContext>;
+  export type DefaultTimelineCountResolver<
+    R = Maybe<number>,
+    Parent = ResponseTimelines,
+    TContext = SiemContext
+  > = Resolver<R, Parent, TContext>;
+  export type TemplateTimelineCountResolver<
+    R = Maybe<number>,
+    Parent = ResponseTimelines,
+    TContext = SiemContext
+  > = Resolver<R, Parent, TContext>;
+  export type ElasticTemplateTimelineCountResolver<
+    R = Maybe<number>,
+    Parent = ResponseTimelines,
+    TContext = SiemContext
+  > = Resolver<R, Parent, TContext>;
+  export type CustomTemplateTimelineCountResolver<
+    R = Maybe<number>,
+    Parent = ResponseTimelines,
+    TContext = SiemContext
+  > = Resolver<R, Parent, TContext>;
+  export type FavoriteCountResolver<
+    R = Maybe<number>,
+    Parent = ResponseTimelines,
+    TContext = SiemContext
+  > = Resolver<R, Parent, TContext>;
 }
 
 export namespace MutationResolvers {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/README.md
index 7a48df72d6bde..fa0716ec08285 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/README.md
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/README.md
@@ -59,7 +59,7 @@ which will:
 - Delete any existing alerts you have
 - Delete any existing alert tasks you have
 - Delete any existing signal mapping, policies, and template, you might have previously had.
-- Add the latest signal index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.security_solution.signalsIndex`.
+- Add the latest signal index and its mappings using your settings from `kibana.dev.yml` environment variable of `xpack.securitySolution.signalsIndex`.
 - Posts the sample rule from `./rules/queries/query_with_rule_id.json`
 - The sample rule checks for root or admin every 5 minutes and reports that as a signal if it is a positive hit
 
@@ -171,6 +171,6 @@ go about doing so.
 To test out the functionality of large lists with rules, the user will need to import a list and post a rule with a reference to that exception list. The following outlines an example using the sample json rule provided in the repo.
 
 * First, set the appropriate env var in order to enable exceptions features`export ELASTIC_XPACK_SECURITY_SOLUTION_LISTS_FEATURE=true` and `export ELASTIC_XPACK_SECURITY_SOLUTION_EXCEPTIONS_LISTS=true` and start kibana
-* Second, import a list of ips from a file called `ci-badguys.txt`. The command should look like this: 
+* Second, import a list of ips from a file called `ci-badguys.txt`. The command should look like this:
 `cd $HOME/kibana/x-pack/plugins/lists/server/scripts && ./import_list_items_by_filename.sh ip ~/ci-badguys.txt`
 * Then, from the detection engine scripts folder (`cd kibana/x-pack/plugins/security_solution/server/lib/detection_engine/scripts`) run `./post_rule.sh rules/queries/lists/query_with_list_plugin.json`
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
index 281726d488abe..68e7f8d5e6fe1 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/pick_saved_timeline.ts
@@ -4,7 +4,6 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
-import uuid from 'uuid';
 import { isEmpty } from 'lodash/fp';
 import { AuthenticatedUser } from '../../../../security/common/model';
 import { UNAUTHENTICATED_USER } from '../../../common/constants';
@@ -28,18 +27,13 @@ export const pickSavedTimeline = (
     savedTimeline.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER;
   }
 
-  if (savedTimeline.timelineType === TimelineType.template) {
-    if (savedTimeline.templateTimelineId == null) {
-      // create template timeline
-      savedTimeline.templateTimelineId = uuid.v4();
-      savedTimeline.templateTimelineVersion = 1;
-    } else {
-      // update template timeline
-      if (savedTimeline.templateTimelineVersion != null) {
-        savedTimeline.templateTimelineVersion = savedTimeline.templateTimelineVersion + 1;
-      }
-    }
-  } else {
+  if (savedTimeline.status === TimelineStatus.draft) {
+    savedTimeline.status = !isEmpty(savedTimeline.title)
+      ? TimelineStatus.active
+      : TimelineStatus.draft;
+  }
+
+  if (savedTimeline.timelineType === TimelineType.default) {
     savedTimeline.timelineType = savedTimeline.timelineType ?? TimelineType.default;
     savedTimeline.templateTimelineId = null;
     savedTimeline.templateTimelineVersion = null;
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts
index 7180f06d853be..adfdf831f22cf 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/import_timelines.ts
@@ -138,6 +138,7 @@ export const mockGetTimelineValue = {
   kqlMode: 'filter',
   kqlQuery: { filterQuery: [] },
   title: 'My duplicate timeline',
+  timelineType: TimelineType.default,
   dateRange: { start: 1584523907294, end: 1584610307294 },
   savedQueryId: null,
   sort: { columnId: '@timestamp', sortDirection: 'desc' },
@@ -145,17 +146,25 @@ export const mockGetTimelineValue = {
   createdBy: 'angela',
   updated: 1584868346013,
   updatedBy: 'angela',
-  noteIds: [],
+  noteIds: ['d2649d40-6bc5-xxxx-0000-5db0048c6086'],
   pinnedEventIds: ['k-gi8nABm-sIqJ_scOoS'],
 };
 
 export const mockGetTemplateTimelineValue = {
   ...mockGetTimelineValue,
   timelineType: TimelineType.template,
-  templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
+  templateTimelineId: '79deb4c0-6bc1-0000-0000-f5341fb7a189',
   templateTimelineVersion: 1,
 };
 
+export const mockUniqueParsedTemplateTimelineObjects = [
+  { ...mockUniqueParsedObjects[0], ...mockGetTemplateTimelineValue, templateTimelineVersion: 2 },
+];
+
+export const mockParsedTemplateTimelineObjects = [
+  { ...mockParsedObjects[0], ...mockGetTemplateTimelineValue },
+];
+
 export const mockGetDraftTimelineValue = {
   savedObjectId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
   version: 'WzEyMjUsMV0=',
@@ -195,8 +204,51 @@ export const mockParsedTimelineObject = omit(
   mockUniqueParsedObjects[0]
 );
 
+export const mockParsedTemplateTimelineObject = omit(
+  [
+    'globalNotes',
+    'eventNotes',
+    'pinnedEventIds',
+    'version',
+    'savedObjectId',
+    'created',
+    'createdBy',
+    'updated',
+    'updatedBy',
+  ],
+  mockUniqueParsedTemplateTimelineObjects[0]
+);
+
 export const mockGetCurrentUser = {
   user: {
     username: 'mockUser',
   },
 };
+
+export const mockCreatedTimeline = {
+  savedObjectId: '79deb4c0-1111-1111-1111-f5341fb7a189',
+  version: 'WzEyMjUsMV0=',
+  columns: [],
+  dataProviders: [],
+  description: 'description',
+  eventType: 'all',
+  filters: [],
+  kqlMode: 'filter',
+  kqlQuery: { filterQuery: [] },
+  title: 'My duplicate timeline',
+  dateRange: { start: 1584523907294, end: 1584610307294 },
+  savedQueryId: null,
+  sort: { columnId: '@timestamp', sortDirection: 'desc' },
+  created: 1584828930463,
+  createdBy: 'angela',
+  updated: 1584868346013,
+  updatedBy: 'angela',
+  eventNotes: [],
+  globalNotes: [],
+  pinnedEventIds: [],
+};
+
+export const mockCreatedTemplateTimeline = {
+  ...mockCreatedTimeline,
+  ...mockGetTemplateTimelineValue,
+};
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts
index 0b320459c76a8..9afe5ad533324 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/__mocks__/request_responses.ts
@@ -4,15 +4,18 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 import * as rt from 'io-ts';
+import stream from 'stream';
+
 import {
   TIMELINE_DRAFT_URL,
   TIMELINE_EXPORT_URL,
   TIMELINE_IMPORT_URL,
   TIMELINE_URL,
 } from '../../../../../common/constants';
-import stream from 'stream';
-import { requestMock } from '../../../detection_engine/routes/__mocks__';
 import { SavedTimeline, TimelineType, TimelineStatus } from '../../../../../common/types/timeline';
+
+import { requestMock } from '../../../detection_engine/routes/__mocks__';
+
 import { updateTimelineSchema } from '../schemas/update_timelines_schema';
 import { createTimelineSchema } from '../schemas/create_timelines_schema';
 
@@ -59,7 +62,7 @@ export const inputTimeline: SavedTimeline = {
   title: 't',
   timelineType: TimelineType.default,
   templateTimelineId: null,
-  templateTimelineVersion: null,
+  templateTimelineVersion: 1,
   dateRange: { start: 1585227005527, end: 1585313405527 },
   savedQueryId: null,
   sort: { columnId: '@timestamp', sortDirection: 'desc' },
@@ -68,7 +71,7 @@ export const inputTimeline: SavedTimeline = {
 export const inputTemplateTimeline = {
   ...inputTimeline,
   timelineType: TimelineType.template,
-  templateTimelineId: null,
+  templateTimelineId: '79deb4c0-6bc1-11ea-inpt-templatea189',
   templateTimelineVersion: null,
 };
 
@@ -90,11 +93,11 @@ export const createDraftTimelineWithoutTimelineId = {
 };
 
 export const createTemplateTimelineWithoutTimelineId = {
-  templateTimelineId: null,
   timeline: inputTemplateTimeline,
   timelineId: null,
   version: null,
   timelineType: TimelineType.template,
+  status: TimelineStatus.active,
 };
 
 export const createTimelineWithTimelineId = {
@@ -110,7 +113,6 @@ export const createDraftTimelineWithTimelineId = {
 export const createTemplateTimelineWithTimelineId = {
   ...createTemplateTimelineWithoutTimelineId,
   timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
-  templateTimelineId: 'existing template timeline id',
 };
 
 export const updateTimelineWithTimelineId = {
@@ -122,7 +124,7 @@ export const updateTimelineWithTimelineId = {
 export const updateTemplateTimelineWithTimelineId = {
   timeline: {
     ...inputTemplateTimeline,
-    templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
+    templateTimelineId: '79deb4c0-6bc1-0000-0000-f5341fb7a189',
     templateTimelineVersion: 1,
   },
   timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts
index 9ad50b8f2266c..8cabd84a965b7 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/clean_draft_timelines_route.ts
@@ -4,6 +4,7 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import uuid from 'uuid';
 import { IRouter } from '../../../../../../../src/core/server';
 import { ConfigType } from '../../..';
 import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils';
@@ -14,6 +15,7 @@ import { buildRouteValidation } from '../../../utils/build_validation/route_vali
 import { getDraftTimeline, resetTimeline, getTimeline, persistTimeline } from '../saved_object';
 import { draftTimelineDefaults } from '../default_timeline';
 import { cleanDraftTimelineSchema } from './schemas/clean_draft_timelines_schema';
+import { TimelineType } from '../../../../common/types/timeline';
 
 export const cleanDraftTimelinesRoute = (
   router: IRouter,
@@ -60,10 +62,18 @@ export const cleanDraftTimelinesRoute = (
             },
           });
         }
+        const templateTimelineData =
+          request.body.timelineType === TimelineType.template
+            ? {
+                timelineType: request.body.timelineType,
+                templateTimelineId: uuid.v4(),
+                templateTimelineVersion: 1,
+              }
+            : {};
 
         const newTimelineResponse = await persistTimeline(frameworkRequest, null, null, {
           ...draftTimelineDefaults,
-          timelineType: request.body.timelineType,
+          ...templateTimelineData,
         });
 
         if (newTimelineResponse.code === 200) {
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
index 70ee1532395a5..f5345c3dce222 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.test.ts
@@ -23,6 +23,7 @@ import {
   createTimelineWithTimelineId,
   createTemplateTimelineWithoutTimelineId,
   createTemplateTimelineWithTimelineId,
+  updateTemplateTimelineWithTimelineId,
 } from './__mocks__/request_responses';
 import {
   CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
@@ -34,6 +35,7 @@ describe('create timelines', () => {
   let securitySetup: SecurityPluginSetup;
   let { context } = requestContextMock.createTools();
   let mockGetTimeline: jest.Mock;
+  let mockGetTemplateTimeline: jest.Mock;
   let mockPersistTimeline: jest.Mock;
   let mockPersistPinnedEventOnTimeline: jest.Mock;
   let mockPersistNote: jest.Mock;
@@ -55,6 +57,7 @@ describe('create timelines', () => {
     } as unknown) as SecurityPluginSetup;
 
     mockGetTimeline = jest.fn();
+    mockGetTemplateTimeline = jest.fn();
     mockPersistTimeline = jest.fn();
     mockPersistPinnedEventOnTimeline = jest.fn();
     mockPersistNote = jest.fn();
@@ -231,11 +234,14 @@ describe('create timelines', () => {
       });
     });
 
-    describe('Import a template timeline already exist', () => {
+    describe('Create a template timeline already exist', () => {
       beforeEach(() => {
         jest.doMock('../saved_object', () => {
           return {
             getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue),
+            getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+              timeline: [mockGetTemplateTimelineValue],
+            }),
             persistTimeline: mockPersistTimeline,
           };
         });
@@ -259,7 +265,7 @@ describe('create timelines', () => {
 
       test('returns error message', async () => {
         const response = await server.inject(
-          getCreateTimelinesRequest(createTemplateTimelineWithTimelineId),
+          getCreateTimelinesRequest(updateTemplateTimelineWithTimelineId),
           context
         );
         expect(response.body).toEqual({
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts
index d92f2ce0764c5..60ddaea367aed 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts
@@ -6,7 +6,6 @@
 import { IRouter } from '../../../../../../../src/core/server';
 
 import { TIMELINE_URL } from '../../../../common/constants';
-import { TimelineType } from '../../../../common/types/timeline';
 
 import { ConfigType } from '../../..';
 import { SetupPlugins } from '../../../plugin';
@@ -15,14 +14,12 @@ import { buildRouteValidation } from '../../../utils/build_validation/route_vali
 import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils';
 
 import { createTimelineSchema } from './schemas/create_timelines_schema';
-import { buildFrameworkRequest } from './utils/common';
 import {
-  createTimelines,
-  getTimeline,
-  getTemplateTimeline,
-  CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
-  CREATE_TIMELINE_ERROR_MESSAGE,
-} from './utils/create_timelines';
+  buildFrameworkRequest,
+  CompareTimelinesStatus,
+  TimelineStatusActions,
+} from './utils/common';
+import { createTimelines } from './utils/create_timelines';
 
 export const createTimelinesRoute = (
   router: IRouter,
@@ -36,7 +33,7 @@ export const createTimelinesRoute = (
         body: buildRouteValidation(createTimelineSchema),
       },
       options: {
-        tags: ['access:securitySolution'],
+        tags: ['access:siem'],
       },
     },
     async (context, request, response) => {
@@ -46,40 +43,54 @@ export const createTimelinesRoute = (
         const frameworkRequest = await buildFrameworkRequest(context, security, request);
 
         const { timelineId, timeline, version } = request.body;
-        const { templateTimelineId, timelineType } = timeline;
-        const isHandlingTemplateTimeline = timelineType === TimelineType.template;
-
-        const existTimeline =
-          timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null;
-        const existTemplateTimeline =
-          templateTimelineId != null
-            ? await getTemplateTimeline(frameworkRequest, templateTimelineId)
-            : null;
+        const {
+          templateTimelineId,
+          templateTimelineVersion,
+          timelineType,
+          title,
+          status,
+        } = timeline;
+        const compareTimelinesStatus = new CompareTimelinesStatus({
+          status,
+          title,
+          timelineType,
+          timelineInput: {
+            id: timelineId,
+            version,
+          },
+          templateTimelineInput: {
+            id: templateTimelineId,
+            version: templateTimelineVersion,
+          },
+          frameworkRequest,
+        });
+        await compareTimelinesStatus.init();
 
-        if (
-          (!isHandlingTemplateTimeline && existTimeline != null) ||
-          (isHandlingTemplateTimeline && (existTemplateTimeline != null || existTimeline != null))
-        ) {
-          return siemResponse.error({
-            body: isHandlingTemplateTimeline
-              ? CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE
-              : CREATE_TIMELINE_ERROR_MESSAGE,
-            statusCode: 405,
+        // Create timeline
+        if (compareTimelinesStatus.isCreatable) {
+          const newTimeline = await createTimelines({
+            frameworkRequest,
+            timeline,
+            timelineVersion: version,
           });
-        }
 
-        // Create timeline
-        const newTimeline = await createTimelines(frameworkRequest, timeline, null, version);
-        return response.ok({
-          body: {
-            data: {
-              persistTimeline: newTimeline,
+          return response.ok({
+            body: {
+              data: {
+                persistTimeline: newTimeline,
+              },
             },
-          },
-        });
+          });
+        } else {
+          return siemResponse.error(
+            compareTimelinesStatus.checkIsFailureCases(TimelineStatusActions.create) || {
+              statusCode: 405,
+              body: 'update timeline error',
+            }
+          );
+        }
       } catch (err) {
         const error = transformError(err);
-
         return siemResponse.error({
           body: error.message,
           statusCode: error.statusCode,
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts
index 48e22f6af2a7b..15fb8f3411cfa 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.test.ts
@@ -12,7 +12,7 @@ import {
   createMockConfig,
 } from '../../detection_engine/routes/__mocks__';
 import { TIMELINE_EXPORT_URL } from '../../../../common/constants';
-import { TimelineStatus } from '../../../../common/types/timeline';
+import { TimelineStatus, TimelineType } from '../../../../common/types/timeline';
 import { SecurityPluginSetup } from '../../../../../../plugins/security/server';
 
 import {
@@ -22,7 +22,19 @@ import {
   mockGetCurrentUser,
   mockGetTimelineValue,
   mockParsedTimelineObject,
+  mockParsedTemplateTimelineObjects,
+  mockUniqueParsedTemplateTimelineObjects,
+  mockParsedTemplateTimelineObject,
+  mockCreatedTemplateTimeline,
+  mockGetTemplateTimelineValue,
+  mockCreatedTimeline,
 } from './__mocks__/import_timelines';
+import {
+  TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE,
+  EMPTY_TITLE_ERROR_MESSAGE,
+  NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE,
+  NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE,
+} from './utils/failure_cases';
 
 describe('import timelines', () => {
   let server: ReturnType<typeof serverMock.create>;
@@ -35,8 +47,7 @@ describe('import timelines', () => {
   let mockPersistPinnedEventOnTimeline: jest.Mock;
   let mockPersistNote: jest.Mock;
   let mockGetTupleDuplicateErrorsAndUniqueTimeline: jest.Mock;
-  const newTimelineSavedObjectId = '79deb4c0-6bc1-11ea-9999-f5341fb7a189';
-  const newTimelineVersion = '9999';
+
   beforeEach(() => {
     jest.resetModules();
     jest.resetAllMocks();
@@ -90,7 +101,7 @@ describe('import timelines', () => {
           getTimeline: mockGetTimeline.mockReturnValue(null),
           getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null),
           persistTimeline: mockPersistTimeline.mockReturnValue({
-            timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion },
+            timeline: mockCreatedTimeline,
           }),
         };
       });
@@ -139,19 +150,38 @@ describe('import timelines', () => {
     test('should Create a new timeline savedObject with given timeline', async () => {
       const mockRequest = getImportTimelinesRequest();
       await server.inject(mockRequest, context);
-      expect(mockPersistTimeline.mock.calls[0][3]).toEqual(mockParsedTimelineObject);
+      expect(mockPersistTimeline.mock.calls[0][3]).toEqual({
+        ...mockParsedTimelineObject,
+        status: TimelineStatus.active,
+        templateTimelineId: null,
+        templateTimelineVersion: null,
+      });
     });
 
-    test('should Create a new timeline savedObject with given draft timeline', async () => {
+    test('should throw error if given an untitle timeline', async () => {
       mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([
         mockDuplicateIdErrors,
-        [{ ...mockUniqueParsedObjects[0], status: TimelineStatus.draft }],
+        [
+          {
+            ...mockUniqueParsedObjects[0],
+            title: '',
+          },
+        ],
       ]);
       const mockRequest = getImportTimelinesRequest();
-      await server.inject(mockRequest, context);
-      expect(mockPersistTimeline.mock.calls[0][3]).toEqual({
-        ...mockParsedTimelineObject,
-        status: TimelineStatus.active,
+      const response = await server.inject(mockRequest, context);
+      expect(response.body).toEqual({
+        success: false,
+        success_count: 0,
+        errors: [
+          {
+            id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
+            error: {
+              status_code: 409,
+              message: EMPTY_TITLE_ERROR_MESSAGE,
+            },
+          },
+        ],
       });
     });
 
@@ -178,7 +208,9 @@ describe('import timelines', () => {
     test('should Create a new pinned event with new timelineSavedObjectId', async () => {
       const mockRequest = getImportTimelinesRequest();
       await server.inject(mockRequest, context);
-      expect(mockPersistPinnedEventOnTimeline.mock.calls[0][3]).toEqual(newTimelineSavedObjectId);
+      expect(mockPersistPinnedEventOnTimeline.mock.calls[0][3]).toEqual(
+        mockCreatedTimeline.savedObjectId
+      );
     });
 
     test('should Create notes', async () => {
@@ -202,7 +234,7 @@ describe('import timelines', () => {
     test('should provide note content when Creating notes for a timeline', async () => {
       const mockRequest = getImportTimelinesRequest();
       await server.inject(mockRequest, context);
-      expect(mockPersistNote.mock.calls[0][2]).toEqual(newTimelineVersion);
+      expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTimeline.version);
     });
 
     test('should provide new notes when Creating notes for a timeline', async () => {
@@ -211,17 +243,17 @@ describe('import timelines', () => {
       expect(mockPersistNote.mock.calls[0][3]).toEqual({
         eventId: undefined,
         note: mockUniqueParsedObjects[0].globalNotes[0].note,
-        timelineId: newTimelineSavedObjectId,
+        timelineId: mockCreatedTimeline.savedObjectId,
       });
       expect(mockPersistNote.mock.calls[1][3]).toEqual({
         eventId: mockUniqueParsedObjects[0].eventNotes[0].eventId,
         note: mockUniqueParsedObjects[0].eventNotes[0].note,
-        timelineId: newTimelineSavedObjectId,
+        timelineId: mockCreatedTimeline.savedObjectId,
       });
       expect(mockPersistNote.mock.calls[2][3]).toEqual({
         eventId: mockUniqueParsedObjects[0].eventNotes[1].eventId,
         note: mockUniqueParsedObjects[0].eventNotes[1].note,
-        timelineId: newTimelineSavedObjectId,
+        timelineId: mockCreatedTimeline.savedObjectId,
       });
     });
 
@@ -268,7 +300,458 @@ describe('import timelines', () => {
             id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
             error: {
               status_code: 409,
-              message: `timeline_id: "79deb4c0-6bc1-11ea-a90b-f5341fb7a189" already exists`,
+              message: `savedObjectId: "79deb4c0-6bc1-11ea-a90b-f5341fb7a189" already exists`,
+            },
+          },
+        ],
+      });
+    });
+
+    test('should throw error if given an untitle timeline', async () => {
+      mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([
+        mockDuplicateIdErrors,
+        [
+          {
+            ...mockUniqueParsedObjects[0],
+            title: '',
+          },
+        ],
+      ]);
+      const mockRequest = getImportTimelinesRequest();
+      const response = await server.inject(mockRequest, context);
+      expect(response.body).toEqual({
+        success: false,
+        success_count: 0,
+        errors: [
+          {
+            id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
+            error: {
+              status_code: 409,
+              message: EMPTY_TITLE_ERROR_MESSAGE,
+            },
+          },
+        ],
+      });
+    });
+
+    test('should throw error if timelineType updated', async () => {
+      mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([
+        mockDuplicateIdErrors,
+        [
+          {
+            ...mockGetTimelineValue,
+            timelineType: TimelineType.template,
+          },
+        ],
+      ]);
+      const mockRequest = getImportTimelinesRequest();
+      const response = await server.inject(mockRequest, context);
+      expect(response.body).toEqual({
+        success: false,
+        success_count: 0,
+        errors: [
+          {
+            id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
+            error: {
+              status_code: 409,
+              message: NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE,
+            },
+          },
+        ],
+      });
+    });
+  });
+
+  describe('request validation', () => {
+    beforeEach(() => {
+      jest.doMock('../saved_object', () => {
+        return {
+          getTimeline: mockGetTimeline.mockReturnValue(null),
+          persistTimeline: mockPersistTimeline.mockReturnValue({
+            timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' },
+          }),
+        };
+      });
+
+      jest.doMock('../../pinned_event/saved_object', () => {
+        return {
+          persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue(
+            new Error('Test error')
+          ),
+        };
+      });
+
+      jest.doMock('../../note/saved_object', () => {
+        return {
+          persistNote: mockPersistNote,
+        };
+      });
+    });
+    test('disallows invalid query', async () => {
+      request = requestMock.create({
+        method: 'post',
+        path: TIMELINE_EXPORT_URL,
+        body: { id: 'someId' },
+      });
+      const importTimelinesRoute = jest.requireActual('./import_timelines_route')
+        .importTimelinesRoute;
+
+      importTimelinesRoute(server.router, createMockConfig(), securitySetup);
+      const result = server.validate(request);
+
+      expect(result.badRequest).toHaveBeenCalledWith(
+        [
+          'Invalid value "undefined" supplied to "file"',
+          'Invalid value "undefined" supplied to "file"',
+        ].join(',')
+      );
+    });
+  });
+});
+
+describe('import template timelines', () => {
+  let server: ReturnType<typeof serverMock.create>;
+  let request: ReturnType<typeof requestMock.create>;
+  let securitySetup: SecurityPluginSetup;
+  let { context } = requestContextMock.createTools();
+  let mockGetTimeline: jest.Mock;
+  let mockGetTemplateTimeline: jest.Mock;
+  let mockPersistTimeline: jest.Mock;
+  let mockPersistPinnedEventOnTimeline: jest.Mock;
+  let mockPersistNote: jest.Mock;
+  let mockGetTupleDuplicateErrorsAndUniqueTimeline: jest.Mock;
+  const mockNewTemplateTimelineId = 'new templateTimelineId';
+  beforeEach(() => {
+    jest.resetModules();
+    jest.resetAllMocks();
+    jest.restoreAllMocks();
+    jest.clearAllMocks();
+
+    server = serverMock.create();
+    context = requestContextMock.createTools().context;
+
+    securitySetup = ({
+      authc: {
+        getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser),
+      },
+      authz: {},
+    } as unknown) as SecurityPluginSetup;
+
+    mockGetTimeline = jest.fn();
+    mockGetTemplateTimeline = jest.fn();
+    mockPersistTimeline = jest.fn();
+    mockPersistPinnedEventOnTimeline = jest.fn();
+    mockPersistNote = jest.fn();
+    mockGetTupleDuplicateErrorsAndUniqueTimeline = jest.fn();
+
+    jest.doMock('../create_timelines_stream_from_ndjson', () => {
+      return {
+        createTimelinesStreamFromNdJson: jest
+          .fn()
+          .mockReturnValue(mockParsedTemplateTimelineObjects),
+      };
+    });
+
+    jest.doMock('../../../../../../../src/legacy/utils', () => {
+      return {
+        createPromiseFromStreams: jest.fn().mockReturnValue(mockParsedTemplateTimelineObjects),
+      };
+    });
+
+    jest.doMock('./utils/import_timelines', () => {
+      const originalModule = jest.requireActual('./utils/import_timelines');
+      return {
+        ...originalModule,
+        getTupleDuplicateErrorsAndUniqueTimeline: mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue(
+          [mockDuplicateIdErrors, mockUniqueParsedTemplateTimelineObjects]
+        ),
+      };
+    });
+
+    jest.doMock('uuid', () => ({
+      v4: jest.fn().mockReturnValue(mockNewTemplateTimelineId),
+    }));
+  });
+
+  describe('Import a new template timeline', () => {
+    beforeEach(() => {
+      jest.doMock('../saved_object', () => {
+        return {
+          getTimeline: mockGetTimeline.mockReturnValue(null),
+          getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null),
+          persistTimeline: mockPersistTimeline.mockReturnValue({
+            timeline: mockCreatedTemplateTimeline,
+          }),
+        };
+      });
+
+      jest.doMock('../../pinned_event/saved_object', () => {
+        return {
+          persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
+        };
+      });
+
+      jest.doMock('../../note/saved_object', () => {
+        return {
+          persistNote: mockPersistNote,
+        };
+      });
+
+      const importTimelinesRoute = jest.requireActual('./import_timelines_route')
+        .importTimelinesRoute;
+      importTimelinesRoute(server.router, createMockConfig(), securitySetup);
+    });
+
+    test('should use given timelineId to check if the timeline savedObject already exist', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockGetTimeline.mock.calls[0][1]).toEqual(
+        mockUniqueParsedTemplateTimelineObjects[0].savedObjectId
+      );
+    });
+
+    test('should use given templateTimelineId to check if the timeline savedObject already exist', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual(
+        mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId
+      );
+    });
+
+    test('should Create a new timeline savedObject', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline).toHaveBeenCalled();
+    });
+
+    test('should Create a new timeline savedObject without timelineId', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline.mock.calls[0][1]).toBeNull();
+    });
+
+    test('should Create a new timeline savedObject without timeline version', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline.mock.calls[0][2]).toBeNull();
+    });
+
+    test('should Create a new timeline savedObject witn given timeline and skip the omitted fields', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline.mock.calls[0][3]).toEqual({
+        ...mockParsedTemplateTimelineObject,
+        status: TimelineStatus.active,
+      });
+    });
+
+    test('should NOT Create new pinned events', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistPinnedEventOnTimeline).not.toHaveBeenCalled();
+    });
+
+    test('should provide no noteSavedObjectId when Creating notes for a timeline', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistNote.mock.calls[0][1]).toBeNull();
+    });
+
+    test('should provide new timeline version when Creating notes for a timeline', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTemplateTimeline.version);
+    });
+
+    test('should exclude event notes when creating notes', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistNote.mock.calls[0][3]).toEqual({
+        eventId: undefined,
+        note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note,
+        timelineId: mockCreatedTemplateTimeline.savedObjectId,
+      });
+    });
+
+    test('returns 200 when import timeline successfully', async () => {
+      const response = await server.inject(getImportTimelinesRequest(), context);
+      expect(response.status).toEqual(200);
+    });
+
+    test('should assign a templateTimeline Id automatically if not given one', async () => {
+      mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([
+        mockDuplicateIdErrors,
+        [
+          {
+            ...mockUniqueParsedTemplateTimelineObjects[0],
+            templateTimelineId: null,
+          },
+        ],
+      ]);
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline.mock.calls[0][3].templateTimelineId).toEqual(
+        mockNewTemplateTimelineId
+      );
+    });
+  });
+
+  describe('Import a template timeline already exist', () => {
+    beforeEach(() => {
+      jest.doMock('../saved_object', () => {
+        return {
+          getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue),
+          getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+            timeline: [mockGetTemplateTimelineValue],
+          }),
+          persistTimeline: mockPersistTimeline.mockReturnValue({
+            timeline: mockCreatedTemplateTimeline,
+          }),
+        };
+      });
+
+      jest.doMock('../../pinned_event/saved_object', () => {
+        return {
+          persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline,
+        };
+      });
+
+      jest.doMock('../../note/saved_object', () => {
+        return {
+          persistNote: mockPersistNote,
+        };
+      });
+
+      const importTimelinesRoute = jest.requireActual('./import_timelines_route')
+        .importTimelinesRoute;
+      importTimelinesRoute(server.router, createMockConfig(), securitySetup);
+    });
+
+    test('should use given timelineId to check if the timeline savedObject already exist', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockGetTimeline.mock.calls[0][1]).toEqual(
+        mockUniqueParsedTemplateTimelineObjects[0].savedObjectId
+      );
+    });
+
+    test('should use given templateTimelineId to check if the timeline savedObject already exist', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual(
+        mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId
+      );
+    });
+
+    test('should UPDATE timeline savedObject', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline).toHaveBeenCalled();
+    });
+
+    test('should UPDATE timeline savedObject with timelineId', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline.mock.calls[0][1]).toEqual(
+        mockUniqueParsedTemplateTimelineObjects[0].savedObjectId
+      );
+    });
+
+    test('should UPDATE timeline savedObject without timeline version', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline.mock.calls[0][2]).toEqual(
+        mockUniqueParsedTemplateTimelineObjects[0].version
+      );
+    });
+
+    test('should UPDATE a new timeline savedObject witn given timeline and skip the omitted fields', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistTimeline.mock.calls[0][3]).toEqual(mockParsedTemplateTimelineObject);
+    });
+
+    test('should NOT Create new pinned events', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistPinnedEventOnTimeline).not.toHaveBeenCalled();
+    });
+
+    test('should provide noteSavedObjectId when Creating notes for a timeline', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistNote.mock.calls[0][1]).toBeNull();
+    });
+
+    test('should provide new timeline version when Creating notes for a timeline', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistNote.mock.calls[0][2]).toEqual(mockCreatedTemplateTimeline.version);
+    });
+
+    test('should exclude event notes when creating notes', async () => {
+      const mockRequest = getImportTimelinesRequest();
+      await server.inject(mockRequest, context);
+      expect(mockPersistNote.mock.calls[0][3]).toEqual({
+        eventId: undefined,
+        note: mockUniqueParsedTemplateTimelineObjects[0].globalNotes[0].note,
+        timelineId: mockCreatedTemplateTimeline.savedObjectId,
+      });
+    });
+
+    test('returns 200 when import timeline successfully', async () => {
+      const response = await server.inject(getImportTimelinesRequest(), context);
+      expect(response.status).toEqual(200);
+    });
+
+    test('should throw error if with given template timeline version conflict', async () => {
+      mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([
+        mockDuplicateIdErrors,
+        [
+          {
+            ...mockUniqueParsedTemplateTimelineObjects[0],
+            templateTimelineVersion: 1,
+          },
+        ],
+      ]);
+      const mockRequest = getImportTimelinesRequest();
+      const response = await server.inject(mockRequest, context);
+      expect(response.body).toEqual({
+        success: false,
+        success_count: 0,
+        errors: [
+          {
+            id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
+            error: {
+              status_code: 409,
+              message: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE,
+            },
+          },
+        ],
+      });
+    });
+
+    test('should throw error if status updated', async () => {
+      mockGetTupleDuplicateErrorsAndUniqueTimeline.mockReturnValue([
+        mockDuplicateIdErrors,
+        [
+          {
+            ...mockUniqueParsedTemplateTimelineObjects[0],
+            status: TimelineStatus.immutable,
+          },
+        ],
+      ]);
+      const mockRequest = getImportTimelinesRequest();
+      const response = await server.inject(mockRequest, context);
+      expect(response.body).toEqual({
+        success: false,
+        success_count: 0,
+        errors: [
+          {
+            id: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189',
+            error: {
+              status_code: 409,
+              message: NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE,
             },
           },
         ],
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
index 5080142f22b15..fb4991d7d1e7d 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/import_timelines_route.ts
@@ -7,17 +7,17 @@
 import { extname } from 'path';
 import { chunk, omit } from 'lodash/fp';
 
-import { validate } from '../../../../common/validate';
-import { importRulesSchema } from '../../../../common/detection_engine/schemas/response/import_rules_schema';
+import uuid from 'uuid';
 import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils';
 import { IRouter } from '../../../../../../../src/core/server';
 
 import { TIMELINE_IMPORT_URL } from '../../../../common/constants';
+import { validate } from '../../../../common/validate';
 
 import { SetupPlugins } from '../../../plugin';
 import { ConfigType } from '../../../config';
 import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
-
+import { importRulesSchema } from '../../../../common/detection_engine/schemas/response/import_rules_schema';
 import {
   buildSiemResponse,
   createBulkErrorObject,
@@ -28,7 +28,11 @@ import {
 import { createTimelinesStreamFromNdJson } from '../create_timelines_stream_from_ndjson';
 
 import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema';
-import { buildFrameworkRequest } from './utils/common';
+import {
+  buildFrameworkRequest,
+  CompareTimelinesStatus,
+  TimelineStatusActions,
+} from './utils/common';
 import {
   getTupleDuplicateErrorsAndUniqueTimeline,
   isBulkError,
@@ -38,11 +42,11 @@ import {
   PromiseFromStreams,
   timelineSavedObjectOmittedFields,
 } from './utils/import_timelines';
-import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines';
-import { TimelineType, TimelineStatus } from '../../../../common/types/timeline';
-import { checkIsFailureCases } from './utils/update_timelines';
+import { createTimelines } from './utils/create_timelines';
+import { TimelineStatus } from '../../../../common/types/timeline';
 
 const CHUNK_PARSED_OBJECT_SIZE = 10;
+const DEFAULT_IMPORT_ERROR = `Something went wrong, there's something we didn't handle properly, please help us improve by providing the file you try to import on https://discuss.elastic.co/c/security/siem`;
 
 export const importTimelinesRoute = (
   router: IRouter,
@@ -118,100 +122,112 @@ export const importTimelinesRoute = (
 
                       return null;
                     }
+
                     const {
-                      savedObjectId = null,
+                      savedObjectId,
                       pinnedEventIds,
                       globalNotes,
                       eventNotes,
+                      status,
                       templateTimelineId,
                       templateTimelineVersion,
+                      title,
                       timelineType,
-                      version = null,
+                      version,
                     } = parsedTimeline;
                     const parsedTimelineObject = omit(
                       timelineSavedObjectOmittedFields,
                       parsedTimeline
                     );
-
                     let newTimeline = null;
                     try {
-                      const templateTimeline =
-                        templateTimelineId != null
-                          ? await getTemplateTimeline(frameworkRequest, templateTimelineId)
-                          : null;
-
-                      const timeline =
-                        savedObjectId != null &&
-                        (await getTimeline(frameworkRequest, savedObjectId));
-                      const isHandlingTemplateTimeline = timelineType === TimelineType.template;
-
-                      if (
-                        (timeline == null && !isHandlingTemplateTimeline) ||
-                        (timeline == null && templateTimeline == null && isHandlingTemplateTimeline)
-                      ) {
+                      const compareTimelinesStatus = new CompareTimelinesStatus({
+                        status,
+                        timelineType,
+                        title,
+                        timelineInput: {
+                          id: savedObjectId,
+                          version,
+                        },
+                        templateTimelineInput: {
+                          id: templateTimelineId,
+                          version: templateTimelineVersion,
+                        },
+                        frameworkRequest,
+                      });
+                      await compareTimelinesStatus.init();
+                      const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline;
+                      if (compareTimelinesStatus.isCreatableViaImport) {
                         // create timeline / template timeline
-                        newTimeline = await createTimelines(
+                        newTimeline = await createTimelines({
                           frameworkRequest,
-                          {
+                          timeline: {
                             ...parsedTimelineObject,
                             status:
-                              parsedTimelineObject.status === TimelineStatus.draft
+                              status === TimelineStatus.draft
                                 ? TimelineStatus.active
-                                : parsedTimelineObject.status,
+                                : status ?? TimelineStatus.active,
+                            templateTimelineVersion: isTemplateTimeline
+                              ? templateTimelineVersion
+                              : null,
+                            templateTimelineId: isTemplateTimeline
+                              ? templateTimelineId ?? uuid.v4()
+                              : null,
                           },
-                          null, // timelineSavedObjectId
-                          null, // timelineVersion
-                          pinnedEventIds,
-                          isHandlingTemplateTimeline
-                            ? globalNotes
-                            : [...globalNotes, ...eventNotes],
-                          [] // existing note ids
-                        );
+                          pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds,
+                          notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes],
+                        });
 
                         resolve({
                           timeline_id: newTimeline.timeline.savedObjectId,
                           status_code: 200,
                         });
-                      } else if (
-                        timeline &&
-                        timeline != null &&
-                        templateTimeline != null &&
-                        isHandlingTemplateTimeline
-                      ) {
-                        // update template timeline
-                        const errorObj = checkIsFailureCases(
-                          isHandlingTemplateTimeline,
-                          version,
-                          templateTimelineVersion ?? null,
-                          timeline,
-                          templateTimeline
-                        );
-                        if (errorObj != null) {
-                          return siemResponse.error(errorObj);
-                        }
+                      }
 
-                        newTimeline = await createTimelines(
-                          frameworkRequest,
-                          { ...parsedTimelineObject, templateTimelineId, templateTimelineVersion },
-                          timeline.savedObjectId, // timelineSavedObjectId
-                          timeline.version, // timelineVersion
-                          pinnedEventIds,
-                          globalNotes,
-                          [] // existing note ids
+                      if (!compareTimelinesStatus.isHandlingTemplateTimeline) {
+                        const errorMessage = compareTimelinesStatus.checkIsFailureCases(
+                          TimelineStatusActions.createViaImport
                         );
+                        const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR;
 
-                        resolve({
-                          timeline_id: newTimeline.timeline.savedObjectId,
-                          status_code: 200,
-                        });
-                      } else {
                         resolve(
                           createBulkErrorObject({
                             id: savedObjectId ?? 'unknown',
                             statusCode: 409,
-                            message: `timeline_id: "${savedObjectId}" already exists`,
+                            message,
                           })
                         );
+                      } else {
+                        if (compareTimelinesStatus.isUpdatableViaImport) {
+                          // update template timeline
+                          newTimeline = await createTimelines({
+                            frameworkRequest,
+                            timeline: parsedTimelineObject,
+                            timelineSavedObjectId: compareTimelinesStatus.timelineId,
+                            timelineVersion: compareTimelinesStatus.timelineVersion,
+                            notes: globalNotes,
+                            existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds,
+                          });
+
+                          resolve({
+                            timeline_id: newTimeline.timeline.savedObjectId,
+                            status_code: 200,
+                          });
+                        } else {
+                          const errorMessage = compareTimelinesStatus.checkIsFailureCases(
+                            TimelineStatusActions.updateViaImport
+                          );
+
+                          const message = errorMessage?.body ?? DEFAULT_IMPORT_ERROR;
+
+                          resolve(
+                            createBulkErrorObject({
+                              id: savedObjectId ?? 'unknown',
+                              statusCode: 409,
+                              message,
+                            })
+                          );
+                        }
                       }
                     } catch (err) {
                       resolve(
@@ -236,9 +252,9 @@ export const importTimelinesRoute = (
           ];
         }
 
-        const errorsResp = importTimelineResponse.filter((resp) =>
-          isBulkError(resp)
-        ) as BulkError[];
+        const errorsResp = importTimelineResponse.filter((resp) => {
+          return isBulkError(resp);
+        }) as BulkError[];
         const successes = importTimelineResponse.filter((resp) => {
           if (isImportRegular(resp)) {
             return resp.status_code === 200;
@@ -261,7 +277,6 @@ export const importTimelinesRoute = (
       } catch (err) {
         const error = transformError(err);
         const siemResponse = buildSiemResponse(response);
-
         return siemResponse.error({
           body: error.message,
           statusCode: error.statusCode,
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts
index 2a3feb7afd59c..3cedb925649a2 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.test.ts
@@ -26,7 +26,7 @@ import {
 import {
   UPDATE_TIMELINE_ERROR_MESSAGE,
   UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
-} from './utils/update_timelines';
+} from './utils/failure_cases';
 
 describe('update timelines', () => {
   let server: ReturnType<typeof serverMock.create>;
@@ -93,7 +93,7 @@ describe('update timelines', () => {
         await server.inject(mockRequest, context);
       });
 
-      test('should Check a if given timeline id exist', async () => {
+      test('should Check if given timeline id exist', async () => {
         expect(mockGetTimeline.mock.calls[0][1]).toEqual(updateTimelineWithTimelineId.timelineId);
       });
 
@@ -178,7 +178,7 @@ describe('update timelines', () => {
               timeline: [mockGetTemplateTimelineValue],
             }),
             persistTimeline: mockPersistTimeline.mockReturnValue({
-              timeline: updateTimelineWithTimelineId.timeline,
+              timeline: updateTemplateTimelineWithTimelineId.timeline,
             }),
           };
         });
@@ -211,7 +211,7 @@ describe('update timelines', () => {
 
       test('should Update existing template timeline with template timelineId', async () => {
         expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual(
-          updateTemplateTimelineWithTimelineId.timelineId
+          updateTemplateTimelineWithTimelineId.timeline.templateTimelineId
         );
       });
 
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts
index d5ecd408a6ef4..f59df151b6955 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts
@@ -7,19 +7,17 @@
 import { IRouter } from '../../../../../../../src/core/server';
 
 import { TIMELINE_URL } from '../../../../common/constants';
-import { TimelineType } from '../../../../common/types/timeline';
 
 import { SetupPlugins } from '../../../plugin';
 import { buildRouteValidation } from '../../../utils/build_validation/route_validation';
 import { ConfigType } from '../../..';
 
 import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils';
-import { FrameworkRequest } from '../../framework';
 
 import { updateTimelineSchema } from './schemas/update_timelines_schema';
-import { buildFrameworkRequest } from './utils/common';
-import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines';
-import { checkIsFailureCases } from './utils/update_timelines';
+import { buildFrameworkRequest, TimelineStatusActions } from './utils/common';
+import { createTimelines } from './utils/create_timelines';
+import { CompareTimelinesStatus } from './utils/compare_timelines_status';
 
 export const updateTimelinesRoute = (
   router: IRouter,
@@ -33,7 +31,7 @@ export const updateTimelinesRoute = (
         body: buildRouteValidation(updateTimelineSchema),
       },
       options: {
-        tags: ['access:securitySolution'],
+        tags: ['access:siem'],
       },
     },
     // eslint-disable-next-line complexity
@@ -43,39 +41,54 @@ export const updateTimelinesRoute = (
       try {
         const frameworkRequest = await buildFrameworkRequest(context, security, request);
         const { timelineId, timeline, version } = request.body;
-        const { templateTimelineId, templateTimelineVersion, timelineType } = timeline;
-        const isHandlingTemplateTimeline = timelineType === TimelineType.template;
-        const existTimeline =
-          timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null;
+        const {
+          templateTimelineId,
+          templateTimelineVersion,
+          timelineType,
+          title,
+          status,
+        } = timeline;
 
-        const existTemplateTimeline =
-          templateTimelineId != null
-            ? await getTemplateTimeline(frameworkRequest, templateTimelineId)
-            : null;
-
-        const errorObj = checkIsFailureCases(
-          isHandlingTemplateTimeline,
-          version,
-          templateTimelineVersion ?? null,
-          existTimeline,
-          existTemplateTimeline
-        );
-        if (errorObj != null) {
-          return siemResponse.error(errorObj);
-        }
-        const updatedTimeline = await createTimelines(
-          (frameworkRequest as unknown) as FrameworkRequest,
-          timeline,
-          timelineId,
-          version
-        );
-        return response.ok({
-          body: {
-            data: {
-              persistTimeline: updatedTimeline,
-            },
+        const compareTimelinesStatus = new CompareTimelinesStatus({
+          status,
+          title,
+          timelineType,
+          timelineInput: {
+            id: timelineId,
+            version,
+          },
+          templateTimelineInput: {
+            id: templateTimelineId,
+            version: templateTimelineVersion,
           },
+          frameworkRequest,
         });
+
+        await compareTimelinesStatus.init();
+        if (compareTimelinesStatus.isUpdatable) {
+          const updatedTimeline = await createTimelines({
+            frameworkRequest,
+            timeline,
+            timelineSavedObjectId: timelineId,
+            timelineVersion: version,
+          });
+
+          return response.ok({
+            body: {
+              data: {
+                persistTimeline: updatedTimeline,
+              },
+            },
+          });
+        } else {
+          const error = compareTimelinesStatus.checkIsFailureCases(TimelineStatusActions.update);
+          return siemResponse.error(
+            error || {
+              statusCode: 405,
+              body: 'update timeline error',
+            }
+          );
+        }
       } catch (err) {
         const error = transformError(err);
         return siemResponse.error({
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts
index adbfdbf6d6051..2c2d651fd483b 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts
@@ -5,9 +5,10 @@
  */
 import { set } from 'lodash/fp';
 
-import { RequestHandlerContext } from 'src/core/server';
+import { KibanaRequest, RequestHandlerContext } from 'src/core/server';
+
 import { SetupPlugins } from '../../../../plugin';
-import { KibanaRequest } from '../../../../../../../../src/core/server';
+
 import { FrameworkRequest } from '../../../framework';
 
 export const buildFrameworkRequest = async (
@@ -28,3 +29,19 @@ export const buildFrameworkRequest = async (
     )
   );
 };
+
+export enum TimelineStatusActions {
+  create = 'create',
+  createViaImport = 'createViaImport',
+  update = 'update',
+  updateViaImport = 'updateViaImport',
+}
+
+export type TimelineStatusAction =
+  | TimelineStatusActions.create
+  | TimelineStatusActions.createViaImport
+  | TimelineStatusActions.update
+  | TimelineStatusActions.updateViaImport;
+
+export * from './compare_timelines_status';
+export * from './timeline_object';
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts
new file mode 100644
index 0000000000000..a6d379e534bc2
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.test.ts
@@ -0,0 +1,810 @@
+/*
+ * 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 { TimelineType, TimelineStatus } from '../../../../../common/types/timeline';
+import { FrameworkRequest } from '../../../framework';
+
+import {
+  mockUniqueParsedObjects,
+  mockUniqueParsedTemplateTimelineObjects,
+  mockGetTemplateTimelineValue,
+  mockGetTimelineValue,
+} from '../__mocks__/import_timelines';
+
+import { CompareTimelinesStatus as TimelinesStatusType } from './compare_timelines_status';
+import {
+  EMPTY_TITLE_ERROR_MESSAGE,
+  UPDATE_STATUS_ERROR_MESSAGE,
+  UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
+  TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE,
+  CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
+  getImportExistingTimelineError,
+} from './failure_cases';
+import { TimelineStatusActions } from './common';
+
+describe('CompareTimelinesStatus', () => {
+  describe('timeline', () => {
+    describe('given timeline exists', () => {
+      const mockGetTimeline: jest.Mock = jest.fn();
+      const mockGetTemplateTimeline: jest.Mock = jest.fn();
+      let timelineObj: TimelinesStatusType;
+
+      afterEach(() => {
+        jest.clearAllMocks();
+      });
+
+      afterAll(() => {
+        jest.resetModules();
+      });
+
+      beforeAll(() => {
+        jest.resetModules();
+      });
+
+      beforeEach(async () => {
+        jest.doMock('../../saved_object', () => {
+          return {
+            getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue),
+            getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+              timeline: [],
+            }),
+          };
+        });
+
+        const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+          .CompareTimelinesStatus;
+
+        timelineObj = new CompareTimelinesStatus({
+          timelineInput: {
+            id: mockUniqueParsedObjects[0].savedObjectId,
+            version: mockUniqueParsedObjects[0].version,
+          },
+          timelineType: TimelineType.default,
+          title: mockUniqueParsedObjects[0].title,
+          templateTimelineInput: {
+            id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+            version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+          },
+          frameworkRequest: {} as FrameworkRequest,
+        });
+
+        await timelineObj.init();
+      });
+
+      test('should get timeline', () => {
+        expect(mockGetTimeline).toHaveBeenCalled();
+      });
+
+      test('should get templateTimeline', () => {
+        expect(mockGetTemplateTimeline).toHaveBeenCalled();
+      });
+
+      test('should not creatable', () => {
+        expect(timelineObj.isCreatable).toEqual(false);
+      });
+
+      test('should not CreatableViaImport', () => {
+        expect(timelineObj.isCreatableViaImport).toEqual(false);
+      });
+
+      test('should be Updatable', () => {
+        expect(timelineObj.isUpdatable).toEqual(true);
+      });
+
+      test('should not be UpdatableViaImport', () => {
+        expect(timelineObj.isUpdatableViaImport).toEqual(false);
+      });
+
+      test('should indicate we are handling a timeline', () => {
+        expect(timelineObj.isHandlingTemplateTimeline).toEqual(false);
+      });
+    });
+
+    describe('given timeline does NOT exists', () => {
+      const mockGetTimeline: jest.Mock = jest.fn();
+      const mockGetTemplateTimeline: jest.Mock = jest.fn();
+      let timelineObj: TimelinesStatusType;
+
+      afterEach(() => {
+        jest.clearAllMocks();
+      });
+
+      afterAll(() => {
+        jest.resetModules();
+      });
+
+      beforeAll(() => {
+        jest.resetModules();
+      });
+
+      beforeEach(async () => {
+        jest.doMock('../../saved_object', () => {
+          return {
+            getTimeline: mockGetTimeline.mockReturnValue(null),
+            getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+              timeline: [],
+            }),
+          };
+        });
+
+        const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+          .CompareTimelinesStatus;
+
+        timelineObj = new CompareTimelinesStatus({
+          timelineInput: {
+            id: mockUniqueParsedObjects[0].savedObjectId,
+            version: mockUniqueParsedObjects[0].version,
+          },
+          timelineType: TimelineType.default,
+          title: mockUniqueParsedObjects[0].title,
+          templateTimelineInput: {
+            id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+            version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+          },
+          frameworkRequest: {} as FrameworkRequest,
+        });
+
+        await timelineObj.init();
+      });
+
+      test('should get timeline', () => {
+        expect(mockGetTimeline).toHaveBeenCalled();
+      });
+
+      test('should get templateTimeline', () => {
+        expect(mockGetTemplateTimeline).toHaveBeenCalled();
+      });
+
+      test('should be creatable', () => {
+        expect(timelineObj.isCreatable).toEqual(true);
+      });
+
+      test('should be CreatableViaImport', () => {
+        expect(timelineObj.isCreatableViaImport).toEqual(true);
+      });
+
+      test('should be Updatable', () => {
+        expect(timelineObj.isUpdatable).toEqual(false);
+      });
+
+      test('should not be UpdatableViaImport', () => {
+        expect(timelineObj.isUpdatableViaImport).toEqual(false);
+      });
+
+      test('should indicate we are handling a timeline', () => {
+        expect(timelineObj.isHandlingTemplateTimeline).toEqual(false);
+      });
+    });
+  });
+
+  describe('template timeline', () => {
+    describe('given template timeline exists', () => {
+      const mockGetTimeline: jest.Mock = jest.fn();
+      const mockGetTemplateTimeline: jest.Mock = jest.fn();
+
+      let timelineObj: TimelinesStatusType;
+
+      beforeEach(async () => {
+        jest.doMock('../../saved_object', () => ({
+          getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue),
+          getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+            timeline: [mockGetTemplateTimelineValue],
+          }),
+        }));
+
+        const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+          .CompareTimelinesStatus;
+
+        timelineObj = new CompareTimelinesStatus({
+          timelineInput: {
+            id: mockUniqueParsedObjects[0].savedObjectId,
+            version: mockUniqueParsedObjects[0].version,
+          },
+          timelineType: TimelineType.template,
+          title: mockUniqueParsedObjects[0].title,
+          templateTimelineInput: {
+            id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+            version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+          },
+          frameworkRequest: {} as FrameworkRequest,
+        });
+        await timelineObj.init();
+      });
+
+      afterEach(() => {
+        jest.clearAllMocks();
+      });
+
+      afterAll(() => {
+        jest.resetModules();
+      });
+
+      beforeAll(() => {
+        jest.resetModules();
+      });
+
+      test('should get timeline', () => {
+        expect(mockGetTimeline).toHaveBeenCalled();
+      });
+
+      test('should get templateTimeline', () => {
+        expect(mockGetTemplateTimeline).toHaveBeenCalled();
+      });
+
+      test('should not creatable', () => {
+        expect(timelineObj.isCreatable).toEqual(false);
+      });
+
+      test('should not CreatableViaImport', () => {
+        expect(timelineObj.isCreatableViaImport).toEqual(false);
+      });
+
+      test('should be Updatable', () => {
+        expect(timelineObj.isUpdatable).toEqual(true);
+      });
+
+      test('should be UpdatableViaImport', () => {
+        expect(timelineObj.isUpdatableViaImport).toEqual(true);
+      });
+
+      test('should indicate we are handling a template timeline', () => {
+        expect(timelineObj.isHandlingTemplateTimeline).toEqual(true);
+      });
+    });
+
+    describe('given template timeline does NOT exists', () => {
+      const mockGetTimeline: jest.Mock = jest.fn();
+      const mockGetTemplateTimeline: jest.Mock = jest.fn();
+
+      let timelineObj: TimelinesStatusType;
+
+      afterEach(() => {
+        jest.clearAllMocks();
+      });
+
+      afterAll(() => {
+        jest.resetModules();
+      });
+
+      beforeAll(() => {
+        jest.resetModules();
+      });
+
+      beforeEach(async () => {
+        jest.doMock('../../saved_object', () => ({
+          getTimeline: mockGetTimeline,
+          getTimelineByTemplateTimelineId: mockGetTemplateTimeline,
+        }));
+
+        const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+          .CompareTimelinesStatus;
+
+        timelineObj = new CompareTimelinesStatus({
+          timelineInput: {
+            id: mockUniqueParsedObjects[0].savedObjectId,
+            version: mockUniqueParsedObjects[0].version,
+          },
+          timelineType: TimelineType.template,
+          title: mockUniqueParsedObjects[0].title,
+          templateTimelineInput: {
+            id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+            version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+          },
+          frameworkRequest: {} as FrameworkRequest,
+        });
+        await timelineObj.init();
+      });
+
+      test('should get timeline', () => {
+        expect(mockGetTimeline).toHaveBeenCalled();
+      });
+
+      test('should get templateTimeline', () => {
+        expect(mockGetTemplateTimeline).toHaveBeenCalled();
+      });
+
+      test('should be creatable', () => {
+        expect(timelineObj.isCreatable).toEqual(true);
+      });
+
+      test('should throw no error on creatable', () => {
+        expect(timelineObj.checkIsFailureCases(TimelineStatusActions.create)).toBeNull();
+      });
+
+      test('should be CreatableViaImport', () => {
+        expect(timelineObj.isCreatableViaImport).toEqual(true);
+      });
+
+      test('should throw no error on CreatableViaImport', () => {
+        expect(timelineObj.checkIsFailureCases(TimelineStatusActions.createViaImport)).toBeNull();
+      });
+
+      test('should not be Updatable', () => {
+        expect(timelineObj.isUpdatable).toEqual(false);
+      });
+
+      test('should throw error when updat', () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.update);
+        expect(error?.body).toEqual(UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE);
+      });
+
+      test('should not be UpdatableViaImport', () => {
+        expect(timelineObj.isUpdatableViaImport).toEqual(false);
+      });
+
+      test('should throw error when UpdatableViaImport', () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.updateViaImport);
+        expect(error?.body).toEqual(UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE);
+      });
+
+      test('should indicate we are handling a template timeline', () => {
+        expect(timelineObj.isHandlingTemplateTimeline).toEqual(true);
+      });
+    });
+  });
+
+  describe(`Throw error if given title does NOT exists`, () => {
+    describe('timeline', () => {
+      const mockGetTimeline: jest.Mock = jest.fn();
+      const mockGetTemplateTimeline: jest.Mock = jest.fn();
+      let timelineObj: TimelinesStatusType;
+
+      afterEach(() => {
+        jest.clearAllMocks();
+      });
+
+      afterAll(() => {
+        jest.resetModules();
+      });
+
+      beforeAll(() => {
+        jest.resetModules();
+      });
+
+      beforeEach(async () => {
+        jest.doMock('../../saved_object', () => {
+          return {
+            getTimeline: mockGetTimeline.mockReturnValue(null),
+            getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+              timeline: [],
+            }),
+          };
+        });
+
+        const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+          .CompareTimelinesStatus;
+
+        timelineObj = new CompareTimelinesStatus({
+          timelineInput: {
+            id: mockUniqueParsedObjects[0].savedObjectId,
+            type: TimelineType.default,
+            version: mockUniqueParsedObjects[0].version,
+          },
+          timelineType: TimelineType.default,
+          title: null,
+          templateTimelineInput: {
+            id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+            type: TimelineType.template,
+            version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+          },
+          frameworkRequest: {} as FrameworkRequest,
+        });
+
+        await timelineObj.init();
+      });
+
+      test(`should not be creatable`, () => {
+        expect(timelineObj.isCreatable).toEqual(false);
+      });
+
+      test(`throw error on create`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.create);
+        expect(error?.body).toEqual(EMPTY_TITLE_ERROR_MESSAGE);
+      });
+
+      test(`should not be creatable via import`, () => {
+        expect(timelineObj.isCreatableViaImport).toEqual(false);
+      });
+
+      test(`throw error when create via import`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.createViaImport);
+        expect(error?.body).toEqual(EMPTY_TITLE_ERROR_MESSAGE);
+      });
+
+      test(`should not be updatable`, () => {
+        expect(timelineObj.isUpdatable).toEqual(false);
+      });
+
+      test(`throw error when update`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.update);
+        expect(error?.body).toEqual(EMPTY_TITLE_ERROR_MESSAGE);
+      });
+
+      test(`should not be updatable via import`, () => {
+        expect(timelineObj.isUpdatableViaImport).toEqual(false);
+      });
+    });
+
+    describe('template timeline', () => {
+      const mockGetTimeline: jest.Mock = jest.fn();
+      const mockGetTemplateTimeline: jest.Mock = jest.fn();
+      let timelineObj: TimelinesStatusType;
+
+      afterEach(() => {
+        jest.clearAllMocks();
+      });
+
+      afterAll(() => {
+        jest.resetModules();
+      });
+
+      beforeAll(() => {
+        jest.resetModules();
+      });
+
+      beforeEach(async () => {
+        jest.doMock('../../saved_object', () => {
+          return {
+            getTimeline: mockGetTimeline.mockReturnValue(null),
+            getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+              timeline: [],
+            }),
+          };
+        });
+
+        const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+          .CompareTimelinesStatus;
+
+        timelineObj = new CompareTimelinesStatus({
+          timelineInput: {
+            id: mockUniqueParsedObjects[0].savedObjectId,
+            type: TimelineType.default,
+            version: mockUniqueParsedObjects[0].version,
+          },
+          timelineType: TimelineType.default,
+          title: null,
+          templateTimelineInput: {
+            id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+            type: TimelineType.template,
+            version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+          },
+          frameworkRequest: {} as FrameworkRequest,
+        });
+
+        await timelineObj.init();
+      });
+
+      test(`should not be creatable`, () => {
+        expect(timelineObj.isCreatable).toEqual(false);
+      });
+
+      test(`throw error on create`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.create);
+        expect(error?.body).toEqual(EMPTY_TITLE_ERROR_MESSAGE);
+      });
+
+      test(`should not be creatable via import`, () => {
+        expect(timelineObj.isCreatableViaImport).toEqual(false);
+      });
+
+      test(`throw error when create via import`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.createViaImport);
+        expect(error?.body).toEqual(EMPTY_TITLE_ERROR_MESSAGE);
+      });
+
+      test(`should not be updatable`, () => {
+        expect(timelineObj.isUpdatable).toEqual(false);
+      });
+
+      test(`throw error when update`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.update);
+        expect(error?.body).toEqual(EMPTY_TITLE_ERROR_MESSAGE);
+      });
+
+      test(`should not be updatable via import`, () => {
+        expect(timelineObj.isUpdatableViaImport).toEqual(false);
+      });
+
+      test(`throw error when update via import`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.updateViaImport);
+        expect(error?.body).toEqual(EMPTY_TITLE_ERROR_MESSAGE);
+      });
+    });
+  });
+
+  describe(`Throw error if timeline status is updated`, () => {
+    describe('immutable timeline', () => {
+      const mockGetTimeline: jest.Mock = jest.fn();
+      const mockGetTemplateTimeline: jest.Mock = jest.fn();
+      let timelineObj: TimelinesStatusType;
+
+      afterEach(() => {
+        jest.clearAllMocks();
+      });
+
+      afterAll(() => {
+        jest.resetModules();
+      });
+
+      beforeAll(() => {
+        jest.resetModules();
+      });
+
+      beforeEach(async () => {
+        jest.doMock('../../saved_object', () => {
+          return {
+            getTimeline: mockGetTimeline.mockReturnValue({
+              ...mockGetTimelineValue,
+              status: TimelineStatus.immutable,
+            }),
+            getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+              timeline: [],
+            }),
+          };
+        });
+
+        const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+          .CompareTimelinesStatus;
+
+        timelineObj = new CompareTimelinesStatus({
+          timelineInput: {
+            id: mockUniqueParsedObjects[0].savedObjectId,
+            type: TimelineType.default,
+            version: mockUniqueParsedObjects[0].version,
+          },
+          timelineType: TimelineType.default,
+          title: 'mock title',
+          status: TimelineStatus.immutable,
+          templateTimelineInput: {
+            id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+            type: TimelineType.template,
+            version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+          },
+          frameworkRequest: {} as FrameworkRequest,
+        });
+
+        await timelineObj.init();
+      });
+
+      test(`should not be updatable if existing status is immutable`, () => {
+        expect(timelineObj.isUpdatable).toBe(false);
+      });
+
+      test(`should throw error when update`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.update);
+        expect(error?.body).toEqual(UPDATE_STATUS_ERROR_MESSAGE);
+      });
+
+      test(`should not be updatable via import if existing status is immutable`, () => {
+        expect(timelineObj.isUpdatableViaImport).toBe(false);
+      });
+
+      test(`should throw error when update via import`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.updateViaImport);
+        expect(error?.body).toEqual(
+          getImportExistingTimelineError(mockUniqueParsedObjects[0].savedObjectId)
+        );
+      });
+    });
+
+    describe('immutable template timeline', () => {
+      const mockGetTimeline: jest.Mock = jest.fn();
+      const mockGetTemplateTimeline: jest.Mock = jest.fn();
+      let timelineObj: TimelinesStatusType;
+
+      afterEach(() => {
+        jest.clearAllMocks();
+      });
+
+      afterAll(() => {
+        jest.resetModules();
+      });
+
+      beforeAll(() => {
+        jest.resetModules();
+      });
+
+      beforeEach(async () => {
+        jest.doMock('../../saved_object', () => {
+          return {
+            getTimeline: mockGetTimeline.mockReturnValue({
+              ...mockGetTemplateTimelineValue,
+              status: TimelineStatus.immutable,
+            }),
+            getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+              timeline: [{ ...mockGetTemplateTimelineValue, status: TimelineStatus.immutable }],
+            }),
+          };
+        });
+
+        const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+          .CompareTimelinesStatus;
+
+        timelineObj = new CompareTimelinesStatus({
+          timelineInput: {
+            id: mockUniqueParsedObjects[0].savedObjectId,
+            type: TimelineType.default,
+            version: mockUniqueParsedObjects[0].version,
+          },
+          status: TimelineStatus.immutable,
+          timelineType: TimelineType.template,
+          title: 'mock title',
+          templateTimelineInput: {
+            id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+            type: TimelineType.template,
+            version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+          },
+          frameworkRequest: {} as FrameworkRequest,
+        });
+
+        await timelineObj.init();
+      });
+
+      test(`should not be able to update`, () => {
+        expect(timelineObj.isUpdatable).toEqual(false);
+      });
+
+      test(`should not throw error when update`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.update);
+        expect(error?.body).toEqual(UPDATE_STATUS_ERROR_MESSAGE);
+      });
+
+      test(`should not be able to update via import`, () => {
+        expect(timelineObj.isUpdatableViaImport).toEqual(true);
+      });
+
+      test(`should not throw error when update via import`, () => {
+        const error = timelineObj.checkIsFailureCases(TimelineStatusActions.updateViaImport);
+        expect(error?.body).toBeUndefined();
+      });
+    });
+  });
+
+  describe('If create template timeline without template timeline id', () => {
+    const mockGetTimeline: jest.Mock = jest.fn();
+    const mockGetTemplateTimeline: jest.Mock = jest.fn();
+
+    let timelineObj: TimelinesStatusType;
+
+    afterEach(() => {
+      jest.clearAllMocks();
+    });
+
+    afterAll(() => {
+      jest.resetModules();
+    });
+
+    beforeAll(() => {
+      jest.resetModules();
+    });
+
+    beforeEach(async () => {
+      jest.doMock('../../saved_object', () => ({
+        getTimeline: mockGetTimeline,
+        getTimelineByTemplateTimelineId: mockGetTemplateTimeline,
+      }));
+
+      const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+        .CompareTimelinesStatus;
+
+      timelineObj = new CompareTimelinesStatus({
+        timelineInput: {
+          id: mockUniqueParsedObjects[0].savedObjectId,
+          version: mockUniqueParsedObjects[0].version,
+        },
+        timelineType: TimelineType.template,
+        title: mockUniqueParsedObjects[0].title,
+        templateTimelineInput: {
+          id: null,
+          version: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineVersion,
+        },
+        frameworkRequest: {} as FrameworkRequest,
+      });
+      await timelineObj.init();
+    });
+
+    test('should not be creatable', () => {
+      expect(timelineObj.isCreatable).toEqual(true);
+    });
+
+    test(`throw no error when create`, () => {
+      const error = timelineObj.checkIsFailureCases(TimelineStatusActions.create);
+      expect(error?.body).toBeUndefined();
+    });
+
+    test('should be Creatable via import', () => {
+      expect(timelineObj.isCreatableViaImport).toEqual(true);
+    });
+
+    test(`throw no error when CreatableViaImport`, () => {
+      const error = timelineObj.checkIsFailureCases(TimelineStatusActions.createViaImport);
+      expect(error?.body).toBeUndefined();
+    });
+  });
+
+  describe('Throw error if template timeline version is conflict when update via import', () => {
+    const mockGetTimeline: jest.Mock = jest.fn();
+    const mockGetTemplateTimeline: jest.Mock = jest.fn();
+
+    let timelineObj: TimelinesStatusType;
+
+    afterEach(() => {
+      jest.clearAllMocks();
+    });
+
+    afterAll(() => {
+      jest.resetModules();
+    });
+
+    beforeAll(() => {
+      jest.resetModules();
+    });
+
+    beforeEach(async () => {
+      jest.doMock('../../saved_object', () => ({
+        getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue),
+        getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({
+          timeline: [mockGetTemplateTimelineValue],
+        }),
+      }));
+
+      const CompareTimelinesStatus = jest.requireActual('./compare_timelines_status')
+        .CompareTimelinesStatus;
+
+      timelineObj = new CompareTimelinesStatus({
+        timelineInput: {
+          id: mockUniqueParsedObjects[0].savedObjectId,
+          version: mockUniqueParsedObjects[0].version,
+        },
+        timelineType: TimelineType.template,
+        title: mockUniqueParsedObjects[0].title,
+        templateTimelineInput: {
+          id: mockUniqueParsedTemplateTimelineObjects[0].templateTimelineId,
+          version: mockGetTemplateTimelineValue.templateTimelineVersion,
+        },
+        frameworkRequest: {} as FrameworkRequest,
+      });
+      await timelineObj.init();
+    });
+
+    test('should not be creatable', () => {
+      expect(timelineObj.isCreatable).toEqual(false);
+    });
+
+    test(`throw error when create`, () => {
+      const error = timelineObj.checkIsFailureCases(TimelineStatusActions.create);
+      expect(error?.body).toEqual(CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE);
+    });
+
+    test('should not be Creatable via import', () => {
+      expect(timelineObj.isCreatableViaImport).toEqual(false);
+    });
+
+    test(`throw error when CreatableViaImport`, () => {
+      const error = timelineObj.checkIsFailureCases(TimelineStatusActions.createViaImport);
+      expect(error?.body).toEqual(
+        getImportExistingTimelineError(mockUniqueParsedObjects[0].savedObjectId)
+      );
+    });
+
+    test('should be updatable', () => {
+      expect(timelineObj.isUpdatable).toEqual(true);
+    });
+
+    test(`throw no error when update`, () => {
+      const error = timelineObj.checkIsFailureCases(TimelineStatusActions.update);
+      expect(error).toBeNull();
+    });
+
+    test('should not be updatable via import', () => {
+      expect(timelineObj.isUpdatableViaImport).toEqual(false);
+    });
+
+    test(`throw error when UpdatableViaImport`, () => {
+      const error = timelineObj.checkIsFailureCases(TimelineStatusActions.updateViaImport);
+      expect(error?.body).toEqual(TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE);
+    });
+  });
+});
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts
new file mode 100644
index 0000000000000..d61d217a4cf49
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/compare_timelines_status.ts
@@ -0,0 +1,247 @@
+/*
+ * 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 { isEmpty } from 'lodash/fp';
+import {
+  TimelineTypeLiteralWithNull,
+  TimelineType,
+  TimelineStatus,
+  TimelineTypeLiteral,
+} from '../../../../../common/types/timeline';
+import { FrameworkRequest } from '../../../framework';
+
+import { TimelineStatusActions, TimelineStatusAction } from './common';
+import { TimelineObject } from './timeline_object';
+import {
+  checkIsCreateFailureCases,
+  checkIsUpdateFailureCases,
+  checkIsCreateViaImportFailureCases,
+  checkIsUpdateViaImportFailureCases,
+  commonFailureChecker,
+} from './failure_cases';
+
+interface GivenTimelineInput {
+  id: string | null | undefined;
+  type?: TimelineTypeLiteralWithNull;
+  version: string | number | null | undefined;
+}
+
+interface TimelinesStatusProps {
+  status: TimelineStatus | null | undefined;
+  title: string | null | undefined;
+  timelineType: TimelineTypeLiteralWithNull | undefined;
+  timelineInput: GivenTimelineInput;
+  templateTimelineInput: GivenTimelineInput;
+  frameworkRequest: FrameworkRequest;
+}
+
+export class CompareTimelinesStatus {
+  public readonly timelineObject: TimelineObject;
+  public readonly templateTimelineObject: TimelineObject;
+  private readonly timelineType: TimelineTypeLiteral;
+  private readonly title: string | null;
+  private readonly status: TimelineStatus;
+  constructor({
+    status = TimelineStatus.active,
+    title,
+    timelineType = TimelineType.default,
+    timelineInput,
+    templateTimelineInput,
+    frameworkRequest,
+  }: TimelinesStatusProps) {
+    this.timelineObject = new TimelineObject({
+      id: timelineInput.id,
+      type: timelineInput.type ?? TimelineType.default,
+      version: timelineInput.version,
+      frameworkRequest,
+    });
+
+    this.templateTimelineObject = new TimelineObject({
+      id: templateTimelineInput.id,
+      type: templateTimelineInput.type ?? TimelineType.template,
+      version: templateTimelineInput.version,
+      frameworkRequest,
+    });
+
+    this.timelineType = timelineType ?? TimelineType.default;
+    this.title = title ?? null;
+    this.status = status ?? TimelineStatus.active;
+  }
+
+  public get isCreatable() {
+    return (
+      this.isTitleValid &&
+      !this.isSavedObjectVersionConflict &&
+      ((this.timelineObject.isCreatable && !this.isHandlingTemplateTimeline) ||
+        (this.templateTimelineObject.isCreatable &&
+          this.timelineObject.isCreatable &&
+          this.isHandlingTemplateTimeline))
+    );
+  }
+
+  public get isCreatableViaImport() {
+    return (
+      this.isCreatedStatusValid &&
+      ((this.isCreatable && !this.isHandlingTemplateTimeline) ||
+        (this.isCreatable && this.isHandlingTemplateTimeline && this.isTemplateVersionValid))
+    );
+  }
+
+  private get isCreatedStatusValid() {
+    const obj = this.isHandlingTemplateTimeline ? this.templateTimelineObject : this.timelineObject;
+
+    return obj.isExists
+      ? this.status === obj.getData?.status && this.status !== TimelineStatus.draft
+      : this.status !== TimelineStatus.draft;
+  }
+
+  public get isUpdatable() {
+    return (
+      this.isTitleValid &&
+      !this.isSavedObjectVersionConflict &&
+      ((this.timelineObject.isUpdatable && !this.isHandlingTemplateTimeline) ||
+        (this.templateTimelineObject.isUpdatable && this.isHandlingTemplateTimeline))
+    );
+  }
+
+  private get isTimelineTypeValid() {
+    const obj = this.isHandlingTemplateTimeline ? this.templateTimelineObject : this.timelineObject;
+    const existintTimelineType = obj.getData?.timelineType ?? TimelineType.default;
+    return obj.isExists ? this.timelineType === existintTimelineType : true;
+  }
+
+  public get isUpdatableViaImport() {
+    return (
+      this.isTimelineTypeValid &&
+      this.isTitleValid &&
+      this.isUpdatedTimelineStatusValid &&
+      (this.timelineObject.isUpdatableViaImport ||
+        (this.templateTimelineObject.isUpdatableViaImport &&
+          this.isTemplateVersionValid &&
+          this.isHandlingTemplateTimeline))
+    );
+  }
+
+  public get isTitleValid() {
+    return (
+      (this.status !== TimelineStatus.draft && !isEmpty(this.title)) ||
+      this.status === TimelineStatus.draft
+    );
+  }
+
+  public getFailureChecker(action?: TimelineStatusAction) {
+    if (action === TimelineStatusActions.create) {
+      return checkIsCreateFailureCases;
+    } else if (action === TimelineStatusActions.createViaImport) {
+      return checkIsCreateViaImportFailureCases;
+    } else if (action === TimelineStatusActions.update) {
+      return checkIsUpdateFailureCases;
+    } else {
+      return checkIsUpdateViaImportFailureCases;
+    }
+  }
+
+  public checkIsFailureCases(action?: TimelineStatusAction) {
+    const failureChecker = this.getFailureChecker(action);
+    const version = this.templateTimelineObject.getVersion;
+    const commonError = commonFailureChecker(this.status, this.title);
+    if (commonError != null) {
+      return commonError;
+    }
+
+    const msg = failureChecker(
+      this.isHandlingTemplateTimeline,
+      this.status,
+      this.timelineType,
+      this.timelineObject.getVersion?.toString() ?? null,
+      version != null && typeof version === 'string' ? parseInt(version, 10) : version,
+      this.templateTimelineObject.getId,
+      this.timelineObject.getData,
+      this.templateTimelineObject.getData
+    );
+    return msg;
+  }
+
+  public get templateTimelineInput() {
+    return this.templateTimelineObject;
+  }
+
+  public get timelineInput() {
+    return this.timelineObject;
+  }
+
+  private getTimelines() {
+    return Promise.all([
+      this.timelineObject.getTimeline(),
+      this.templateTimelineObject.getTimeline(),
+    ]);
+  }
+
+  public get isHandlingTemplateTimeline() {
+    return this.timelineType === TimelineType.template;
+  }
+
+  private get isSavedObjectVersionConflict() {
+    const version = this.timelineObject?.getVersion;
+    const existingVersion = this.timelineObject?.data?.version;
+    if (version != null && this.timelineObject.isExists) {
+      return version !== existingVersion;
+    } else if (this.timelineObject.isExists && version == null) {
+      return true;
+    }
+    return false;
+  }
+
+  private get isTemplateVersionConflict() {
+    const version = this.templateTimelineObject?.getVersion;
+    const existingTemplateTimelineVersion = this.templateTimelineObject?.data
+      ?.templateTimelineVersion;
+    if (
+      version != null &&
+      this.templateTimelineObject.isExists &&
+      existingTemplateTimelineVersion != null
+    ) {
+      return version <= existingTemplateTimelineVersion;
+    } else if (this.templateTimelineObject.isExists && version == null) {
+      return true;
+    }
+    return false;
+  }
+
+  private get isTemplateVersionValid() {
+    const version = this.templateTimelineObject?.getVersion;
+    return typeof version === 'number' && !this.isTemplateVersionConflict;
+  }
+
+  private get isUpdatedTimelineStatusValid() {
+    const status = this.status;
+    const existingStatus = this.isHandlingTemplateTimeline
+      ? this.templateTimelineInput.data?.status
+      : this.timelineInput.data?.status;
+    return (
+      ((existingStatus == null || existingStatus === TimelineStatus.active) &&
+        (status == null || status === TimelineStatus.active)) ||
+      (existingStatus != null && status === existingStatus)
+    );
+  }
+
+  public get timelineId() {
+    if (this.isHandlingTemplateTimeline) {
+      return this.templateTimelineInput.data?.savedObjectId ?? this.templateTimelineInput.getId;
+    }
+    return this.timelineInput.data?.savedObjectId ?? this.timelineInput.getId;
+  }
+
+  public get timelineVersion() {
+    const version = this.isHandlingTemplateTimeline
+      ? this.templateTimelineInput.data?.version ?? this.timelineInput.getVersion
+      : this.timelineInput.data?.version ?? this.timelineInput.getVersion;
+    return version != null ? version.toString() : null;
+  }
+
+  public async init() {
+    await this.getTimelines();
+  }
+}
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
index 5b2470821b690..abe298566341c 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/create_timelines.ts
@@ -12,6 +12,7 @@ import { FrameworkRequest } from '../../../framework';
 import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/timeline';
 import { SavedNote } from '../../../../../common/types/timeline/note';
 import { NoteResult, ResponseTimeline } from '../../../../graphql/types';
+
 export const CREATE_TIMELINE_ERROR_MESSAGE =
   'UPDATE timeline with POST is not allowed, please use PATCH instead';
 export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
@@ -20,16 +21,10 @@ export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
 export const saveTimelines = (
   frameworkRequest: FrameworkRequest,
   timeline: SavedTimeline,
-  timelineSavedObjectId?: string | null,
-  timelineVersion?: string | null
-): Promise<ResponseTimeline> => {
-  return timelineLib.persistTimeline(
-    frameworkRequest,
-    timelineSavedObjectId ?? null,
-    timelineVersion ?? null,
-    timeline
-  );
-};
+  timelineSavedObjectId: string | null = null,
+  timelineVersion: string | null = null
+): Promise<ResponseTimeline> =>
+  timelineLib.persistTimeline(frameworkRequest, timelineSavedObjectId, timelineVersion, timeline);
 
 export const savePinnedEvents = (
   frameworkRequest: FrameworkRequest,
@@ -72,15 +67,25 @@ export const saveNotes = (
   );
 };
 
-export const createTimelines = async (
-  frameworkRequest: FrameworkRequest,
-  timeline: SavedTimeline,
-  timelineSavedObjectId?: string | null,
-  timelineVersion?: string | null,
-  pinnedEventIds?: string[] | null,
-  notes?: NoteResult[],
-  existingNoteIds?: string[]
-): Promise<ResponseTimeline> => {
+interface CreateTimelineProps {
+  frameworkRequest: FrameworkRequest;
+  timeline: SavedTimeline;
+  timelineSavedObjectId?: string | null;
+  timelineVersion?: string | null;
+  pinnedEventIds?: string[] | null;
+  notes?: NoteResult[];
+  existingNoteIds?: string[];
+}
+
+export const createTimelines = async ({
+  frameworkRequest,
+  timeline,
+  timelineSavedObjectId = null,
+  timelineVersion = null,
+  pinnedEventIds = null,
+  notes = [],
+  existingNoteIds = [],
+}: CreateTimelineProps): Promise<ResponseTimeline> => {
   const responseTimeline = await saveTimelines(
     frameworkRequest,
     timeline,
@@ -89,7 +94,6 @@ export const createTimelines = async (
   );
   const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId;
   const newTimelineVersion = responseTimeline.timeline.version;
-
   let myPromises: unknown[] = [];
   if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) {
     myPromises = [
@@ -143,8 +147,9 @@ export const getTemplateTimeline = async (
       frameworkRequest,
       templateTimelineId
     );
+    // eslint-disable-next-line no-empty
   } catch (e) {
     return null;
   }
-  return templateTimeline.timeline[0];
+  return templateTimeline?.timeline[0] ?? null;
 };
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts
index 1f02851c56b80..23090bfc6f0bd 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/export_timelines.ts
@@ -4,6 +4,8 @@
  * you may not use this file except in compliance with the Elastic License.
  */
 
+import { omit } from 'lodash/fp';
+
 import {
   SavedObjectsClient,
   SavedObjectsFindOptions,
@@ -16,7 +18,6 @@ import {
   ExportedNotes,
   TimelineSavedObject,
   ExportTimelineNotFoundError,
-  TimelineStatus,
 } from '../../../../../common/types/timeline';
 import { NoteSavedObject } from '../../../../../common/types/timeline/note';
 import { PinnedEventSavedObject } from '../../../../../common/types/timeline/pinned_event';
@@ -180,12 +181,11 @@ const getTimelinesFromObjects = async (
     if (myTimeline != null) {
       const timelineNotes = myNotes.filter((n) => n.timelineId === timelineId);
       const timelinePinnedEventIds = myPinnedEventIds.filter((p) => p.timelineId === timelineId);
+      const exportedTimeline = omit('status', myTimeline);
       return [
         ...acc,
         {
-          ...myTimeline,
-          status:
-            myTimeline.status === TimelineStatus.draft ? TimelineStatus.active : myTimeline.status,
+          ...exportedTimeline,
           ...getGlobalEventNotesByTimelineId(timelineNotes),
           pinnedEventIds: getPinnedEventsIdsByTimelineId(timelinePinnedEventIds),
         },
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
new file mode 100644
index 0000000000000..60ba5389280c4
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/failure_cases.ts
@@ -0,0 +1,377 @@
+/*
+ * 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 { isEmpty } from 'lodash/fp';
+import {
+  TimelineSavedObject,
+  TimelineStatus,
+  TimelineTypeLiteral,
+} from '../../../../../common/types/timeline';
+
+export const UPDATE_TIMELINE_ERROR_MESSAGE =
+  'CREATE timeline with PATCH is not allowed, please use POST instead';
+export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
+  "CREATE template timeline with PATCH is not allowed, please use POST instead (Given template timeline doesn't exist)";
+export const NO_MATCH_VERSION_ERROR_MESSAGE =
+  'TimelineVersion conflict: The given version doesn not match with existing timeline';
+export const NO_MATCH_ID_ERROR_MESSAGE =
+  "Timeline id doesn't match with existing template timeline";
+export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict';
+export const CREATE_TIMELINE_ERROR_MESSAGE =
+  'UPDATE timeline with POST is not allowed, please use PATCH instead';
+export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
+  'UPDATE template timeline with POST is not allowed, please use PATCH instead';
+export const EMPTY_TITLE_ERROR_MESSAGE = 'Title cannot be empty';
+export const UPDATE_STATUS_ERROR_MESSAGE = 'Update an immutable timeline is is not allowed';
+export const CREATE_TEMPLATE_TIMELINE_WITHOUT_VERSION_ERROR_MESSAGE =
+  'Create template timeline without a valid templateTimelineVersion is not allowed. Please start from 1 to create a new template timeline';
+export const CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE = 'Cannot create a draft timeline';
+export const NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE = 'Update status is not allowed';
+export const NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE = 'Update timelineType is not allowed';
+export const UPDAT_TIMELINE_VIA_IMPORT_NOT_ALLOWED_ERROR_MESSAGE =
+  'Update timeline via import is not allowed';
+
+const isUpdatingStatus = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  const obj = isHandlingTemplateTimeline ? existTemplateTimeline : existTimeline;
+  return obj?.status === TimelineStatus.immutable ? UPDATE_STATUS_ERROR_MESSAGE : null;
+};
+
+const isGivenTitleValid = (status: TimelineStatus, title: string | null | undefined) => {
+  return (status !== TimelineStatus.draft && !isEmpty(title)) || status === TimelineStatus.draft
+    ? null
+    : EMPTY_TITLE_ERROR_MESSAGE;
+};
+
+export const getImportExistingTimelineError = (id: string) =>
+  `savedObjectId: "${id}" already exists`;
+
+export const commonFailureChecker = (status: TimelineStatus, title: string | null | undefined) => {
+  const error = [isGivenTitleValid(status, title)].filter((msg) => msg != null).join(',');
+  return !isEmpty(error)
+    ? {
+        body: error,
+        statusCode: 405,
+      }
+    : null;
+};
+
+const commonUpdateTemplateTimelineCheck = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus | null | undefined,
+  timelineType: TimelineTypeLiteral,
+  version: string | null,
+  templateTimelineVersion: number | null,
+  templateTimelineId: string | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  if (isHandlingTemplateTimeline) {
+    if (existTimeline != null && timelineType !== existTimeline.timelineType) {
+      return {
+        body: NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE,
+        statusCode: 403,
+      };
+    }
+
+    if (existTemplateTimeline == null && templateTimelineVersion != null) {
+      // template timeline !exists
+      // Throw error to create template timeline in patch
+      return {
+        body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
+        statusCode: 405,
+      };
+    }
+
+    if (
+      existTimeline != null &&
+      existTemplateTimeline != null &&
+      existTimeline.savedObjectId !== existTemplateTimeline.savedObjectId
+    ) {
+      // Throw error you can not have a no matching between your timeline and your template timeline during an update
+      return {
+        body: NO_MATCH_ID_ERROR_MESSAGE,
+        statusCode: 409,
+      };
+    }
+
+    if (
+      existTemplateTimeline != null &&
+      existTemplateTimeline.templateTimelineVersion == null &&
+      existTemplateTimeline.version !== version
+    ) {
+      // throw error 409 conflict timeline
+      return {
+        body: NO_MATCH_VERSION_ERROR_MESSAGE,
+        statusCode: 409,
+      };
+    }
+  }
+  return null;
+};
+
+const commonUpdateTimelineCheck = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus | null | undefined,
+  timelineType: TimelineTypeLiteral,
+  version: string | null,
+  templateTimelineVersion: number | null,
+  templateTimelineId: string | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  if (existTimeline == null) {
+    // timeline !exists
+    return {
+      body: UPDATE_TIMELINE_ERROR_MESSAGE,
+      statusCode: 405,
+    };
+  }
+
+  if (existTimeline?.version !== version) {
+    // throw error 409 conflict timeline
+    return {
+      body: NO_MATCH_VERSION_ERROR_MESSAGE,
+      statusCode: 409,
+    };
+  }
+
+  return null;
+};
+
+const commonUpdateCases = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus | null | undefined,
+  timelineType: TimelineTypeLiteral,
+  version: string | null,
+  templateTimelineVersion: number | null,
+  templateTimelineId: string | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  if (isHandlingTemplateTimeline) {
+    return commonUpdateTemplateTimelineCheck(
+      isHandlingTemplateTimeline,
+      status,
+      timelineType,
+      version,
+      templateTimelineVersion,
+      templateTimelineId,
+      existTimeline,
+      existTemplateTimeline
+    );
+  } else {
+    return commonUpdateTimelineCheck(
+      isHandlingTemplateTimeline,
+      status,
+      timelineType,
+      version,
+      templateTimelineVersion,
+      templateTimelineId,
+      existTimeline,
+      existTemplateTimeline
+    );
+  }
+};
+
+const createTemplateTimelineCheck = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus,
+  timelineType: TimelineTypeLiteral,
+  version: string | null,
+  templateTimelineVersion: number | null,
+  templateTimelineId: string | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  if (isHandlingTemplateTimeline && existTemplateTimeline != null) {
+    // Throw error to create template timeline in patch
+    return {
+      body: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
+      statusCode: 405,
+    };
+  } else if (isHandlingTemplateTimeline && templateTimelineVersion == null) {
+    return {
+      body: CREATE_TEMPLATE_TIMELINE_WITHOUT_VERSION_ERROR_MESSAGE,
+      statusCode: 403,
+    };
+  } else {
+    return null;
+  }
+};
+
+export const checkIsUpdateViaImportFailureCases = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus | null | undefined,
+  timelineType: TimelineTypeLiteral,
+  version: string | null,
+  templateTimelineVersion: number | null,
+  templateTimelineId: string | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  if (!isHandlingTemplateTimeline) {
+    if (existTimeline == null) {
+      return { body: UPDAT_TIMELINE_VIA_IMPORT_NOT_ALLOWED_ERROR_MESSAGE, statusCode: 405 };
+    } else {
+      return {
+        body: getImportExistingTimelineError(existTimeline!.savedObjectId),
+        statusCode: 405,
+      };
+    }
+  } else {
+    if (existTemplateTimeline != null && timelineType !== existTemplateTimeline?.timelineType) {
+      return {
+        body: NOT_ALLOW_UPDATE_TIMELINE_TYPE_ERROR_MESSAGE,
+        statusCode: 403,
+      };
+    }
+    const isStatusValid =
+      ((existTemplateTimeline?.status == null ||
+        existTemplateTimeline?.status === TimelineStatus.active) &&
+        (status == null || status === TimelineStatus.active)) ||
+      (existTemplateTimeline?.status != null && status === existTemplateTimeline?.status);
+
+    if (!isStatusValid) {
+      return {
+        body: NOT_ALLOW_UPDATE_STATUS_ERROR_MESSAGE,
+        statusCode: 405,
+      };
+    }
+
+    const error = commonUpdateTemplateTimelineCheck(
+      isHandlingTemplateTimeline,
+      status,
+      timelineType,
+      version,
+      templateTimelineVersion,
+      templateTimelineId,
+      existTimeline,
+      existTemplateTimeline
+    );
+    if (error) {
+      return error;
+    }
+    if (
+      templateTimelineVersion != null &&
+      existTemplateTimeline != null &&
+      existTemplateTimeline.templateTimelineVersion != null &&
+      existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion
+    ) {
+      // Throw error you can not update a template timeline version with an old version
+      return {
+        body: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE,
+        statusCode: 409,
+      };
+    }
+  }
+  return null;
+};
+
+export const checkIsUpdateFailureCases = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus | null | undefined,
+  timelineType: TimelineTypeLiteral,
+  version: string | null,
+  templateTimelineVersion: number | null,
+  templateTimelineId: string | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  const error = isUpdatingStatus(
+    isHandlingTemplateTimeline,
+    status,
+    existTimeline,
+    existTemplateTimeline
+  );
+  if (error) {
+    return {
+      body: error,
+      statusCode: 403,
+    };
+  }
+  return commonUpdateCases(
+    isHandlingTemplateTimeline,
+    status,
+    timelineType,
+    version,
+    templateTimelineVersion,
+    templateTimelineId,
+    existTimeline,
+    existTemplateTimeline
+  );
+};
+
+export const checkIsCreateFailureCases = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus,
+  timelineType: TimelineTypeLiteral,
+  version: string | null,
+  templateTimelineVersion: number | null,
+  templateTimelineId: string | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  if (!isHandlingTemplateTimeline && existTimeline != null) {
+    return {
+      body: CREATE_TIMELINE_ERROR_MESSAGE,
+      statusCode: 405,
+    };
+  } else if (isHandlingTemplateTimeline) {
+    return createTemplateTimelineCheck(
+      isHandlingTemplateTimeline,
+      status,
+      timelineType,
+      version,
+      templateTimelineVersion,
+      templateTimelineId,
+      existTimeline,
+      existTemplateTimeline
+    );
+  } else {
+    return null;
+  }
+};
+
+export const checkIsCreateViaImportFailureCases = (
+  isHandlingTemplateTimeline: boolean,
+  status: TimelineStatus | null | undefined,
+  timelineType: TimelineTypeLiteral,
+  version: string | null,
+  templateTimelineVersion: number | null,
+  templateTimelineId: string | null | undefined,
+  existTimeline: TimelineSavedObject | null,
+  existTemplateTimeline: TimelineSavedObject | null
+) => {
+  if (status === TimelineStatus.draft) {
+    return {
+      body: CREATE_WITH_INVALID_STATUS_ERROR_MESSAGE,
+      statusCode: 405,
+    };
+  }
+
+  if (!isHandlingTemplateTimeline) {
+    if (existTimeline != null) {
+      return {
+        body: getImportExistingTimelineError(existTimeline.savedObjectId),
+        statusCode: 405,
+      };
+    }
+  } else {
+    if (existTemplateTimeline != null) {
+      // Throw error to create template timeline in patch
+      return {
+        body: getImportExistingTimelineError(existTemplateTimeline.savedObjectId),
+        statusCode: 405,
+      };
+    }
+  }
+
+  return null;
+};
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/timeline_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/timeline_object.ts
new file mode 100644
index 0000000000000..9fb96b509ec3e
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/timeline_object.ts
@@ -0,0 +1,86 @@
+/*
+ * 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 {
+  TimelineType,
+  TimelineTypeLiteral,
+  TimelineSavedObject,
+  TimelineStatus,
+} from '../../../../../common/types/timeline';
+import { getTimeline, getTemplateTimeline } from './create_timelines';
+import { FrameworkRequest } from '../../../framework';
+
+interface TimelineObjectProps {
+  id: string | null | undefined;
+  type: TimelineTypeLiteral;
+  version: string | number | null | undefined;
+  frameworkRequest: FrameworkRequest;
+}
+
+export class TimelineObject {
+  public readonly id: string | null;
+  private type: TimelineTypeLiteral;
+  public readonly version: string | number | null;
+  private frameworkRequest: FrameworkRequest;
+
+  public data: TimelineSavedObject | null;
+
+  constructor({
+    id = null,
+    type = TimelineType.default,
+    version = null,
+    frameworkRequest,
+  }: TimelineObjectProps) {
+    this.id = id;
+    this.type = type;
+
+    this.version = version;
+    this.frameworkRequest = frameworkRequest;
+    this.data = null;
+  }
+
+  public async getTimeline() {
+    this.data =
+      this.id != null
+        ? this.type === TimelineType.template
+          ? await getTemplateTimeline(this.frameworkRequest, this.id)
+          : await getTimeline(this.frameworkRequest, this.id)
+        : null;
+
+    return this.data;
+  }
+
+  public get getData() {
+    return this.data;
+  }
+
+  private get isImmutable() {
+    return this.data?.status === TimelineStatus.immutable;
+  }
+
+  public get isExists() {
+    return this.data != null;
+  }
+
+  public get isUpdatable() {
+    return this.isExists && !this.isImmutable;
+  }
+
+  public get isCreatable() {
+    return !this.isExists;
+  }
+
+  public get isUpdatableViaImport() {
+    return this.type === TimelineType.template && this.isExists;
+  }
+
+  public get getVersion() {
+    return this.version;
+  }
+
+  public get getId() {
+    return this.id;
+  }
+}
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/update_timelines.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/update_timelines.ts
deleted file mode 100644
index a4efa676daddc..0000000000000
--- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/update_timelines.ts
+++ /dev/null
@@ -1,80 +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 { TimelineSavedObject } from '../../../../../common/types/timeline';
-
-export const UPDATE_TIMELINE_ERROR_MESSAGE =
-  'CREATE timeline with PATCH is not allowed, please use POST instead';
-export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE =
-  'CREATE template timeline with PATCH is not allowed, please use POST instead';
-export const NO_MATCH_VERSION_ERROR_MESSAGE =
-  'TimelineVersion conflict: The given version doesn not match with existing timeline';
-export const NO_MATCH_ID_ERROR_MESSAGE =
-  "Timeline id doesn't match with existing template timeline";
-export const TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE = 'Template timelineVersion conflict';
-
-export const checkIsFailureCases = (
-  isHandlingTemplateTimeline: boolean,
-  version: string | null,
-  templateTimelineVersion: number | null,
-  existTimeline: TimelineSavedObject | null,
-  existTemplateTimeline: TimelineSavedObject | null
-) => {
-  if (!isHandlingTemplateTimeline && existTimeline == null) {
-    return {
-      body: UPDATE_TIMELINE_ERROR_MESSAGE,
-      statusCode: 405,
-    };
-  } else if (isHandlingTemplateTimeline && existTemplateTimeline == null) {
-    // Throw error to create template timeline in patch
-    return {
-      body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE,
-      statusCode: 405,
-    };
-  } else if (
-    isHandlingTemplateTimeline &&
-    existTimeline != null &&
-    existTemplateTimeline != null &&
-    existTimeline.savedObjectId !== existTemplateTimeline.savedObjectId
-  ) {
-    // Throw error you can not have a no matching between your timeline and your template timeline during an update
-    return {
-      body: NO_MATCH_ID_ERROR_MESSAGE,
-      statusCode: 409,
-    };
-  } else if (!isHandlingTemplateTimeline && existTimeline?.version !== version) {
-    // throw error 409 conflict timeline
-    return {
-      body: NO_MATCH_VERSION_ERROR_MESSAGE,
-      statusCode: 409,
-    };
-  } else if (
-    isHandlingTemplateTimeline &&
-    existTemplateTimeline != null &&
-    existTemplateTimeline.templateTimelineVersion == null &&
-    existTemplateTimeline.version !== version
-  ) {
-    // throw error 409 conflict timeline
-    return {
-      body: NO_MATCH_VERSION_ERROR_MESSAGE,
-      statusCode: 409,
-    };
-  } else if (
-    isHandlingTemplateTimeline &&
-    templateTimelineVersion != null &&
-    existTemplateTimeline != null &&
-    existTemplateTimeline.templateTimelineVersion != null &&
-    existTemplateTimeline.templateTimelineVersion !== templateTimelineVersion
-  ) {
-    // Throw error you can not update a template timeline version with an old version
-    return {
-      body: TEMPLATE_TIMELINE_VERSION_CONFLICT_MESSAGE,
-      statusCode: 409,
-    };
-  } else {
-    return null;
-  }
-};
diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts
index bbb11cd642c4c..ec90fc6d8e071 100644
--- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts
+++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object.ts
@@ -7,13 +7,20 @@
 import { getOr } from 'lodash/fp';
 
 import { SavedObjectsFindOptions } from '../../../../../../src/core/server';
-import { UNAUTHENTICATED_USER, disableTemplate } from '../../../common/constants';
+import {
+  UNAUTHENTICATED_USER,
+  disableTemplate,
+  enableElasticFilter,
+} from '../../../common/constants';
 import { NoteSavedObject } from '../../../common/types/timeline/note';
 import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event';
 import {
   SavedTimeline,
   TimelineSavedObject,
   TimelineTypeLiteralWithNull,
+  TimelineStatusLiteralWithNull,
+  TemplateTimelineTypeLiteralWithNull,
+  TemplateTimelineType,
 } from '../../../common/types/timeline';
 import {
   ResponseTimeline,
@@ -38,6 +45,14 @@ interface ResponseTimelines {
   totalCount: number;
 }
 
+interface AllTimelinesResponse extends ResponseTimelines {
+  defaultTimelineCount: number;
+  templateTimelineCount: number;
+  elasticTemplateTimelineCount: number;
+  customTemplateTimelineCount: number;
+  favoriteCount: number;
+}
+
 export interface ResponseTemplateTimeline {
   code?: Maybe<number>;
 
@@ -55,8 +70,10 @@ export interface Timeline {
     pageInfo: PageInfoTimeline | null,
     search: string | null,
     sort: SortTimeline | null,
-    timelineType: TimelineTypeLiteralWithNull
-  ) => Promise<ResponseTimelines>;
+    status: TimelineStatusLiteralWithNull,
+    timelineType: TimelineTypeLiteralWithNull,
+    templateTimelineType: TemplateTimelineTypeLiteralWithNull
+  ) => Promise<AllTimelinesResponse>;
 
   persistFavorite: (
     request: FrameworkRequest,
@@ -97,7 +114,7 @@ export const getTimelineByTemplateTimelineId = async (
 }> => {
   const options: SavedObjectsFindOptions = {
     type: timelineSavedObjectType,
-    filter: `siem-ui-timeline.attributes.templateTimelineId: ${templateTimelineId}`,
+    filter: `siem-ui-timeline.attributes.templateTimelineId: "${templateTimelineId}"`,
   };
   return getAllSavedTimeline(request, options);
 };
@@ -106,10 +123,13 @@ export const getTimelineByTemplateTimelineId = async (
  * which has no timelineType exists in the savedObject */
 const getTimelineTypeFilter = (
   timelineType: TimelineTypeLiteralWithNull,
-  includeDraft: boolean
+  templateTimelineType: TemplateTimelineTypeLiteralWithNull,
+  status: TimelineStatusLiteralWithNull
 ) => {
   const typeFilter =
-    timelineType === TimelineType.template
+    timelineType == null
+      ? null
+      : timelineType === TimelineType.template
       ? `siem-ui-timeline.attributes.timelineType: ${TimelineType.template}` /** Show only whose timelineType exists and equals to "template" */
       : /** Show me every timeline whose timelineType is not "template".
          * which includes timelineType === 'default' and
@@ -119,10 +139,30 @@ const getTimelineTypeFilter = (
   /** Show me every timeline whose status is not "draft".
    * which includes status === 'active' and
    * those status doesn't exists */
-  const draftFilter = includeDraft
-    ? `siem-ui-timeline.attributes.status: ${TimelineStatus.draft}`
-    : `not siem-ui-timeline.attributes.status: ${TimelineStatus.draft}`;
-  return `${typeFilter} and ${draftFilter}`;
+  const draftFilter =
+    status === TimelineStatus.draft
+      ? `siem-ui-timeline.attributes.status: ${TimelineStatus.draft}`
+      : `not siem-ui-timeline.attributes.status: ${TimelineStatus.draft}`;
+
+  const immutableFilter =
+    status == null
+      ? null
+      : status === TimelineStatus.immutable
+      ? `siem-ui-timeline.attributes.status: ${TimelineStatus.immutable}`
+      : `not siem-ui-timeline.attributes.status: ${TimelineStatus.immutable}`;
+
+  const templateTimelineTypeFilter =
+    templateTimelineType == null
+      ? null
+      : templateTimelineType === TemplateTimelineType.elastic
+      ? `siem-ui-timeline.attributes.createdBy: "Elsatic"`
+      : `not siem-ui-timeline.attributes.createdBy: "Elastic"`;
+
+  const filters =
+    !disableTemplate && enableElasticFilter
+      ? [typeFilter, draftFilter, immutableFilter, templateTimelineTypeFilter]
+      : [typeFilter, draftFilter, immutableFilter];
+  return filters.filter((f) => f != null).join(' and ');
 };
 
 export const getAllTimeline = async (
@@ -131,8 +171,10 @@ export const getAllTimeline = async (
   pageInfo: PageInfoTimeline | null,
   search: string | null,
   sort: SortTimeline | null,
-  timelineType: TimelineTypeLiteralWithNull
-): Promise<ResponseTimelines> => {
+  status: TimelineStatusLiteralWithNull,
+  timelineType: TimelineTypeLiteralWithNull,
+  templateTimelineType: TemplateTimelineTypeLiteralWithNull
+): Promise<AllTimelinesResponse> => {
   const options: SavedObjectsFindOptions = {
     type: timelineSavedObjectType,
     perPage: pageInfo != null ? pageInfo.pageSize : undefined,
@@ -144,13 +186,78 @@ export const getAllTimeline = async (
     /**
      * CreateTemplateTimelineBtn
      * Remove the comment here to enable template timeline and apply the change below
-     * filter: getTimelineTypeFilter(timelineType, false)
+     * filter: getTimelineTypeFilter(timelineType, templateTimelineType, false)
      */
-    filter: getTimelineTypeFilter(disableTemplate ? TimelineType.default : timelineType, false),
+    filter: getTimelineTypeFilter(
+      disableTemplate ? TimelineType.default : timelineType,
+      disableTemplate ? null : templateTimelineType,
+      disableTemplate ? null : status
+    ),
     sortField: sort != null ? sort.sortField : undefined,
     sortOrder: sort != null ? sort.sortOrder : undefined,
   };
-  return getAllSavedTimeline(request, options);
+
+  const timelineOptions = {
+    type: timelineSavedObjectType,
+    perPage: 1,
+    page: 1,
+    filter: getTimelineTypeFilter(TimelineType.default, null, TimelineStatus.active),
+  };
+
+  const templateTimelineOptions = {
+    type: timelineSavedObjectType,
+    perPage: 1,
+    page: 1,
+    filter: getTimelineTypeFilter(TimelineType.template, null, null),
+  };
+
+  const elasticTemplateTimelineOptions = {
+    type: timelineSavedObjectType,
+    perPage: 1,
+    page: 1,
+    filter: getTimelineTypeFilter(
+      TimelineType.template,
+      TemplateTimelineType.elastic,
+      TimelineStatus.immutable
+    ),
+  };
+
+  const customTemplateTimelineOptions = {
+    type: timelineSavedObjectType,
+    perPage: 1,
+    page: 1,
+    filter: getTimelineTypeFilter(
+      TimelineType.template,
+      TemplateTimelineType.custom,
+      TimelineStatus.active
+    ),
+  };
+
+  const favoriteTimelineOptions = {
+    type: timelineSavedObjectType,
+    searchFields: ['title', 'description', 'favorite.keySearch'],
+    perPage: 1,
+    page: 1,
+    filter: getTimelineTypeFilter(timelineType, null, TimelineStatus.active),
+  };
+
+  const result = await Promise.all([
+    getAllSavedTimeline(request, options),
+    getAllSavedTimeline(request, timelineOptions),
+    getAllSavedTimeline(request, templateTimelineOptions),
+    getAllSavedTimeline(request, elasticTemplateTimelineOptions),
+    getAllSavedTimeline(request, customTemplateTimelineOptions),
+    getAllSavedTimeline(request, favoriteTimelineOptions),
+  ]);
+
+  return Promise.resolve({
+    ...result[0],
+    defaultTimelineCount: result[1].totalCount,
+    templateTimelineCount: result[2].totalCount,
+    elasticTemplateTimelineCount: result[3].totalCount,
+    customTemplateTimelineCount: result[4].totalCount,
+    favoriteCount: result[5].totalCount,
+  });
 };
 
 export const getDraftTimeline = async (
@@ -160,7 +267,11 @@ export const getDraftTimeline = async (
   const options: SavedObjectsFindOptions = {
     type: timelineSavedObjectType,
     perPage: 1,
-    filter: getTimelineTypeFilter(timelineType, true),
+    filter: getTimelineTypeFilter(
+      timelineType,
+      timelineType === TimelineType.template ? TemplateTimelineType.custom : null,
+      TimelineStatus.draft
+    ),
     sortField: 'created',
     sortOrder: 'desc',
   };
@@ -395,7 +506,6 @@ const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObje
   }
 
   const savedObjects = await savedObjectsClient.find(options);
-
   const timelinesWithNotesAndPinnedEvents = await Promise.all(
     savedObjects.saved_objects.map(async (savedObject) => {
       const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject);

From 40ff82d7794a84cb3faf06b8a3eb201a3da925c9 Mon Sep 17 00:00:00 2001
From: Wylie Conlon <william.conlon@elastic.co>
Date: Sat, 27 Jun 2020 02:20:29 -0400
Subject: [PATCH 21/21] [Lens] Fix broken test (#70117)

---
 .../lens/public/indexpattern_datasource/loader.test.ts       | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
index d8d8ebcf12de4..e8c8c5762bb83 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts
@@ -177,8 +177,7 @@ function mockClient() {
   } as unknown) as Pick<SavedObjectsClientContract, 'find' | 'bulkGet'>;
 }
 
-// Failing: See https://github.com/elastic/kibana/issues/70104
-describe.skip('loader', () => {
+describe('loader', () => {
   describe('loadIndexPatterns', () => {
     it('should not load index patterns that are already loaded', async () => {
       const cache = await loadIndexPatterns({
@@ -318,7 +317,6 @@ describe.skip('loader', () => {
           a: sampleIndexPatterns.a,
         },
         layers: {},
-        showEmptyFields: false,
       });
       expect(storage.set).toHaveBeenCalledWith('lens-settings', {
         indexPatternId: 'a',
@@ -341,7 +339,6 @@ describe.skip('loader', () => {
           b: sampleIndexPatterns.b,
         },
         layers: {},
-        showEmptyFields: false,
       });
     });