diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md
new file mode 100644
index 0000000000000..7d29ced66afa8
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md
@@ -0,0 +1,16 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md)
+
+## IndexPatternsService.getIdsWithTitle property
+
+Get list of index pattern ids with titles
+
+Signature:
+
+```typescript
+getIdsWithTitle: (refresh?: boolean) => Promise>;
+```
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md
index 0022bff34a8e7..af087344268d7 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md
@@ -29,6 +29,7 @@ export declare class IndexPatternsService
| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise<any>
| Get field list by providing an index patttern (or spec) |
| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options?: GetFieldsOptions) => Promise<any>
| Get field list by providing { pattern } |
| [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]>
| Get list of index pattern ids |
+| [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
id: string;
title: string;
}>>
| Get list of index pattern ids with titles |
| [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]>
| Get list of index pattern titles |
| [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void>
| Refresh field list for a given index pattern |
| [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec
| Converts index pattern saved object to index pattern spec |
diff --git a/docs/management/images/add_remote_cluster.png b/docs/management/images/add_remote_cluster.png
deleted file mode 100755
index 160d29b741c62..0000000000000
Binary files a/docs/management/images/add_remote_cluster.png and /dev/null differ
diff --git a/docs/management/images/auto_follow_pattern.png b/docs/management/images/auto_follow_pattern.png
deleted file mode 100755
index f80de9352280f..0000000000000
Binary files a/docs/management/images/auto_follow_pattern.png and /dev/null differ
diff --git a/docs/management/images/cross-cluster-replication-list-view.png b/docs/management/images/cross-cluster-replication-list-view.png
deleted file mode 100755
index 4c45174cff7f1..0000000000000
Binary files a/docs/management/images/cross-cluster-replication-list-view.png and /dev/null differ
diff --git a/docs/management/images/remote-clusters-list-view.png b/docs/management/images/remote-clusters-list-view.png
deleted file mode 100755
index c28379863b74b..0000000000000
Binary files a/docs/management/images/remote-clusters-list-view.png and /dev/null differ
diff --git a/docs/management/managing-ccr.asciidoc b/docs/management/managing-ccr.asciidoc
deleted file mode 100644
index 9c06e479e28b2..0000000000000
--- a/docs/management/managing-ccr.asciidoc
+++ /dev/null
@@ -1,80 +0,0 @@
-[role="xpack"]
-[[managing-cross-cluster-replication]]
-== Cross-Cluster Replication
-
-Use *Cross-Cluster Replication* to reproduce indices in
-remote clusters on a local cluster. {ref}/xpack-ccr.html[Cross-cluster replication]
-is commonly used to provide remote backups for disaster recovery and for
-geo-proximite copies of data.
-
-To get started, open the menu, then go to *Stack Management > Data > Cross-Cluster Replication*.
-
-[role="screenshot"]
-image::images/cross-cluster-replication-list-view.png[][Cross-cluster replication list view]
-
-[float]
-=== Prerequisites
-
-* You must have a {ref}/modules-remote-clusters.html[remote cluster].
-* Leader indices must meet {ref}/ccr-requirements.html[these requirements].
-* The Elasticsearch version of the local cluster must be the same as or newer than the remote cluster.
-Refer to {ref}/ccr-overview.html[this document] for more information.
-
-[float]
-=== Required permissions
-
-The `manage` and `manage_ccr` cluster privileges are required to access *Cross-Cluster Replication*.
-
-You can add these privileges in *Stack Management > Security > Roles*.
-
-[float]
-[[configure-replication]]
-=== Configure replication
-
-Replication requires a leader index, the index being replicated, and a
-follower index, which will contain the leader index's replicated data.
-The follower index is passive in that it can read requests and searches,
-but cannot accept direct writes. Only the leader index is active for direct writes.
-
-You can configure follower indices in two ways:
-
-* Create specific follower indices
-* Create follower indices from an auto-follow pattern
-
-[float]
-==== Create specific follower indices
-
-To replicate data from existing indices, or set up local followers on a case-by-case basis,
-go to *Follower indices*. When you create the follower index, you must reference the
-remote cluster and the leader index that you created in the remote cluster.
-
-[role="screenshot"]
-image::images/follower_indices.png[][UI for adding follower indices]
-
-[float]
-==== Create follower indices from an auto-follow pattern
-
-To automatically detect and follow new indices when they are created on a remote cluster,
-go to *Auto-follow patterns*. Creating an auto-follow pattern is useful when you have
-time series data, like event logs, on the remote cluster that is created or rolled over on a daily basis.
-
-When creating the pattern, you must reference the remote cluster that you
-connected to your local cluster. You must also specify a collection of index patterns
-that match the indices you want to automatically follow.
-
-Once you configure an
-auto-follow pattern, any time a new index with a name that matches the pattern is
-created in the remote cluster, a follower index is automatically configured in the local cluster.
-
-[role="screenshot"]
-image::images/auto_follow_pattern.png[UI for adding an auto-follow pattern]
-
-[float]
-[[manage-replication]]
-=== Manage replication
-
-Use the list views in *Cross-Cluster Replication* to monitor whether the replication is active and
-pause and resume replication. You can also edit and remove the follower indices and auto-follow patterns.
-
-For an example of cross-cluster replication,
-refer to https://www.elastic.co/blog/bi-directional-replication-with-elasticsearch-cross-cluster-replication-ccr[Bi-directional replication with Elasticsearch cross-cluster replication].
diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc
deleted file mode 100644
index 92e0fa822b056..0000000000000
--- a/docs/management/managing-remote-clusters.asciidoc
+++ /dev/null
@@ -1,50 +0,0 @@
-[[working-remote-clusters]]
-== Remote Clusters
-
-Use *Remote Clusters* to establish a unidirectional
-connection from your cluster to other clusters. This functionality is
-required for {ref}/xpack-ccr.html[cross-cluster replication] and
-{ref}/modules-cross-cluster-search.html[cross-cluster search].
-
-To get started, open the menu, then go to *Stack Management > Data > Remote Clusters*.
-
-[role="screenshot"]
-image::images/remote-clusters-list-view.png[Remote Clusters list view, including Add a remote cluster button]
-
-[float]
-=== Required permissions
-
-The `manage` cluster privilege is required to access *Remote Clusters*.
-
-You can add this privilege in *Stack Management > Security > Roles*.
-
-[float]
-[[managing-remote-clusters]]
-=== Add a remote cluster
-
-A {ref}/modules-remote-clusters.html[remote cluster] connection works by configuring a remote cluster and
-connecting to a limited number of nodes, called {ref}/modules-remote-clusters.html#sniff-mode[seed nodes],
-in that cluster.
-Alternatively, you can define a single proxy address for the remote cluster.
-
-By default, a cross-cluster request, such as a cross-cluster search or
-replication request, fails if any cluster in the request is unavailable.
-To skip a cluster when its unavailable,
-set *Skip if unavailable* to true.
-
-Once you add a remote cluster, you can configure <>
-to reproduce indices in the remote cluster on a local cluster.
-
-[role="screenshot"]
-image::images/add_remote_cluster.png[][UI for adding a remote cluster]
-
-To create an index pattern to search across clusters,
-use the same syntax that you’d use in a raw cross-cluster search request in {es}: :.
-See <> for examples.
-
-[float]
-[[manage-remote-clusters]]
-=== Manage remote clusters
-
-From the *Remote Clusters* list view, you can drill down into each cluster and
-view its status. You can also edit and delete a cluster.
diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc
index 42d1d89145d79..5067bc08bec99 100644
--- a/docs/redirects.asciidoc
+++ b/docs/redirects.asciidoc
@@ -100,3 +100,15 @@ This content has moved to the <> page.
== TSVB
This page was deleted. See <>.
+
+[role="exclude",id="managing-cross-cluster-replication"]
+== Cross-Cluster Replication
+
+This content has moved. See
+{ref}/ccr-getting-started.html[Set up cross-cluster replication].
+
+[role="exclude",id="working-remote-clusters"]
+== Remote clusters
+
+This content has moved. See
+{ref}/ccr-getting-started.html#ccr-getting-started-remote-cluster[Connect to a remote cluster].
diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc
index bc96463f6efba..e0d550a15a907 100644
--- a/docs/user/management.asciidoc
+++ b/docs/user/management.asciidoc
@@ -58,12 +58,12 @@ years of historical data in combination with your raw data.
| {ref}/transforms.html[Transforms]
|Use transforms to pivot existing {es} indices into summarized or entity-centric indices.
-| <>
+| {ref}/ccr-getting-started.html[Cross-Cluster Replication]
|Replicate indices on a remote cluster and copy them to a follower index on a local cluster.
This is important for
disaster recovery. It also keeps data local for faster queries.
-| <>
+| {ref}/ccr-getting-started.html#ccr-getting-started-remote-cluster[Remote Clusters]
|Manage your remote clusters for use with cross-cluster search and cross-cluster replication.
You can add and remove remote clusters, and check their connectivity.
|===
@@ -180,8 +180,6 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[]
include::{kib-repo-dir}/management/managing-beats.asciidoc[]
-include::{kib-repo-dir}/management/managing-ccr.asciidoc[]
-
include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[]
include::{kib-repo-dir}/management/index-lifecycle-policies/create-policy.asciidoc[]
@@ -200,8 +198,6 @@ include::{kib-repo-dir}/management/managing-licenses.asciidoc[]
include::{kib-repo-dir}/management/numeral.asciidoc[]
-include::{kib-repo-dir}/management/managing-remote-clusters.asciidoc[]
-
include::{kib-repo-dir}/management/rollups/create_and_manage_rollups.asciidoc[]
include::{kib-repo-dir}/management/managing-saved-objects.asciidoc[]
diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
index c56954ba6a29b..eef8ef10ea754 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts
@@ -133,6 +133,25 @@ export class IndexPatternsService {
return this.savedObjectsCache.map((obj) => obj?.attributes?.title);
};
+ /**
+ * Get list of index pattern ids with titles
+ * @param refresh Force refresh of index pattern list
+ */
+ getIdsWithTitle = async (
+ refresh: boolean = false
+ ): Promise> => {
+ if (!this.savedObjectsCache || refresh) {
+ await this.refreshSavedObjectsCache();
+ }
+ if (!this.savedObjectsCache) {
+ return [];
+ }
+ return this.savedObjectsCache.map((obj) => ({
+ id: obj?.id,
+ title: obj?.attributes?.title,
+ }));
+ };
+
/**
* Clear index pattern list cache
* @param id optionally clear a single id
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 5919c1e294b2f..ed58ee840a8f8 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -1381,6 +1381,10 @@ export class IndexPatternsService {
// Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts
getFieldsForWildcard: (options?: GetFieldsOptions) => Promise;
getIds: (refresh?: boolean) => Promise;
+ getIdsWithTitle: (refresh?: boolean) => Promise>;
getTitles: (refresh?: boolean) => Promise;
refreshFields: (indexPattern: IndexPattern) => Promise;
savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec;
diff --git a/test/functional/apps/discover/_inspector.js b/test/functional/apps/discover/_inspector.js
index 900ad28e14e69..fcb66fbd52cf7 100644
--- a/test/functional/apps/discover/_inspector.js
+++ b/test/functional/apps/discover/_inspector.js
@@ -34,7 +34,8 @@ export default function ({ getService, getPageObjects }) {
return hitsCountStatsRow[STATS_ROW_VALUE_INDEX];
}
- describe('inspect', () => {
+ // FLAKY: https://github.com/elastic/kibana/issues/39842
+ describe.skip('inspect', () => {
before(async () => {
await esArchiver.loadIfNeeded('logstash_functional');
await esArchiver.load('discover');
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts
index f66235ff44c6a..88a900f69c5ec 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts
@@ -6,7 +6,7 @@
export { mockHistory, mockLocation } from './react_router_history.mock';
export { mockKibanaContext } from './kibana_context.mock';
-export { mockLicenseContext } from './license_context.mock';
+export { mockLicensingValues } from './licensing_logic.mock';
export { mockHttpValues } from './http_logic.mock';
export { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock';
export { mockAllValues, mockAllActions, setMockValues } from './kea.mock';
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts
index 8e6b0baa5fc00..bad6beaa1652e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts
@@ -5,13 +5,17 @@
*/
/**
+ * Combine all shared mock values/actions into a single obj
+ *
* NOTE: These variable names MUST start with 'mock*' in order for
* Jest to accept its use within a jest.mock()
*/
+import { mockLicensingValues } from './licensing_logic.mock';
import { mockHttpValues } from './http_logic.mock';
import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock';
export const mockAllValues = {
+ ...mockLicensingValues,
...mockHttpValues,
...mockFlashMessagesValues,
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/licensing_logic.mock.ts
similarity index 79%
rename from x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts
rename to x-pack/plugins/enterprise_search/public/applications/__mocks__/licensing_logic.mock.ts
index 7c37ecc7cde1b..51b32e7a877b2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/licensing_logic.mock.ts
@@ -6,6 +6,8 @@
import { licensingMock } from '../../../../licensing/public/mocks';
-export const mockLicenseContext = {
+export const mockLicensingValues = {
license: licensingMock.createLicense(),
+ hasPlatinumLicense: false,
+ hasGoldLicense: false,
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx
index 5e56f17c8e7f3..646c3104c286f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx
@@ -15,8 +15,6 @@ import { getContext, resetContext } from 'kea';
import { I18nProvider } from '@kbn/i18n/react';
import { KibanaContext } from '../';
import { mockKibanaContext } from './kibana_context.mock';
-import { LicenseContext } from '../shared/licensing';
-import { mockLicenseContext } from './license_context.mock';
/**
* This helper mounts a component with all the contexts/providers used
@@ -34,9 +32,7 @@ export const mountWithContext = (children: React.ReactNode, context?: object) =>
return mount(
-
- {children}
-
+ {children}
);
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts
index 3a2193db646de..df9e58994e36b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts
@@ -9,11 +9,10 @@
* Jest to accept its use within a jest.mock()
*/
import { mockKibanaContext } from './kibana_context.mock';
-import { mockLicenseContext } from './license_context.mock';
jest.mock('react', () => ({
...(jest.requireActual('react') as object),
- useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })),
+ useContext: jest.fn(() => ({ ...mockKibanaContext })),
useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior
}));
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
index 928d92d791094..44afce96c1a6c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
@@ -82,9 +82,11 @@ describe('EngineOverview', () => {
describe('when on a platinum license', () => {
it('renders a 2nd meta engines table & makes a 2nd meta engines API call', async () => {
- const wrapper = await mountWithAsyncContext(, {
- license: { type: 'platinum', isActive: true },
+ setMockValues({
+ hasPlatinumLicense: true,
+ http: { ...mockHttpValues.http, get: mockApi },
});
+ const wrapper = await mountWithAsyncContext();
expect(wrapper.find(EngineTable)).toHaveLength(2);
expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
index c0aedbe7dc6b4..0cb9ba106dbb8 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext, useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
import { useValues } from 'kea';
import {
EuiPageContent,
@@ -19,7 +19,7 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chro
import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
import { FlashMessages } from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
-import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing';
+import { LicensingLogic } from '../../../shared/licensing';
import { EngineIcon } from './assets/engine_icon';
import { MetaEngineIcon } from './assets/meta_engine_icon';
@@ -40,7 +40,7 @@ interface ISetEnginesCallbacks {
export const EngineOverview: React.FC = () => {
const { http } = useValues(HttpLogic);
- const { license } = useContext(LicenseContext) as ILicenseContext;
+ const { hasPlatinumLicense } = useValues(LicensingLogic);
const [isLoading, setIsLoading] = useState(true);
const [engines, setEngines] = useState([]);
@@ -72,13 +72,13 @@ export const EngineOverview: React.FC = () => {
}, [enginesPage]);
useEffect(() => {
- if (hasPlatinumLicense(license)) {
+ if (hasPlatinumLicense) {
const params = { type: 'meta', pageIndex: metaEnginesPage };
const callbacks = { setResults: setMetaEngines, setResultsTotal: setMetaEnginesTotal };
setEnginesData(params, callbacks);
}
- }, [license, metaEnginesPage]);
+ }, [hasPlatinumLicense, metaEnginesPage]);
if (isLoading) return ;
if (!engines.length) return ;
diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx
index 053c450ab925e..6ee63ee22cae2 100644
--- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx
@@ -6,7 +6,6 @@
import React from 'react';
-import { AppMountParameters } from 'src/core/public';
import { coreMock } from 'src/core/public/mocks';
import { licensingMock } from '../../../licensing/public/mocks';
@@ -15,37 +14,38 @@ import { AppSearch } from './app_search';
import { WorkplaceSearch } from './workplace_search';
describe('renderApp', () => {
- let params: AppMountParameters;
- const core = coreMock.createStart();
- const plugins = {
- licensing: licensingMock.createSetup(),
+ const kibanaDeps = {
+ params: coreMock.createAppMountParamters(),
+ core: coreMock.createStart(),
+ plugins: { licensing: licensingMock.createStart() },
+ } as any;
+ const pluginData = {
+ config: {},
+ data: {},
} as any;
- const config = {};
- const data = {} as any;
beforeEach(() => {
jest.clearAllMocks();
- params = coreMock.createAppMountParamters();
});
it('mounts and unmounts UI', () => {
const MockApp = () => Hello world!
;
- const unmount = renderApp(MockApp, params, core, plugins, config, data);
- expect(params.element.querySelector('.hello-world')).not.toBeNull();
+ const unmount = renderApp(MockApp, kibanaDeps, pluginData);
+ expect(kibanaDeps.params.element.querySelector('.hello-world')).not.toBeNull();
unmount();
- expect(params.element.innerHTML).toEqual('');
+ expect(kibanaDeps.params.element.innerHTML).toEqual('');
});
it('renders AppSearch', () => {
- renderApp(AppSearch, params, core, plugins, config, data);
- expect(params.element.querySelector('.setupGuide')).not.toBeNull();
+ renderApp(AppSearch, kibanaDeps, pluginData);
+ expect(kibanaDeps.params.element.querySelector('.setupGuide')).not.toBeNull();
});
it('renders WorkplaceSearch', () => {
- renderApp(WorkplaceSearch, params, core, plugins, config, data);
- expect(params.element.querySelector('.setupGuide')).not.toBeNull();
+ renderApp(WorkplaceSearch, kibanaDeps, pluginData);
+ expect(kibanaDeps.params.element.querySelector('.setupGuide')).not.toBeNull();
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx
index 0869ef7b22729..4a25ecf6067cc 100644
--- a/x-pack/plugins/enterprise_search/public/applications/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx
@@ -14,8 +14,8 @@ import { getContext, resetContext } from 'kea';
import { I18nProvider } from '@kbn/i18n/react';
import { AppMountParameters, CoreStart, ApplicationStart, ChromeBreadcrumb } from 'src/core/public';
-import { ClientConfigType, ClientData, PluginsSetup } from '../plugin';
-import { LicenseProvider } from './shared/licensing';
+import { PluginsStart, ClientConfigType, ClientData } from '../plugin';
+import { mountLicensingLogic } from './shared/licensing';
import { mountHttpLogic } from './shared/http';
import { mountFlashMessagesLogic } from './shared/flash_messages';
import { IExternalUrl } from './shared/enterprise_search_url';
@@ -39,15 +39,18 @@ export const KibanaContext = React.createContext({});
export const renderApp = (
App: React.FC,
- params: AppMountParameters,
- core: CoreStart,
- plugins: PluginsSetup,
- config: ClientConfigType,
- { externalUrl, errorConnecting, ...initialData }: ClientData
+ { params, core, plugins }: { params: AppMountParameters; core: CoreStart; plugins: PluginsStart },
+ { config, data }: { config: ClientConfigType; data: ClientData }
) => {
+ const { externalUrl, errorConnecting, ...initialData } = data;
+
resetContext({ createStore: true });
const store = getContext().store as Store;
+ const unmountLicensingLogic = mountLicensingLogic({
+ license$: plugins.licensing.license$,
+ });
+
const unmountHttpLogic = mountHttpLogic({
http: core.http,
errorConnecting,
@@ -67,19 +70,18 @@ export const renderApp = (
setDocTitle: core.chrome.docTitle.change,
}}
>
-
-
-
-
-
-
-
+
+
+
+
+
,
params.element
);
return () => {
ReactDOM.unmountComponentAtNode(params.element);
+ unmountLicensingLogic();
unmountHttpLogic();
unmountFlashMessagesLogic();
};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts
index 29c11ffa1cef8..4e371b337c40a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts
@@ -4,5 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { LicenseContext, LicenseProvider, ILicenseContext } from './license_context';
-export { hasPlatinumLicense, hasGoldLicense } from './license_checks';
+export { LicensingLogic, mountLicensingLogic } from './licensing_logic';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts
deleted file mode 100644
index 40f0f6380c21c..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { hasPlatinumLicense, hasGoldLicense } from './license_checks';
-
-describe('hasPlatinumLicense', () => {
- it('is true for platinum licenses', () => {
- expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true);
- });
-
- it('is true for enterprise licenses', () => {
- expect(hasPlatinumLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true);
- });
-
- it('is true for trial licenses', () => {
- expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true);
- });
-
- it('is false if the current license is expired', () => {
- expect(hasPlatinumLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false);
- expect(hasPlatinumLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false);
- expect(hasPlatinumLicense({ isActive: false, type: 'trial' } as any)).toEqual(false);
- });
-
- it('is false for licenses below platinum', () => {
- expect(hasPlatinumLicense({ isActive: true, type: 'basic' } as any)).toEqual(false);
- expect(hasPlatinumLicense({ isActive: false, type: 'standard' } as any)).toEqual(false);
- expect(hasPlatinumLicense({ isActive: true, type: 'gold' } as any)).toEqual(false);
- });
-});
-
-describe('hasGoldLicense', () => {
- it('is true for gold+ and trial licenses', () => {
- expect(hasGoldLicense({ isActive: true, type: 'gold' } as any)).toEqual(true);
- expect(hasGoldLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true);
- expect(hasGoldLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true);
- expect(hasGoldLicense({ isActive: true, type: 'trial' } as any)).toEqual(true);
- });
-
- it('is false if the current license is expired', () => {
- expect(hasGoldLicense({ isActive: false, type: 'gold' } as any)).toEqual(false);
- expect(hasGoldLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false);
- expect(hasGoldLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false);
- expect(hasGoldLicense({ isActive: false, type: 'trial' } as any)).toEqual(false);
- });
-
- it('is false for licenses below gold', () => {
- expect(hasGoldLicense({ isActive: true, type: 'basic' } as any)).toEqual(false);
- expect(hasGoldLicense({ isActive: false, type: 'standard' } as any)).toEqual(false);
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts
deleted file mode 100644
index d13d0909243be..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { ILicense } from '../../../../../licensing/public';
-
-export const hasPlatinumLicense = (license?: ILicense) => {
- const qualifyingLicenses = ['platinum', 'enterprise', 'trial'];
- return license?.isActive && qualifyingLicenses.includes(license?.type as string);
-};
-
-export const hasGoldLicense = (license?: ILicense) => {
- const qualifyingLicenses = ['gold', 'platinum', 'enterprise', 'trial'];
- return license?.isActive && qualifyingLicenses.includes(license?.type as string);
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx
deleted file mode 100644
index c65474ec1f590..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx
+++ /dev/null
@@ -1,24 +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, { useContext } from 'react';
-
-import { mountWithContext } from '../../__mocks__';
-import { LicenseContext, ILicenseContext } from './';
-
-describe('LicenseProvider', () => {
- const MockComponent: React.FC = () => {
- const { license } = useContext(LicenseContext) as ILicenseContext;
- return {license?.type}
;
- };
-
- it('renders children', () => {
- const wrapper = mountWithContext(, { license: { type: 'basic' } });
-
- expect(wrapper.find('.license-test')).toHaveLength(1);
- expect(wrapper.text()).toEqual('basic');
- });
-});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx
deleted file mode 100644
index 9b47959ff7544..0000000000000
--- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx
+++ /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 useObservable from 'react-use/lib/useObservable';
-import { Observable } from 'rxjs';
-
-import { ILicense } from '../../../../../licensing/public';
-
-export interface ILicenseContext {
- license: ILicense;
-}
-interface ILicenseContextProps {
- license$: Observable;
- children: React.ReactNode;
-}
-
-export const LicenseContext = React.createContext({});
-
-export const LicenseProvider: React.FC = ({ license$, children }) => {
- // Listen for changes to license subscription
- const license = useObservable(license$);
-
- // Render rest of application and pass down license via context
- return ;
-};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts
new file mode 100644
index 0000000000000..153a5ae765468
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts
@@ -0,0 +1,161 @@
+/*
+ * 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 { resetContext } from 'kea';
+import { BehaviorSubject } from 'rxjs';
+
+import { licensingMock } from '../../../../../licensing/public/mocks';
+
+import { LicensingLogic, mountLicensingLogic } from './licensing_logic';
+
+describe('LicensingLogic', () => {
+ const mockLicense = licensingMock.createLicense();
+ const mockLicense$ = new BehaviorSubject(mockLicense);
+ const mount = () => mountLicensingLogic({ license$: mockLicense$ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ resetContext({});
+ });
+
+ describe('setLicense()', () => {
+ it('sets license value', () => {
+ mount();
+ LicensingLogic.actions.setLicense('test' as any);
+ expect(LicensingLogic.values.license).toEqual('test');
+ });
+ });
+
+ describe('setLicenseSubscription()', () => {
+ it('sets licenseSubscription value', () => {
+ mount();
+ LicensingLogic.actions.setLicenseSubscription('test' as any);
+ expect(LicensingLogic.values.licenseSubscription).toEqual('test');
+ });
+ });
+
+ describe('licensing subscription', () => {
+ describe('on mount', () => {
+ it('subscribes to the license observable', () => {
+ mount();
+ expect(LicensingLogic.values.license).toEqual(mockLicense);
+ expect(LicensingLogic.values.licenseSubscription).not.toBeNull();
+ });
+ });
+
+ describe('on subscription update', () => {
+ it('updates the license value', () => {
+ mount();
+
+ const nextMockLicense = licensingMock.createLicense({ license: { status: 'invalid' } });
+ mockLicense$.next(nextMockLicense);
+
+ expect(LicensingLogic.values.license).toEqual(nextMockLicense);
+ });
+ });
+
+ describe('on unmount', () => {
+ it('unsubscribes to the license observable', () => {
+ const mockUnsubscribe = jest.fn();
+ const unmount = mountLicensingLogic({
+ license$: { subscribe: () => ({ unsubscribe: mockUnsubscribe }) } as any,
+ });
+ unmount();
+ expect(mockUnsubscribe).toHaveBeenCalled();
+ });
+
+ it('does not crash if no subscription exists', () => {
+ const unmount = mount();
+ LicensingLogic.actions.setLicenseSubscription(null as any);
+ unmount();
+ });
+ });
+ });
+
+ describe('license check selectors', () => {
+ beforeEach(() => {
+ mount();
+ });
+
+ const updateLicense = (license: any) => {
+ const updatedLicense = licensingMock.createLicense({ license });
+ mockLicense$.next(updatedLicense);
+ };
+
+ describe('hasPlatinumLicense', () => {
+ it('is true for platinum+ and trial licenses', () => {
+ updateLicense({ status: 'active', type: 'platinum' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(true);
+
+ updateLicense({ status: 'active', type: 'enterprise' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(true);
+
+ updateLicense({ status: 'active', type: 'trial' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(true);
+ });
+
+ it('is false if the current license is expired', () => {
+ updateLicense({ status: 'expired', type: 'platinum' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false);
+
+ updateLicense({ status: 'expired', type: 'enterprise' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false);
+
+ updateLicense({ status: 'expired', type: 'trial' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false);
+ });
+
+ it('is false for licenses below platinum', () => {
+ updateLicense({ status: 'active', type: 'basic' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false);
+
+ updateLicense({ status: 'active', type: 'standard' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false);
+
+ updateLicense({ status: 'active', type: 'gold' });
+ expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false);
+ });
+ });
+
+ describe('hasGoldLicense', () => {
+ it('is true for gold+ and trial licenses', () => {
+ updateLicense({ status: 'active', type: 'gold' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(true);
+
+ updateLicense({ status: 'active', type: 'platinum' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(true);
+
+ updateLicense({ status: 'active', type: 'enterprise' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(true);
+
+ updateLicense({ status: 'active', type: 'trial' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(true);
+ });
+
+ it('is false if the current license is expired', () => {
+ updateLicense({ status: 'expired', type: 'gold' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(false);
+
+ updateLicense({ status: 'expired', type: 'platinum' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(false);
+
+ updateLicense({ status: 'expired', type: 'enterprise' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(false);
+
+ updateLicense({ status: 'expired', type: 'trial' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(false);
+ });
+
+ it('is false for licenses below gold', () => {
+ updateLicense({ status: 'active', type: 'basic' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(false);
+
+ updateLicense({ status: 'active', type: 'standard' });
+ expect(LicensingLogic.values.hasGoldLicense).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts
new file mode 100644
index 0000000000000..ae31b2ec6168a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { kea, MakeLogicType } from 'kea';
+import { Observable, Subscription } from 'rxjs';
+
+import { ILicense } from '../../../../../licensing/public';
+
+export interface ILicensingValues {
+ license: ILicense | null;
+ licenseSubscription: Subscription | null;
+ hasPlatinumLicense: boolean;
+ hasGoldLicense: boolean;
+}
+export interface ILicensingActions {
+ setLicense(license: ILicense): ILicense;
+ setLicenseSubscription(licenseSubscription: Subscription): Subscription;
+}
+
+export const LicensingLogic = kea>({
+ path: ['enterprise_search', 'licensing_logic'],
+ actions: {
+ setLicense: (license) => license,
+ setLicenseSubscription: (licenseSubscription) => licenseSubscription,
+ },
+ reducers: {
+ license: [
+ null,
+ {
+ setLicense: (_, license) => license,
+ },
+ ],
+ licenseSubscription: [
+ null,
+ {
+ setLicenseSubscription: (_, licenseSubscription) => licenseSubscription,
+ },
+ ],
+ },
+ selectors: {
+ hasPlatinumLicense: [
+ (selectors) => [selectors.license],
+ (license) => {
+ const qualifyingLicenses = ['platinum', 'enterprise', 'trial'];
+ return license?.isActive && qualifyingLicenses.includes(license?.type);
+ },
+ ],
+ hasGoldLicense: [
+ (selectors) => [selectors.license],
+ (license) => {
+ const qualifyingLicenses = ['gold', 'platinum', 'enterprise', 'trial'];
+ return license?.isActive && qualifyingLicenses.includes(license?.type);
+ },
+ ],
+ },
+ events: ({ props, actions, values }) => ({
+ afterMount: () => {
+ const licenseSubscription = props.license$.subscribe(async (license: ILicense) => {
+ actions.setLicense(license);
+ });
+ actions.setLicenseSubscription(licenseSubscription);
+ },
+ beforeUnmount: () => {
+ if (values.licenseSubscription) values.licenseSubscription.unsubscribe();
+ },
+ }),
+});
+
+/**
+ * Mount/props helper
+ */
+interface ILicensingLogicProps {
+ license$: Observable;
+}
+export const mountLicensingLogic = (props: ILicensingLogicProps) => {
+ LicensingLogic(props);
+ const unmount = LicensingLogic.mount();
+ return unmount;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx
index ce9071ad7b9d0..62c0af31cffd9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx
@@ -4,9 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import '../../__mocks__/shallow_usecontext.mock';
+import '../../__mocks__/kea.mock';
-import React, { useContext } from 'react';
+import React from 'react';
+import { useValues } from 'kea';
import { shallow } from 'enzyme';
import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui';
@@ -18,13 +19,6 @@ import { WorkplaceSearchLogo } from './assets/workplace_search_logo';
import { NotFound } from './';
describe('NotFound', () => {
- const basicLicense = { isActive: true, type: 'basic' };
- const goldLicense = { isActive: true, type: 'gold' };
-
- beforeEach(() => {
- (useContext as jest.Mock).mockImplementation(() => ({ license: basicLicense }));
- });
-
it('renders an App Search 404 view', () => {
const wrapper = shallow();
const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow();
@@ -50,7 +44,7 @@ describe('NotFound', () => {
});
it('changes the support URL if the user has a gold+ license', () => {
- (useContext as jest.Mock).mockImplementation(() => ({ license: goldLicense }));
+ (useValues as jest.Mock).mockReturnValueOnce({ hasGoldLicense: true });
const wrapper = shallow();
const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow();
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx
index bd988854225fb..40bb5efcc6330 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx
@@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useContext } from 'react';
+import React from 'react';
+import { useValues } from 'kea';
import { i18n } from '@kbn/i18n';
import {
EuiPageContent,
@@ -24,7 +25,7 @@ import {
import { EuiButton } from '../react_router_helpers';
import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome';
import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry';
-import { LicenseContext, ILicenseContext, hasGoldLicense } from '../licensing';
+import { LicensingLogic } from '../licensing';
import { AppSearchLogo } from './assets/app_search_logo';
import { WorkplaceSearchLogo } from './assets/workplace_search_logo';
@@ -39,8 +40,8 @@ interface NotFoundProps {
}
export const NotFound: React.FC = ({ product = {} }) => {
- const { license } = useContext(LicenseContext) as ILicenseContext;
- const supportUrl = hasGoldLicense(license) ? LICENSED_SUPPORT_URL : product.SUPPORT_URL;
+ const { hasGoldLicense } = useValues(LicensingLogic);
+ const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : product.SUPPORT_URL;
let Logo;
let SetPageChrome;
diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts
index c23bb23be3979..f59ec830c812f 100644
--- a/x-pack/plugins/enterprise_search/public/plugin.ts
+++ b/x-pack/plugins/enterprise_search/public/plugin.ts
@@ -7,7 +7,6 @@
import {
AppMountParameters,
CoreSetup,
- CoreStart,
HttpSetup,
Plugin,
PluginInitializerContext,
@@ -17,7 +16,7 @@ import {
FeatureCatalogueCategory,
HomePublicPluginSetup,
} from '../../../../src/plugins/home/public';
-import { LicensingPluginSetup } from '../../licensing/public';
+import { LicensingPluginStart } from '../../licensing/public';
import {
APP_SEARCH_PLUGIN,
ENTERPRISE_SEARCH_PLUGIN,
@@ -36,7 +35,9 @@ export interface ClientData extends IInitialAppData {
export interface PluginsSetup {
home?: HomePublicPluginSetup;
- licensing: LicensingPluginSetup;
+}
+export interface PluginsStart {
+ licensing: LicensingPluginStart;
}
export class EnterpriseSearchPlugin implements Plugin {
@@ -57,16 +58,17 @@ export class EnterpriseSearchPlugin implements Plugin {
appRoute: ENTERPRISE_SEARCH_PLUGIN.URL,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
mount: async (params: AppMountParameters) => {
- const [coreStart] = await core.getStartServices();
- const { chrome } = coreStart;
- chrome.docTitle.change(ENTERPRISE_SEARCH_PLUGIN.NAME);
+ const kibanaDeps = await this.getKibanaDeps(core, params);
+ const pluginData = this.getPluginData();
- await this.getInitialData(coreStart.http);
+ const { chrome, http } = kibanaDeps.core;
+ chrome.docTitle.change(ENTERPRISE_SEARCH_PLUGIN.NAME);
+ await this.getInitialData(http);
const { renderApp } = await import('./applications');
const { EnterpriseSearch } = await import('./applications/enterprise_search');
- return renderApp(EnterpriseSearch, params, coreStart, plugins, this.config, this.data);
+ return renderApp(EnterpriseSearch, kibanaDeps, pluginData);
},
});
@@ -77,16 +79,17 @@ export class EnterpriseSearchPlugin implements Plugin {
appRoute: APP_SEARCH_PLUGIN.URL,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
mount: async (params: AppMountParameters) => {
- const [coreStart] = await core.getStartServices();
- const { chrome } = coreStart;
- chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME);
+ const kibanaDeps = await this.getKibanaDeps(core, params);
+ const pluginData = this.getPluginData();
- await this.getInitialData(coreStart.http);
+ const { chrome, http } = kibanaDeps.core;
+ chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME);
+ await this.getInitialData(http);
const { renderApp } = await import('./applications');
const { AppSearch } = await import('./applications/app_search');
- return renderApp(AppSearch, params, coreStart, plugins, this.config, this.data);
+ return renderApp(AppSearch, kibanaDeps, pluginData);
},
});
@@ -97,11 +100,12 @@ export class EnterpriseSearchPlugin implements Plugin {
appRoute: WORKPLACE_SEARCH_PLUGIN.URL,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
mount: async (params: AppMountParameters) => {
- const [coreStart] = await core.getStartServices();
- const { chrome } = coreStart;
- chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME);
+ const kibanaDeps = await this.getKibanaDeps(core, params);
+ const pluginData = this.getPluginData();
- await this.getInitialData(coreStart.http);
+ const { chrome, http } = kibanaDeps.core;
+ chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME);
+ await this.getInitialData(http);
const { renderApp, renderHeaderActions } = await import('./applications');
const { WorkplaceSearch } = await import('./applications/workplace_search');
@@ -113,7 +117,7 @@ export class EnterpriseSearchPlugin implements Plugin {
renderHeaderActions(WorkplaceSearchHeaderActions, element, this.data.externalUrl)
);
- return renderApp(WorkplaceSearch, params, coreStart, plugins, this.config, this.data);
+ return renderApp(WorkplaceSearch, kibanaDeps, pluginData);
},
});
@@ -149,10 +153,22 @@ export class EnterpriseSearchPlugin implements Plugin {
}
}
- public start(core: CoreStart) {}
+ public start() {}
public stop() {}
+ private async getKibanaDeps(core: CoreSetup, params: AppMountParameters) {
+ // Helper for using start dependencies on mount (instead of setup dependencies)
+ // and for grouping Kibana-related args together (vs. plugin-specific args)
+ const [coreStart, pluginsStart] = await core.getStartServices();
+ return { params, core: coreStart, plugins: pluginsStart as PluginsStart };
+ }
+
+ private getPluginData() {
+ // Small helper for grouping plugin data related args together
+ return { config: this.config, data: this.data };
+ }
+
private async getInitialData(http: HttpSetup) {
if (!this.config.host) return; // No API to call
if (this.hasInitialized) return; // We've already made an initial call
diff --git a/x-pack/plugins/security_solution/server/graphql/overview/index.ts b/x-pack/plugins/infra/common/http_api/infra_ml/index.ts
similarity index 70%
rename from x-pack/plugins/security_solution/server/graphql/overview/index.ts
rename to x-pack/plugins/infra/common/http_api/infra_ml/index.ts
index 58cf182ccd976..38684cb22e237 100644
--- a/x-pack/plugins/security_solution/server/graphql/overview/index.ts
+++ b/x-pack/plugins/infra/common/http_api/infra_ml/index.ts
@@ -4,5 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { createOverviewResolvers } from './resolvers';
-export { overviewSchema } from './schema.gql';
+export * from './results';
diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/common.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/common.ts
new file mode 100644
index 0000000000000..0474fbd1cfc2f
--- /dev/null
+++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/common.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 rt from 'io-ts';
+
+// [Sort field value, tiebreaker value]
+export const paginationCursorRT = rt.tuple([
+ rt.union([rt.string, rt.number]),
+ rt.union([rt.string, rt.number]),
+]);
+
+export type PaginationCursor = rt.TypeOf;
+
+export const anomalyTypeRT = rt.keyof({
+ metrics_hosts: null,
+ metrics_k8s: null,
+});
+
+export type AnomalyType = rt.TypeOf;
+
+const sortOptionsRT = rt.keyof({
+ anomalyScore: null,
+ dataset: null,
+ startTime: null,
+});
+
+const sortDirectionsRT = rt.keyof({
+ asc: null,
+ desc: null,
+});
+
+const paginationPreviousPageCursorRT = rt.type({
+ searchBefore: paginationCursorRT,
+});
+
+const paginationNextPageCursorRT = rt.type({
+ searchAfter: paginationCursorRT,
+});
+
+export const paginationRT = rt.intersection([
+ rt.type({
+ pageSize: rt.number,
+ }),
+ rt.partial({
+ cursor: rt.union([paginationPreviousPageCursorRT, paginationNextPageCursorRT]),
+ }),
+]);
+
+export type Pagination = rt.TypeOf;
+
+export const sortRT = rt.type({
+ field: sortOptionsRT,
+ direction: sortDirectionsRT,
+});
+
+export type Sort = rt.TypeOf;
diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/index.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/index.ts
new file mode 100644
index 0000000000000..efd597a043e07
--- /dev/null
+++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './metrics_hosts_anomalies';
+export * from './metrics_k8s_anomalies';
+export * from './common';
diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts
new file mode 100644
index 0000000000000..9fdac09fec20e
--- /dev/null
+++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 rt from 'io-ts';
+
+import { timeRangeRT, routeTimingMetadataRT } from '../../shared';
+import { anomalyTypeRT, paginationCursorRT, sortRT, paginationRT } from './common';
+
+export const INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH =
+ '/api/infra/infra_ml/results/metrics_hosts_anomalies';
+
+const metricsHostAnomalyCommonFieldsRT = rt.type({
+ id: rt.string,
+ anomalyScore: rt.number,
+ typical: rt.number,
+ actual: rt.number,
+ type: anomalyTypeRT,
+ duration: rt.number,
+ startTime: rt.number,
+ jobId: rt.string,
+});
+const metricsHostsAnomalyRT = metricsHostAnomalyCommonFieldsRT;
+
+export type MetricsHostsAnomaly = rt.TypeOf;
+
+export const getMetricsHostsAnomaliesSuccessReponsePayloadRT = rt.intersection([
+ rt.type({
+ data: rt.intersection([
+ rt.type({
+ anomalies: rt.array(metricsHostsAnomalyRT),
+ // Signifies there are more entries backwards or forwards. If this was a request
+ // for a previous page, there are more previous pages, if this was a request for a next page,
+ // there are more next pages.
+ hasMoreEntries: rt.boolean,
+ }),
+ rt.partial({
+ paginationCursors: rt.type({
+ // The cursor to use to fetch the previous page
+ previousPageCursor: paginationCursorRT,
+ // The cursor to use to fetch the next page
+ nextPageCursor: paginationCursorRT,
+ }),
+ }),
+ ]),
+ }),
+ rt.partial({
+ timing: routeTimingMetadataRT,
+ }),
+]);
+
+export type GetMetricsHostsAnomaliesSuccessResponsePayload = rt.TypeOf<
+ typeof getMetricsHostsAnomaliesSuccessReponsePayloadRT
+>;
+
+export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({
+ data: rt.intersection([
+ rt.type({
+ // the ID of the source configuration
+ sourceId: rt.string,
+ // the time range to fetch the log entry anomalies from
+ timeRange: timeRangeRT,
+ }),
+ rt.partial({
+ // Pagination properties
+ pagination: paginationRT,
+ // Sort properties
+ sort: sortRT,
+ // // Dataset filters
+ // datasets: rt.array(rt.string),
+ }),
+ ]),
+});
+
+export type GetMetricsHostsAnomaliesRequestPayload = rt.TypeOf<
+ typeof getMetricsHostsAnomaliesRequestPayloadRT
+>;
diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts
new file mode 100644
index 0000000000000..ab1f245a74c0c
--- /dev/null
+++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts
@@ -0,0 +1,79 @@
+/*
+ * 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 rt from 'io-ts';
+
+import { timeRangeRT, routeTimingMetadataRT } from '../../shared';
+import { paginationCursorRT, anomalyTypeRT, sortRT, paginationRT } from './common';
+
+export const INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH =
+ '/api/infra/infra_ml/results/metrics_k8s_anomalies';
+
+const metricsK8sAnomalyCommonFieldsRT = rt.type({
+ id: rt.string,
+ anomalyScore: rt.number,
+ typical: rt.number,
+ actual: rt.number,
+ type: anomalyTypeRT,
+ duration: rt.number,
+ startTime: rt.number,
+ jobId: rt.string,
+});
+const metricsK8sAnomalyRT = metricsK8sAnomalyCommonFieldsRT;
+
+export type MetricsK8sAnomaly = rt.TypeOf;
+
+export const getMetricsK8sAnomaliesSuccessReponsePayloadRT = rt.intersection([
+ rt.type({
+ data: rt.intersection([
+ rt.type({
+ anomalies: rt.array(metricsK8sAnomalyRT),
+ // Signifies there are more entries backwards or forwards. If this was a request
+ // for a previous page, there are more previous pages, if this was a request for a next page,
+ // there are more next pages.
+ hasMoreEntries: rt.boolean,
+ }),
+ rt.partial({
+ paginationCursors: rt.type({
+ // The cursor to use to fetch the previous page
+ previousPageCursor: paginationCursorRT,
+ // The cursor to use to fetch the next page
+ nextPageCursor: paginationCursorRT,
+ }),
+ }),
+ ]),
+ }),
+ rt.partial({
+ timing: routeTimingMetadataRT,
+ }),
+]);
+
+export type GetMetricsK8sAnomaliesSuccessResponsePayload = rt.TypeOf<
+ typeof getMetricsK8sAnomaliesSuccessReponsePayloadRT
+>;
+
+export const getMetricsK8sAnomaliesRequestPayloadRT = rt.type({
+ data: rt.intersection([
+ rt.type({
+ // the ID of the source configuration
+ sourceId: rt.string,
+ // the time range to fetch the log entry anomalies from
+ timeRange: timeRangeRT,
+ }),
+ rt.partial({
+ // Pagination properties
+ pagination: paginationRT,
+ // Sort properties
+ sort: sortRT,
+ // Dataset filters
+ datasets: rt.array(rt.string),
+ }),
+ ]),
+});
+
+export type GetMetricsK8sAnomaliesRequestPayload = rt.TypeOf<
+ typeof getMetricsK8sAnomaliesRequestPayloadRT
+>;
diff --git a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts
new file mode 100644
index 0000000000000..f4497dbba5056
--- /dev/null
+++ b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const ML_SEVERITY_SCORES = {
+ warning: 3,
+ minor: 25,
+ major: 50,
+ critical: 75,
+};
+
+export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES;
+
+export const ML_SEVERITY_COLORS = {
+ critical: 'rgb(228, 72, 72)',
+ major: 'rgb(229, 113, 0)',
+ minor: 'rgb(255, 221, 0)',
+ warning: 'rgb(125, 180, 226)',
+};
+
+export const getSeverityCategoryForScore = (
+ score: number
+): MLSeverityScoreCategories | undefined => {
+ if (score >= ML_SEVERITY_SCORES.critical) {
+ return 'critical';
+ } else if (score >= ML_SEVERITY_SCORES.major) {
+ return 'major';
+ } else if (score >= ML_SEVERITY_SCORES.minor) {
+ return 'minor';
+ } else if (score >= ML_SEVERITY_SCORES.warning) {
+ return 'warning';
+ } else {
+ // Category is too low to include
+ return undefined;
+ }
+};
+
+export const formatAnomalyScore = (score: number) => {
+ return Math.round(score);
+};
+
+export const formatOneDecimalPlace = (number: number) => {
+ return Math.round(number * 10) / 10;
+};
+
+export const getFriendlyNameForPartitionId = (partitionId: string) => {
+ return partitionId !== '' ? partitionId : 'unknown';
+};
+
+export const compareDatasetsByMaximumAnomalyScore = <
+ Dataset extends { maximumAnomalyScore: number }
+>(
+ firstDataset: Dataset,
+ secondDataset: Dataset
+) => firstDataset.maximumAnomalyScore - secondDataset.maximumAnomalyScore;
diff --git a/x-pack/plugins/infra/common/infra_ml/index.ts b/x-pack/plugins/infra/common/infra_ml/index.ts
new file mode 100644
index 0000000000000..88fbbd4f25045
--- /dev/null
+++ b/x-pack/plugins/infra/common/infra_ml/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './infra_ml';
+export * from './anomaly_results';
+export * from './job_parameters';
+export * from './metrics_hosts_ml';
+export * from './metrics_k8s_ml';
diff --git a/x-pack/plugins/infra/common/infra_ml/infra_ml.ts b/x-pack/plugins/infra/common/infra_ml/infra_ml.ts
new file mode 100644
index 0000000000000..680a2a0fef114
--- /dev/null
+++ b/x-pack/plugins/infra/common/infra_ml/infra_ml.ts
@@ -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.
+ */
+
+// combines and abstracts job and datafeed status
+export type JobStatus =
+ | 'unknown'
+ | 'missing'
+ | 'initializing'
+ | 'stopped'
+ | 'started'
+ | 'finished'
+ | 'failed';
+
+export type SetupStatus =
+ | { type: 'initializing' } // acquiring job statuses to determine setup status
+ | { type: 'unknown' } // job status could not be acquired (failed request etc)
+ | { type: 'required' } // setup required
+ | { type: 'pending' } // In the process of setting up the module for the first time or retrying, waiting for response
+ | { type: 'succeeded' } // setup succeeded, notifying user
+ | {
+ type: 'failed';
+ reasons: string[];
+ } // setup failed, notifying user
+ | {
+ type: 'skipped';
+ newlyCreated?: boolean;
+ }; // setup is not necessary
+
+/**
+ * Maps a job status to the possibility that results have already been produced
+ * before this state was reached.
+ */
+export const isJobStatusWithResults = (jobStatus: JobStatus) =>
+ ['started', 'finished', 'stopped', 'failed'].includes(jobStatus);
+
+export const isHealthyJobStatus = (jobStatus: JobStatus) =>
+ ['started', 'finished'].includes(jobStatus);
+
+/**
+ * Maps a setup status to the possibility that results have already been
+ * produced before this state was reached.
+ */
+export const isSetupStatusWithResults = (setupStatus: SetupStatus) =>
+ setupStatus.type === 'skipped';
+
+const KIBANA_SAMPLE_DATA_INDICES = ['kibana_sample_data_logs*'];
+
+export const isExampleDataIndex = (indexName: string) =>
+ KIBANA_SAMPLE_DATA_INDICES.includes(indexName);
diff --git a/x-pack/plugins/infra/common/infra_ml/job_parameters.ts b/x-pack/plugins/infra/common/infra_ml/job_parameters.ts
new file mode 100644
index 0000000000000..8cd1c9ea6e2ba
--- /dev/null
+++ b/x-pack/plugins/infra/common/infra_ml/job_parameters.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 rt from 'io-ts';
+
+export const bucketSpan = 900000;
+
+export const categoriesMessageField = 'message';
+
+export const partitionField = 'event.dataset';
+
+export const getJobIdPrefix = (spaceId: string, sourceId: string) =>
+ `kibana-metrics-ui-${spaceId}-${sourceId}-`;
+
+export const getJobId = (spaceId: string, sourceId: string, jobType: string) =>
+ `${getJobIdPrefix(spaceId, sourceId)}${jobType}`;
+
+export const getDatafeedId = (spaceId: string, sourceId: string, jobType: string) =>
+ `datafeed-${getJobId(spaceId, sourceId, jobType)}`;
+
+export const datasetFilterRT = rt.union([
+ rt.strict({
+ type: rt.literal('includeAll'),
+ }),
+ rt.strict({
+ type: rt.literal('includeSome'),
+ datasets: rt.array(rt.string),
+ }),
+]);
+
+export type DatasetFilter = rt.TypeOf;
+
+export const jobSourceConfigurationRT = rt.partial({
+ indexPattern: rt.string,
+ timestampField: rt.string,
+ bucketSpan: rt.number,
+ datasetFilter: datasetFilterRT,
+});
+
+export type JobSourceConfiguration = rt.TypeOf;
+
+export const jobCustomSettingsRT = rt.partial({
+ job_revision: rt.number,
+ metrics_source_config: jobSourceConfigurationRT,
+});
+
+export type JobCustomSettings = rt.TypeOf;
+
+export const combineDatasetFilters = (
+ firstFilter: DatasetFilter,
+ secondFilter: DatasetFilter
+): DatasetFilter => {
+ if (firstFilter.type === 'includeAll' && secondFilter.type === 'includeAll') {
+ return {
+ type: 'includeAll',
+ };
+ }
+
+ const includedDatasets = new Set([
+ ...(firstFilter.type === 'includeSome' ? firstFilter.datasets : []),
+ ...(secondFilter.type === 'includeSome' ? secondFilter.datasets : []),
+ ]);
+
+ return {
+ type: 'includeSome',
+ datasets: [...includedDatasets],
+ };
+};
+
+export const filterDatasetFilter = (
+ datasetFilter: DatasetFilter,
+ predicate: (dataset: string) => boolean
+): DatasetFilter => {
+ if (datasetFilter.type === 'includeAll') {
+ return datasetFilter;
+ } else {
+ const newDatasets = datasetFilter.datasets.filter(predicate);
+
+ if (newDatasets.length > 0) {
+ return {
+ type: 'includeSome',
+ datasets: newDatasets,
+ };
+ } else {
+ return {
+ type: 'includeAll',
+ };
+ }
+ }
+};
diff --git a/x-pack/plugins/infra/common/infra_ml/metrics_hosts_ml.ts b/x-pack/plugins/infra/common/infra_ml/metrics_hosts_ml.ts
new file mode 100644
index 0000000000000..d09b3be78204f
--- /dev/null
+++ b/x-pack/plugins/infra/common/infra_ml/metrics_hosts_ml.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 rt from 'io-ts';
+
+export const metricsHostsJobTypeRT = rt.keyof({
+ hosts_memory_usage: null,
+ hosts_network_in: null,
+ hosts_network_out: null,
+});
+
+export type MetricsHostsJobType = rt.TypeOf;
+
+export const metricsHostsJobTypes: MetricsHostsJobType[] = [
+ 'hosts_memory_usage',
+ 'hosts_network_in',
+ 'hosts_network_out',
+];
diff --git a/x-pack/plugins/infra/common/infra_ml/metrics_k8s_ml.ts b/x-pack/plugins/infra/common/infra_ml/metrics_k8s_ml.ts
new file mode 100644
index 0000000000000..3c27dbb61a14a
--- /dev/null
+++ b/x-pack/plugins/infra/common/infra_ml/metrics_k8s_ml.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 rt from 'io-ts';
+
+export const metricK8sJobTypeRT = rt.keyof({
+ k8s_memory_usage: null,
+ k8s_network_in: null,
+ k8s_network_out: null,
+});
+
+export type MetricK8sJobType = rt.TypeOf;
+
+export const metricsK8SJobTypes: MetricK8sJobType[] = [
+ 'k8s_memory_usage',
+ 'k8s_network_in',
+ 'k8s_network_out',
+];
diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_api_types.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_api_types.ts
new file mode 100644
index 0000000000000..ee70edc31d49b
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/api/ml_api_types.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 rt from 'io-ts';
+
+export const getMlCapabilitiesResponsePayloadRT = rt.type({
+ capabilities: rt.type({
+ canGetJobs: rt.boolean,
+ canCreateJob: rt.boolean,
+ canDeleteJob: rt.boolean,
+ canOpenJob: rt.boolean,
+ canCloseJob: rt.boolean,
+ canForecastJob: rt.boolean,
+ canGetDatafeeds: rt.boolean,
+ canStartStopDatafeed: rt.boolean,
+ canUpdateJob: rt.boolean,
+ canUpdateDatafeed: rt.boolean,
+ canPreviewDatafeed: rt.boolean,
+ }),
+ isPlatinumOrTrialLicense: rt.boolean,
+ mlFeatureEnabledInSpace: rt.boolean,
+ upgradeInProgress: rt.boolean,
+});
+
+export type GetMlCapabilitiesResponsePayload = rt.TypeOf;
diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_cleanup.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_cleanup.ts
new file mode 100644
index 0000000000000..23fa338e74f14
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/api/ml_cleanup.ts
@@ -0,0 +1,95 @@
+/*
+ * 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 rt from 'io-ts';
+import { pipe } from 'fp-ts/lib/pipeable';
+import { fold } from 'fp-ts/lib/Either';
+import { identity } from 'fp-ts/lib/function';
+import { npStart } from '../../../legacy_singletons';
+
+import { getDatafeedId, getJobId } from '../../../../common/infra_ml';
+import { throwErrors, createPlainError } from '../../../../common/runtime_types';
+
+export const callDeleteJobs = async (
+ spaceId: string,
+ sourceId: string,
+ jobTypes: JobType[]
+) => {
+ // NOTE: Deleting the jobs via this API will delete the datafeeds at the same time
+ const deleteJobsResponse = await npStart.http.fetch('/api/ml/jobs/delete_jobs', {
+ method: 'POST',
+ body: JSON.stringify(
+ deleteJobsRequestPayloadRT.encode({
+ jobIds: jobTypes.map((jobType) => getJobId(spaceId, sourceId, jobType)),
+ })
+ ),
+ });
+
+ return pipe(
+ deleteJobsResponsePayloadRT.decode(deleteJobsResponse),
+ fold(throwErrors(createPlainError), identity)
+ );
+};
+
+export const callGetJobDeletionTasks = async () => {
+ const jobDeletionTasksResponse = await npStart.http.fetch('/api/ml/jobs/deleting_jobs_tasks');
+
+ return pipe(
+ getJobDeletionTasksResponsePayloadRT.decode(jobDeletionTasksResponse),
+ fold(throwErrors(createPlainError), identity)
+ );
+};
+
+export const callStopDatafeeds = async (
+ spaceId: string,
+ sourceId: string,
+ jobTypes: JobType[]
+) => {
+ // Stop datafeed due to https://github.com/elastic/kibana/issues/44652
+ const stopDatafeedResponse = await npStart.http.fetch('/api/ml/jobs/stop_datafeeds', {
+ method: 'POST',
+ body: JSON.stringify(
+ stopDatafeedsRequestPayloadRT.encode({
+ datafeedIds: jobTypes.map((jobType) => getDatafeedId(spaceId, sourceId, jobType)),
+ })
+ ),
+ });
+
+ return pipe(
+ stopDatafeedsResponsePayloadRT.decode(stopDatafeedResponse),
+ fold(throwErrors(createPlainError), identity)
+ );
+};
+
+export const deleteJobsRequestPayloadRT = rt.type({
+ jobIds: rt.array(rt.string),
+});
+
+export type DeleteJobsRequestPayload = rt.TypeOf;
+
+export const deleteJobsResponsePayloadRT = rt.record(
+ rt.string,
+ rt.type({
+ deleted: rt.boolean,
+ })
+);
+
+export type DeleteJobsResponsePayload = rt.TypeOf;
+
+export const getJobDeletionTasksResponsePayloadRT = rt.type({
+ jobIds: rt.array(rt.string),
+});
+
+export const stopDatafeedsRequestPayloadRT = rt.type({
+ datafeedIds: rt.array(rt.string),
+});
+
+export const stopDatafeedsResponsePayloadRT = rt.record(
+ rt.string,
+ rt.type({
+ stopped: rt.boolean,
+ })
+);
diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts
new file mode 100644
index 0000000000000..3fddb63f69791
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 { fold } from 'fp-ts/lib/Either';
+import { identity } from 'fp-ts/lib/function';
+import { pipe } from 'fp-ts/lib/pipeable';
+import * as rt from 'io-ts';
+import { npStart } from '../../../legacy_singletons';
+
+import { getJobId, jobCustomSettingsRT } from '../../../../common/infra_ml';
+import { createPlainError, throwErrors } from '../../../../common/runtime_types';
+
+export const callJobsSummaryAPI = async (
+ spaceId: string,
+ sourceId: string,
+ jobTypes: JobType[]
+) => {
+ const response = await npStart.http.fetch('/api/ml/jobs/jobs_summary', {
+ method: 'POST',
+ body: JSON.stringify(
+ fetchJobStatusRequestPayloadRT.encode({
+ jobIds: jobTypes.map((jobType) => getJobId(spaceId, sourceId, jobType)),
+ })
+ ),
+ });
+ return pipe(
+ fetchJobStatusResponsePayloadRT.decode(response),
+ fold(throwErrors(createPlainError), identity)
+ );
+};
+
+export const fetchJobStatusRequestPayloadRT = rt.type({
+ jobIds: rt.array(rt.string),
+});
+
+export type FetchJobStatusRequestPayload = rt.TypeOf;
+
+const datafeedStateRT = rt.keyof({
+ started: null,
+ stopped: null,
+ stopping: null,
+ '': null,
+});
+
+const jobStateRT = rt.keyof({
+ closed: null,
+ closing: null,
+ deleting: null,
+ failed: null,
+ opened: null,
+ opening: null,
+});
+
+const jobCategorizationStatusRT = rt.keyof({
+ ok: null,
+ warn: null,
+});
+
+const jobModelSizeStatsRT = rt.type({
+ categorization_status: jobCategorizationStatusRT,
+ categorized_doc_count: rt.number,
+ dead_category_count: rt.number,
+ frequent_category_count: rt.number,
+ rare_category_count: rt.number,
+ total_category_count: rt.number,
+});
+
+export type JobModelSizeStats = rt.TypeOf;
+
+export const jobSummaryRT = rt.intersection([
+ rt.type({
+ id: rt.string,
+ jobState: jobStateRT,
+ }),
+ rt.partial({
+ datafeedIndices: rt.array(rt.string),
+ datafeedState: datafeedStateRT,
+ fullJob: rt.partial({
+ custom_settings: jobCustomSettingsRT,
+ finished_time: rt.number,
+ model_size_stats: jobModelSizeStatsRT,
+ }),
+ }),
+]);
+
+export type JobSummary = rt.TypeOf;
+
+export const fetchJobStatusResponsePayloadRT = rt.array(jobSummaryRT);
+
+export type FetchJobStatusResponsePayload = rt.TypeOf;
diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_get_module.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_get_module.ts
new file mode 100644
index 0000000000000..d492522c120a1
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/api/ml_get_module.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { fold } from 'fp-ts/lib/Either';
+import { identity } from 'fp-ts/lib/function';
+import { pipe } from 'fp-ts/lib/pipeable';
+import * as rt from 'io-ts';
+import { npStart } from '../../../legacy_singletons';
+
+import { jobCustomSettingsRT } from '../../../../common/log_analysis';
+import { createPlainError, throwErrors } from '../../../../common/runtime_types';
+
+export const callGetMlModuleAPI = async (moduleId: string) => {
+ const response = await npStart.http.fetch(`/api/ml/modules/get_module/${moduleId}`, {
+ method: 'GET',
+ });
+
+ return pipe(
+ getMlModuleResponsePayloadRT.decode(response),
+ fold(throwErrors(createPlainError), identity)
+ );
+};
+
+const jobDefinitionRT = rt.type({
+ id: rt.string,
+ config: rt.type({
+ custom_settings: jobCustomSettingsRT,
+ }),
+});
+
+export type JobDefinition = rt.TypeOf;
+
+const getMlModuleResponsePayloadRT = rt.type({
+ id: rt.string,
+ jobs: rt.array(jobDefinitionRT),
+});
+
+export type GetMlModuleResponsePayload = rt.TypeOf;
diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_setup_module_api.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_setup_module_api.ts
new file mode 100644
index 0000000000000..06b0e075387b0
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/api/ml_setup_module_api.ts
@@ -0,0 +1,115 @@
+/*
+ * 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 { fold } from 'fp-ts/lib/Either';
+import { identity } from 'fp-ts/lib/function';
+import { pipe } from 'fp-ts/lib/pipeable';
+import * as rt from 'io-ts';
+import { npStart } from '../../../legacy_singletons';
+
+import { getJobIdPrefix, jobCustomSettingsRT } from '../../../../common/infra_ml';
+import { createPlainError, throwErrors } from '../../../../common/runtime_types';
+
+export const callSetupMlModuleAPI = async (
+ moduleId: string,
+ start: number | undefined,
+ end: number | undefined,
+ spaceId: string,
+ sourceId: string,
+ indexPattern: string,
+ jobOverrides: SetupMlModuleJobOverrides[] = [],
+ datafeedOverrides: SetupMlModuleDatafeedOverrides[] = [],
+ query?: object
+) => {
+ const response = await npStart.http.fetch(`/api/ml/modules/setup/${moduleId}`, {
+ method: 'POST',
+ body: JSON.stringify(
+ setupMlModuleRequestPayloadRT.encode({
+ start,
+ end,
+ indexPatternName: indexPattern,
+ prefix: getJobIdPrefix(spaceId, sourceId),
+ startDatafeed: true,
+ jobOverrides,
+ datafeedOverrides,
+ query,
+ })
+ ),
+ });
+
+ return pipe(
+ setupMlModuleResponsePayloadRT.decode(response),
+ fold(throwErrors(createPlainError), identity)
+ );
+};
+
+const setupMlModuleTimeParamsRT = rt.partial({
+ start: rt.number,
+ end: rt.number,
+});
+
+const setupMlModuleJobOverridesRT = rt.type({
+ job_id: rt.string,
+ custom_settings: jobCustomSettingsRT,
+});
+
+export type SetupMlModuleJobOverrides = rt.TypeOf;
+
+const setupMlModuleDatafeedOverridesRT = rt.object;
+
+export type SetupMlModuleDatafeedOverrides = rt.TypeOf;
+
+const setupMlModuleRequestParamsRT = rt.intersection([
+ rt.strict({
+ indexPatternName: rt.string,
+ prefix: rt.string,
+ startDatafeed: rt.boolean,
+ jobOverrides: rt.array(setupMlModuleJobOverridesRT),
+ datafeedOverrides: rt.array(setupMlModuleDatafeedOverridesRT),
+ }),
+ rt.exact(
+ rt.partial({
+ query: rt.object,
+ })
+ ),
+]);
+
+const setupMlModuleRequestPayloadRT = rt.intersection([
+ setupMlModuleTimeParamsRT,
+ setupMlModuleRequestParamsRT,
+]);
+
+const setupErrorResponseRT = rt.type({
+ msg: rt.string,
+});
+
+const datafeedSetupResponseRT = rt.intersection([
+ rt.type({
+ id: rt.string,
+ started: rt.boolean,
+ success: rt.boolean,
+ }),
+ rt.partial({
+ error: setupErrorResponseRT,
+ }),
+]);
+
+const jobSetupResponseRT = rt.intersection([
+ rt.type({
+ id: rt.string,
+ success: rt.boolean,
+ }),
+ rt.partial({
+ error: setupErrorResponseRT,
+ }),
+]);
+
+const setupMlModuleResponsePayloadRT = rt.type({
+ datafeeds: rt.array(datafeedSetupResponseRT),
+ jobs: rt.array(jobSetupResponseRT),
+});
+
+export type SetupMlModuleResponsePayload = rt.TypeOf;
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx
new file mode 100644
index 0000000000000..f4c90a459af6a
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.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 createContainer from 'constate';
+import { useMemo, useState, useEffect } from 'react';
+import { fold } from 'fp-ts/lib/Either';
+import { pipe } from 'fp-ts/lib/pipeable';
+import { identity } from 'fp-ts/lib/function';
+import { useTrackedPromise } from '../../utils/use_tracked_promise';
+import { npStart } from '../../legacy_singletons';
+import {
+ getMlCapabilitiesResponsePayloadRT,
+ GetMlCapabilitiesResponsePayload,
+} from './api/ml_api_types';
+import { throwErrors, createPlainError } from '../../../common/runtime_types';
+
+export const useInfraMLCapabilities = () => {
+ const [mlCapabilities, setMlCapabilities] = useState(
+ initialMlCapabilities
+ );
+
+ const [fetchMlCapabilitiesRequest, fetchMlCapabilities] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ const rawResponse = await npStart.http.fetch('/api/ml/ml_capabilities');
+
+ return pipe(
+ getMlCapabilitiesResponsePayloadRT.decode(rawResponse),
+ fold(throwErrors(createPlainError), identity)
+ );
+ },
+ onResolve: (response) => {
+ setMlCapabilities(response);
+ },
+ },
+ []
+ );
+
+ useEffect(() => {
+ fetchMlCapabilities();
+ }, [fetchMlCapabilities]);
+
+ const isLoading = useMemo(() => fetchMlCapabilitiesRequest.state === 'pending', [
+ fetchMlCapabilitiesRequest.state,
+ ]);
+
+ const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob;
+ const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs;
+ const hasInfraMLCapabilites =
+ mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace;
+
+ return {
+ hasInfraMLCapabilites,
+ hasInfraMLReadCapabilities,
+ hasInfraMLSetupCapabilities,
+ isLoading,
+ };
+};
+
+export const [InfraMLCapabilitiesProvider, useInfraMLCapabilitiesContext] = createContainer(
+ useInfraMLCapabilities
+);
+
+const initialMlCapabilities = {
+ capabilities: {
+ canGetJobs: false,
+ canCreateJob: false,
+ canDeleteJob: false,
+ canOpenJob: false,
+ canCloseJob: false,
+ canForecastJob: false,
+ canGetDatafeeds: false,
+ canStartStopDatafeed: false,
+ canUpdateJob: false,
+ canUpdateDatafeed: false,
+ canPreviewDatafeed: false,
+ canGetCalendars: false,
+ canCreateCalendar: false,
+ canDeleteCalendar: false,
+ canGetFilters: false,
+ canCreateFilter: false,
+ canDeleteFilter: false,
+ canFindFileStructure: false,
+ canGetDataFrameJobs: false,
+ canDeleteDataFrameJob: false,
+ canPreviewDataFrameJob: false,
+ canCreateDataFrameJob: false,
+ canStartStopDataFrameJob: false,
+ },
+ isPlatinumOrTrialLicense: false,
+ mlFeatureEnabledInSpace: false,
+ upgradeInProgress: false,
+};
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_cleanup.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_cleanup.tsx
new file mode 100644
index 0000000000000..736982c8043b1
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_cleanup.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getJobId } from '../../../common/infra_ml';
+import { callDeleteJobs, callGetJobDeletionTasks, callStopDatafeeds } from './api/ml_cleanup';
+
+export const cleanUpJobsAndDatafeeds = async (
+ spaceId: string,
+ sourceId: string,
+ jobTypes: JobType[]
+) => {
+ try {
+ await callStopDatafeeds(spaceId, sourceId, jobTypes);
+ } catch (err) {
+ // Proceed only if datafeed has been deleted or didn't exist in the first place
+ if (err?.res?.status !== 404) {
+ throw err;
+ }
+ }
+
+ return await deleteJobs(spaceId, sourceId, jobTypes);
+};
+
+const deleteJobs = async (
+ spaceId: string,
+ sourceId: string,
+ jobTypes: JobType[]
+) => {
+ const deleteJobsResponse = await callDeleteJobs(spaceId, sourceId, jobTypes);
+ await waitUntilJobsAreDeleted(spaceId, sourceId, jobTypes);
+ return deleteJobsResponse;
+};
+
+const waitUntilJobsAreDeleted = async (
+ spaceId: string,
+ sourceId: string,
+ jobTypes: JobType[]
+) => {
+ const moduleJobIds = jobTypes.map((jobType) => getJobId(spaceId, sourceId, jobType));
+ while (true) {
+ const { jobIds: jobIdsBeingDeleted } = await callGetJobDeletionTasks();
+ const needToWait = jobIdsBeingDeleted.some((jobId) => moduleJobIds.includes(jobId));
+
+ if (needToWait) {
+ await timeout(1000);
+ } else {
+ return true;
+ }
+ }
+};
+
+const timeout = (ms: number) => new Promise((res) => setTimeout(res, ms));
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx
new file mode 100644
index 0000000000000..349541d108f5e
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx
@@ -0,0 +1,147 @@
+/*
+ * 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, useMemo } from 'react';
+import { DatasetFilter } from '../../../common/infra_ml';
+import { useTrackedPromise } from '../../utils/use_tracked_promise';
+import { useModuleStatus } from './infra_ml_module_status';
+import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types';
+
+export const useInfraMLModule = ({
+ sourceConfiguration,
+ moduleDescriptor,
+}: {
+ sourceConfiguration: ModuleSourceConfiguration;
+ moduleDescriptor: ModuleDescriptor;
+}) => {
+ const { spaceId, sourceId, timestampField } = sourceConfiguration;
+ const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes);
+
+ const [, fetchJobStatus] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ dispatchModuleStatus({ type: 'fetchingJobStatuses' });
+ return await moduleDescriptor.getJobSummary(spaceId, sourceId);
+ },
+ onResolve: (jobResponse) => {
+ dispatchModuleStatus({
+ type: 'fetchedJobStatuses',
+ payload: jobResponse,
+ spaceId,
+ sourceId,
+ });
+ },
+ onReject: () => {
+ dispatchModuleStatus({ type: 'failedFetchingJobStatuses' });
+ },
+ },
+ [spaceId, sourceId]
+ );
+
+ const [, setUpModule] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async (
+ selectedIndices: string[],
+ start: number | undefined,
+ end: number | undefined,
+ datasetFilter: DatasetFilter,
+ partitionField?: string
+ ) => {
+ dispatchModuleStatus({ type: 'startedSetup' });
+ const setupResult = await moduleDescriptor.setUpModule(
+ start,
+ end,
+ datasetFilter,
+ {
+ indices: selectedIndices,
+ sourceId,
+ spaceId,
+ timestampField,
+ },
+ partitionField
+ );
+ const jobSummaries = await moduleDescriptor.getJobSummary(spaceId, sourceId);
+ return { setupResult, jobSummaries };
+ },
+ onResolve: ({ setupResult: { datafeeds, jobs }, jobSummaries }) => {
+ dispatchModuleStatus({
+ type: 'finishedSetup',
+ datafeedSetupResults: datafeeds,
+ jobSetupResults: jobs,
+ jobSummaries,
+ spaceId,
+ sourceId,
+ });
+ },
+ onReject: () => {
+ dispatchModuleStatus({ type: 'failedSetup' });
+ },
+ },
+ [moduleDescriptor.setUpModule, spaceId, sourceId, timestampField]
+ );
+
+ const [cleanUpModuleRequest, cleanUpModule] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ return await moduleDescriptor.cleanUpModule(spaceId, sourceId);
+ },
+ },
+ [spaceId, sourceId]
+ );
+
+ const isCleaningUp = useMemo(() => cleanUpModuleRequest.state === 'pending', [
+ cleanUpModuleRequest.state,
+ ]);
+
+ const cleanUpAndSetUpModule = useCallback(
+ (
+ selectedIndices: string[],
+ start: number | undefined,
+ end: number | undefined,
+ datasetFilter: DatasetFilter,
+ partitionField?: string
+ ) => {
+ dispatchModuleStatus({ type: 'startedSetup' });
+ cleanUpModule()
+ .then(() => {
+ setUpModule(selectedIndices, start, end, datasetFilter, partitionField);
+ })
+ .catch(() => {
+ dispatchModuleStatus({ type: 'failedSetup' });
+ });
+ },
+ [cleanUpModule, dispatchModuleStatus, setUpModule]
+ );
+
+ const viewResults = useCallback(() => {
+ dispatchModuleStatus({ type: 'viewedResults' });
+ }, [dispatchModuleStatus]);
+
+ const jobIds = useMemo(() => moduleDescriptor.getJobIds(spaceId, sourceId), [
+ moduleDescriptor,
+ spaceId,
+ sourceId,
+ ]);
+
+ return {
+ cleanUpAndSetUpModule,
+ cleanUpModule,
+ fetchJobStatus,
+ isCleaningUp,
+ jobIds,
+ jobStatus: moduleStatus.jobStatus,
+ jobSummaries: moduleStatus.jobSummaries,
+ lastSetupErrorMessages: moduleStatus.lastSetupErrorMessages,
+ moduleDescriptor,
+ setUpModule,
+ setupStatus: moduleStatus.setupStatus,
+ sourceConfiguration,
+ viewResults,
+ };
+};
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts
new file mode 100644
index 0000000000000..2d90f5d531010
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts
@@ -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 { useMemo } from 'react';
+import { JobSummary } from './api/ml_get_jobs_summary_api';
+import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types';
+
+export const useInfraMLModuleConfiguration = ({
+ moduleDescriptor,
+ sourceConfiguration,
+}: {
+ moduleDescriptor: ModuleDescriptor;
+ sourceConfiguration: ModuleSourceConfiguration;
+}) => {
+ const getIsJobConfigurationOutdated = useMemo(
+ () => isJobConfigurationOutdated(moduleDescriptor, sourceConfiguration),
+ [sourceConfiguration, moduleDescriptor]
+ );
+
+ return {
+ getIsJobConfigurationOutdated,
+ };
+};
+
+export const isJobConfigurationOutdated = (
+ { bucketSpan }: ModuleDescriptor,
+ currentSourceConfiguration: ModuleSourceConfiguration
+) => (jobSummary: JobSummary): boolean => {
+ if (!jobSummary.fullJob || !jobSummary.fullJob.custom_settings) {
+ return false;
+ }
+
+ const jobConfiguration = jobSummary.fullJob.custom_settings.metrics_source_config;
+
+ return !(
+ jobConfiguration &&
+ jobConfiguration.bucketSpan === bucketSpan &&
+ jobConfiguration.indexPattern &&
+ isSubset(
+ new Set(jobConfiguration.indexPattern.split(',')),
+ new Set(currentSourceConfiguration.indices)
+ ) &&
+ jobConfiguration.timestampField === currentSourceConfiguration.timestampField
+ );
+};
+
+const isSubset = (subset: Set, superset: Set) => {
+ return Array.from(subset).every((subsetElement) => superset.has(subsetElement));
+};
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_definition.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_definition.tsx
new file mode 100644
index 0000000000000..3c7ffcfd4a4e2
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_definition.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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, useMemo, useState } from 'react';
+import { getJobId } from '../../../common/log_analysis';
+import { useTrackedPromise } from '../../utils/use_tracked_promise';
+import { JobSummary } from './api/ml_get_jobs_summary_api';
+import { GetMlModuleResponsePayload, JobDefinition } from './api/ml_get_module';
+import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types';
+
+export const useInfraMLModuleDefinition = ({
+ sourceConfiguration: { spaceId, sourceId },
+ moduleDescriptor,
+}: {
+ sourceConfiguration: ModuleSourceConfiguration;
+ moduleDescriptor: ModuleDescriptor;
+}) => {
+ const [moduleDefinition, setModuleDefinition] = useState<
+ GetMlModuleResponsePayload | undefined
+ >();
+
+ const jobDefinitionByJobId = useMemo(
+ () =>
+ moduleDefinition
+ ? moduleDefinition.jobs.reduce>(
+ (accumulatedJobDefinitions, jobDefinition) => ({
+ ...accumulatedJobDefinitions,
+ [getJobId(spaceId, sourceId, jobDefinition.id)]: jobDefinition,
+ }),
+ {}
+ )
+ : {},
+ [moduleDefinition, sourceId, spaceId]
+ );
+
+ const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ return await moduleDescriptor.getModuleDefinition();
+ },
+ onResolve: (response) => {
+ setModuleDefinition(response);
+ },
+ onReject: () => {
+ setModuleDefinition(undefined);
+ },
+ },
+ [moduleDescriptor.getModuleDefinition, spaceId, sourceId]
+ );
+
+ const getIsJobDefinitionOutdated = useCallback(
+ (jobSummary: JobSummary): boolean => {
+ const jobDefinition: JobDefinition | undefined = jobDefinitionByJobId[jobSummary.id];
+
+ if (jobDefinition == null) {
+ return false;
+ }
+
+ const currentRevision = jobDefinition?.config.custom_settings.job_revision;
+ return (jobSummary.fullJob?.custom_settings?.job_revision ?? 0) < (currentRevision ?? 0);
+ },
+ [jobDefinitionByJobId]
+ );
+
+ return {
+ fetchModuleDefinition,
+ fetchModuleDefinitionRequestState: fetchModuleDefinitionRequest.state,
+ getIsJobDefinitionOutdated,
+ jobDefinitionByJobId,
+ moduleDefinition,
+ };
+};
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_status.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_status.tsx
new file mode 100644
index 0000000000000..63d479546b44f
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_status.tsx
@@ -0,0 +1,268 @@
+/*
+ * 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 { useReducer } from 'react';
+
+import {
+ JobStatus,
+ getDatafeedId,
+ getJobId,
+ isJobStatusWithResults,
+ SetupStatus,
+} from '../../../common/infra_ml';
+import { FetchJobStatusResponsePayload, JobSummary } from './api/ml_get_jobs_summary_api';
+import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api';
+import { MandatoryProperty } from '../../../common/utility_types';
+
+interface StatusReducerState {
+ jobStatus: Record;
+ jobSummaries: JobSummary[];
+ lastSetupErrorMessages: string[];
+ setupStatus: SetupStatus;
+}
+
+type StatusReducerAction =
+ | { type: 'startedSetup' }
+ | {
+ type: 'finishedSetup';
+ sourceId: string;
+ spaceId: string;
+ jobSetupResults: SetupMlModuleResponsePayload['jobs'];
+ jobSummaries: FetchJobStatusResponsePayload;
+ datafeedSetupResults: SetupMlModuleResponsePayload['datafeeds'];
+ }
+ | { type: 'failedSetup' }
+ | { type: 'fetchingJobStatuses' }
+ | {
+ type: 'fetchedJobStatuses';
+ spaceId: string;
+ sourceId: string;
+ payload: FetchJobStatusResponsePayload;
+ }
+ | { type: 'failedFetchingJobStatuses' }
+ | { type: 'viewedResults' };
+
+const createInitialState = ({
+ jobTypes,
+}: {
+ jobTypes: JobType[];
+}): StatusReducerState => ({
+ jobStatus: jobTypes.reduce(
+ (accumulatedJobStatus, jobType) => ({
+ ...accumulatedJobStatus,
+ [jobType]: 'unknown',
+ }),
+ {} as Record
+ ),
+ jobSummaries: [],
+ lastSetupErrorMessages: [],
+ setupStatus: { type: 'initializing' },
+});
+
+const createStatusReducer = (jobTypes: JobType[]) => (
+ state: StatusReducerState,
+ action: StatusReducerAction
+): StatusReducerState => {
+ switch (action.type) {
+ case 'startedSetup': {
+ return {
+ ...state,
+ jobStatus: jobTypes.reduce(
+ (accumulatedJobStatus, jobType) => ({
+ ...accumulatedJobStatus,
+ [jobType]: 'initializing',
+ }),
+ {} as Record
+ ),
+ setupStatus: { type: 'pending' },
+ };
+ }
+ case 'finishedSetup': {
+ const { datafeedSetupResults, jobSetupResults, jobSummaries, spaceId, sourceId } = action;
+ const nextJobStatus = jobTypes.reduce(
+ (accumulatedJobStatus, jobType) => ({
+ ...accumulatedJobStatus,
+ [jobType]:
+ hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, jobType))(jobSetupResults) &&
+ hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, jobType))(
+ datafeedSetupResults
+ )
+ ? 'started'
+ : 'failed',
+ }),
+ {} as Record
+ );
+ const nextSetupStatus: SetupStatus = Object.values(nextJobStatus).every(
+ (jobState) => jobState === 'started'
+ )
+ ? { type: 'succeeded' }
+ : {
+ type: 'failed',
+ reasons: [
+ ...Object.values(datafeedSetupResults)
+ .filter(hasError)
+ .map((datafeed) => datafeed.error.msg),
+ ...Object.values(jobSetupResults)
+ .filter(hasError)
+ .map((job) => job.error.msg),
+ ],
+ };
+
+ return {
+ ...state,
+ jobStatus: nextJobStatus,
+ jobSummaries,
+ setupStatus: nextSetupStatus,
+ };
+ }
+ case 'failedSetup': {
+ return {
+ ...state,
+ jobStatus: jobTypes.reduce(
+ (accumulatedJobStatus, jobType) => ({
+ ...accumulatedJobStatus,
+ [jobType]: 'failed',
+ }),
+ {} as Record
+ ),
+ setupStatus: { type: 'failed', reasons: ['unknown'] },
+ };
+ }
+ case 'fetchingJobStatuses': {
+ return {
+ ...state,
+ setupStatus:
+ state.setupStatus.type === 'unknown' ? { type: 'initializing' } : state.setupStatus,
+ };
+ }
+ case 'fetchedJobStatuses': {
+ const { payload: jobSummaries, spaceId, sourceId } = action;
+ const { setupStatus } = state;
+ const nextJobStatus = jobTypes.reduce(
+ (accumulatedJobStatus, jobType) => ({
+ ...accumulatedJobStatus,
+ [jobType]: getJobStatus(getJobId(spaceId, sourceId, jobType))(jobSummaries),
+ }),
+ {} as Record
+ );
+ const nextSetupStatus = getSetupStatus(nextJobStatus)(setupStatus);
+
+ return {
+ ...state,
+ jobSummaries,
+ jobStatus: nextJobStatus,
+ setupStatus: nextSetupStatus,
+ };
+ }
+ case 'failedFetchingJobStatuses': {
+ return {
+ ...state,
+ setupStatus: { type: 'unknown' },
+ jobStatus: jobTypes.reduce(
+ (accumulatedJobStatus, jobType) => ({
+ ...accumulatedJobStatus,
+ [jobType]: 'unknown',
+ }),
+ {} as Record
+ ),
+ };
+ }
+ case 'viewedResults': {
+ return {
+ ...state,
+ setupStatus: { type: 'skipped', newlyCreated: true },
+ };
+ }
+ default: {
+ return state;
+ }
+ }
+};
+
+const hasSuccessfullyCreatedJob = (jobId: string) => (
+ jobSetupResponses: SetupMlModuleResponsePayload['jobs']
+) =>
+ jobSetupResponses.filter(
+ (jobSetupResponse) =>
+ jobSetupResponse.id === jobId && jobSetupResponse.success && !jobSetupResponse.error
+ ).length > 0;
+
+const hasSuccessfullyStartedDatafeed = (datafeedId: string) => (
+ datafeedSetupResponses: SetupMlModuleResponsePayload['datafeeds']
+) =>
+ datafeedSetupResponses.filter(
+ (datafeedSetupResponse) =>
+ datafeedSetupResponse.id === datafeedId &&
+ datafeedSetupResponse.success &&
+ datafeedSetupResponse.started &&
+ !datafeedSetupResponse.error
+ ).length > 0;
+
+const getJobStatus = (jobId: string) => (
+ jobSummaries: FetchJobStatusResponsePayload
+): JobStatus => {
+ return (
+ jobSummaries
+ .filter((jobSummary) => jobSummary.id === jobId)
+ .map(
+ (jobSummary): JobStatus => {
+ if (jobSummary.jobState === 'failed' || jobSummary.datafeedState === '') {
+ return 'failed';
+ } else if (
+ jobSummary.jobState === 'closed' &&
+ jobSummary.datafeedState === 'stopped' &&
+ jobSummary.fullJob &&
+ jobSummary.fullJob.finished_time != null
+ ) {
+ return 'finished';
+ } else if (
+ jobSummary.jobState === 'closed' ||
+ jobSummary.jobState === 'closing' ||
+ jobSummary.datafeedState === 'stopped'
+ ) {
+ return 'stopped';
+ } else if (jobSummary.jobState === 'opening') {
+ return 'initializing';
+ } else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') {
+ return 'started';
+ }
+
+ return 'unknown';
+ }
+ )[0] || 'missing'
+ );
+};
+
+const getSetupStatus = (everyJobStatus: Record) => (
+ previousSetupStatus: SetupStatus
+): SetupStatus => {
+ return Object.entries(everyJobStatus).reduce(
+ (setupStatus, [, jobStatus]) => {
+ if (jobStatus === 'missing') {
+ return { type: 'required' };
+ } else if (setupStatus.type === 'required' || setupStatus.type === 'succeeded') {
+ return setupStatus;
+ } else if (setupStatus.type === 'skipped' || isJobStatusWithResults(jobStatus)) {
+ return {
+ type: 'skipped',
+ // preserve newlyCreated status
+ newlyCreated: setupStatus.type === 'skipped' && setupStatus.newlyCreated,
+ };
+ }
+
+ return setupStatus;
+ },
+ previousSetupStatus
+ );
+};
+
+const hasError = (
+ value: Value
+): value is MandatoryProperty => value.error != null;
+
+export const useModuleStatus = (jobTypes: JobType[]) => {
+ return useReducer(createStatusReducer(jobTypes), { jobTypes }, createInitialState);
+};
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts
new file mode 100644
index 0000000000000..a9f2671de8259
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 {
+ ValidateLogEntryDatasetsResponsePayload,
+ ValidationIndicesResponsePayload,
+} from '../../../common/http_api/log_analysis';
+import { DatasetFilter } from '../../../common/infra_ml';
+import { DeleteJobsResponsePayload } from './api/ml_cleanup';
+import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api';
+import { GetMlModuleResponsePayload } from './api/ml_get_module';
+import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api';
+
+export { JobModelSizeStats, JobSummary } from './api/ml_get_jobs_summary_api';
+
+export interface ModuleDescriptor {
+ moduleId: string;
+ moduleName: string;
+ moduleDescription: string;
+ jobTypes: JobType[];
+ bucketSpan: number;
+ getJobIds: (spaceId: string, sourceId: string) => Record;
+ getJobSummary: (spaceId: string, sourceId: string) => Promise;
+ getModuleDefinition: () => Promise;
+ setUpModule: (
+ start: number | undefined,
+ end: number | undefined,
+ datasetFilter: DatasetFilter,
+ sourceConfiguration: ModuleSourceConfiguration,
+ partitionField?: string
+ ) => Promise;
+ cleanUpModule: (spaceId: string, sourceId: string) => Promise;
+ validateSetupIndices: (
+ indices: string[],
+ timestampField: string
+ ) => Promise;
+ validateSetupDatasets: (
+ indices: string[],
+ timestampField: string,
+ startTime: number,
+ endTime: number
+ ) => Promise;
+}
+
+export interface ModuleSourceConfiguration {
+ indices: string[];
+ sourceId: string;
+ spaceId: string;
+ timestampField: string;
+}
+
+interface ManyCategoriesWarningReason {
+ type: 'manyCategories';
+ categoriesDocumentRatio: number;
+}
+
+interface ManyDeadCategoriesWarningReason {
+ type: 'manyDeadCategories';
+ deadCategoriesRatio: number;
+}
+
+interface ManyRareCategoriesWarningReason {
+ type: 'manyRareCategories';
+ rareCategoriesRatio: number;
+}
+
+interface NoFrequentCategoriesWarningReason {
+ type: 'noFrequentCategories';
+}
+
+interface SingleCategoryWarningReason {
+ type: 'singleCategory';
+}
+
+export type CategoryQualityWarningReason =
+ | ManyCategoriesWarningReason
+ | ManyDeadCategoriesWarningReason
+ | ManyRareCategoriesWarningReason
+ | NoFrequentCategoriesWarningReason
+ | SingleCategoryWarningReason;
+
+export type CategoryQualityWarningReasonType = CategoryQualityWarningReason['type'];
+
+export interface CategoryQualityWarning {
+ type: 'categoryQualityWarning';
+ jobId: string;
+ reasons: CategoryQualityWarningReason[];
+}
+
+export type QualityWarning = CategoryQualityWarning;
diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts
new file mode 100644
index 0000000000000..0dfe3b301f240
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts
@@ -0,0 +1,289 @@
+/*
+ * 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 { isEqual } from 'lodash';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { usePrevious } from 'react-use';
+import {
+ combineDatasetFilters,
+ DatasetFilter,
+ filterDatasetFilter,
+ isExampleDataIndex,
+} from '../../../common/infra_ml';
+import {
+ AvailableIndex,
+ ValidationIndicesError,
+ ValidationUIError,
+} from '../../components/logging/log_analysis_setup/initial_configuration_step';
+import { useTrackedPromise } from '../../utils/use_tracked_promise';
+import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types';
+
+type SetupHandler = (
+ indices: string[],
+ startTime: number | undefined,
+ endTime: number | undefined,
+ datasetFilter: DatasetFilter
+) => void;
+
+interface AnalysisSetupStateArguments {
+ cleanUpAndSetUpModule: SetupHandler;
+ moduleDescriptor: ModuleDescriptor;
+ setUpModule: SetupHandler;
+ sourceConfiguration: ModuleSourceConfiguration;
+}
+
+const fourWeeksInMs = 86400000 * 7 * 4;
+
+export const useAnalysisSetupState = ({
+ cleanUpAndSetUpModule,
+ moduleDescriptor: { validateSetupDatasets, validateSetupIndices },
+ setUpModule,
+ sourceConfiguration,
+}: AnalysisSetupStateArguments) => {
+ const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs);
+ const [endTime, setEndTime] = useState(undefined);
+
+ const isTimeRangeValid = useMemo(
+ () => (startTime != null && endTime != null ? startTime < endTime : true),
+ [endTime, startTime]
+ );
+
+ const [validatedIndices, setValidatedIndices] = useState(
+ sourceConfiguration.indices.map((indexName) => ({
+ name: indexName,
+ validity: 'unknown' as const,
+ }))
+ );
+
+ const updateIndicesWithValidationErrors = useCallback(
+ (validationErrors: ValidationIndicesError[]) =>
+ setValidatedIndices((availableIndices) =>
+ availableIndices.map((previousAvailableIndex) => {
+ const indexValiationErrors = validationErrors.filter(
+ ({ index }) => index === previousAvailableIndex.name
+ );
+
+ if (indexValiationErrors.length > 0) {
+ return {
+ validity: 'invalid',
+ name: previousAvailableIndex.name,
+ errors: indexValiationErrors,
+ };
+ } else if (previousAvailableIndex.validity === 'valid') {
+ return {
+ ...previousAvailableIndex,
+ validity: 'valid',
+ errors: [],
+ };
+ } else {
+ return {
+ validity: 'valid',
+ name: previousAvailableIndex.name,
+ isSelected: !isExampleDataIndex(previousAvailableIndex.name),
+ availableDatasets: [],
+ datasetFilter: {
+ type: 'includeAll' as const,
+ },
+ };
+ }
+ })
+ ),
+ []
+ );
+
+ const updateIndicesWithAvailableDatasets = useCallback(
+ (availableDatasets: Array<{ indexName: string; datasets: string[] }>) =>
+ setValidatedIndices((availableIndices) =>
+ availableIndices.map((previousAvailableIndex) => {
+ if (previousAvailableIndex.validity !== 'valid') {
+ return previousAvailableIndex;
+ }
+
+ const availableDatasetsForIndex = availableDatasets.filter(
+ ({ indexName }) => indexName === previousAvailableIndex.name
+ );
+ const newAvailableDatasets = availableDatasetsForIndex.flatMap(
+ ({ datasets }) => datasets
+ );
+
+ // filter out datasets that have disappeared if this index' datasets were updated
+ const newDatasetFilter: DatasetFilter =
+ availableDatasetsForIndex.length > 0
+ ? filterDatasetFilter(previousAvailableIndex.datasetFilter, (dataset) =>
+ newAvailableDatasets.includes(dataset)
+ )
+ : previousAvailableIndex.datasetFilter;
+
+ return {
+ ...previousAvailableIndex,
+ availableDatasets: newAvailableDatasets,
+ datasetFilter: newDatasetFilter,
+ };
+ })
+ ),
+ []
+ );
+
+ const validIndexNames = useMemo(
+ () => validatedIndices.filter((index) => index.validity === 'valid').map((index) => index.name),
+ [validatedIndices]
+ );
+
+ const selectedIndexNames = useMemo(
+ () =>
+ validatedIndices
+ .filter((index) => index.validity === 'valid' && index.isSelected)
+ .map((i) => i.name),
+ [validatedIndices]
+ );
+
+ const datasetFilter = useMemo(
+ () =>
+ validatedIndices
+ .flatMap((validatedIndex) =>
+ validatedIndex.validity === 'valid'
+ ? validatedIndex.datasetFilter
+ : { type: 'includeAll' as const }
+ )
+ .reduce(combineDatasetFilters, { type: 'includeAll' as const }),
+ [validatedIndices]
+ );
+
+ const [validateIndicesRequest, validateIndices] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ return await validateSetupIndices(
+ sourceConfiguration.indices,
+ sourceConfiguration.timestampField
+ );
+ },
+ onResolve: ({ data: { errors } }) => {
+ updateIndicesWithValidationErrors(errors);
+ },
+ onReject: () => {
+ setValidatedIndices([]);
+ },
+ },
+ [sourceConfiguration.indices, sourceConfiguration.timestampField]
+ );
+
+ const [validateDatasetsRequest, validateDatasets] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'resolution',
+ createPromise: async () => {
+ if (validIndexNames.length === 0) {
+ return { data: { datasets: [] } };
+ }
+
+ return await validateSetupDatasets(
+ validIndexNames,
+ sourceConfiguration.timestampField,
+ startTime ?? 0,
+ endTime ?? Date.now()
+ );
+ },
+ onResolve: ({ data: { datasets } }) => {
+ updateIndicesWithAvailableDatasets(datasets);
+ },
+ },
+ [validIndexNames, sourceConfiguration.timestampField, startTime, endTime]
+ );
+
+ const setUp = useCallback(() => {
+ return setUpModule(selectedIndexNames, startTime, endTime, datasetFilter);
+ }, [setUpModule, selectedIndexNames, startTime, endTime, datasetFilter]);
+
+ const cleanUpAndSetUp = useCallback(() => {
+ return cleanUpAndSetUpModule(selectedIndexNames, startTime, endTime, datasetFilter);
+ }, [cleanUpAndSetUpModule, selectedIndexNames, startTime, endTime, datasetFilter]);
+
+ const isValidating = useMemo(
+ () => validateIndicesRequest.state === 'pending' || validateDatasetsRequest.state === 'pending',
+ [validateDatasetsRequest.state, validateIndicesRequest.state]
+ );
+
+ const validationErrors = useMemo(() => {
+ if (isValidating) {
+ return [];
+ }
+
+ return [
+ // validate request status
+ ...(validateIndicesRequest.state === 'rejected' ||
+ validateDatasetsRequest.state === 'rejected'
+ ? [{ error: 'NETWORK_ERROR' as const }]
+ : []),
+ // validation request results
+ ...validatedIndices.reduce((errors, index) => {
+ return index.validity === 'invalid' && selectedIndexNames.includes(index.name)
+ ? [...errors, ...index.errors]
+ : errors;
+ }, []),
+ // index count
+ ...(selectedIndexNames.length === 0 ? [{ error: 'TOO_FEW_SELECTED_INDICES' as const }] : []),
+ // time range
+ ...(!isTimeRangeValid ? [{ error: 'INVALID_TIME_RANGE' as const }] : []),
+ ];
+ }, [
+ isValidating,
+ validateIndicesRequest.state,
+ validateDatasetsRequest.state,
+ validatedIndices,
+ selectedIndexNames,
+ isTimeRangeValid,
+ ]);
+
+ const prevStartTime = usePrevious(startTime);
+ const prevEndTime = usePrevious(endTime);
+ const prevValidIndexNames = usePrevious(validIndexNames);
+
+ useEffect(() => {
+ if (!isTimeRangeValid) {
+ return;
+ }
+
+ validateIndices();
+ }, [isTimeRangeValid, validateIndices]);
+
+ useEffect(() => {
+ if (!isTimeRangeValid) {
+ return;
+ }
+
+ if (
+ startTime !== prevStartTime ||
+ endTime !== prevEndTime ||
+ !isEqual(validIndexNames, prevValidIndexNames)
+ ) {
+ validateDatasets();
+ }
+ }, [
+ endTime,
+ isTimeRangeValid,
+ prevEndTime,
+ prevStartTime,
+ prevValidIndexNames,
+ startTime,
+ validIndexNames,
+ validateDatasets,
+ ]);
+
+ return {
+ cleanUpAndSetUp,
+ datasetFilter,
+ endTime,
+ isValidating,
+ selectedIndexNames,
+ setEndTime,
+ setStartTime,
+ setUp,
+ startTime,
+ validatedIndices,
+ setValidatedIndices,
+ validationErrors,
+ };
+};
diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx
new file mode 100644
index 0000000000000..9c065f3e91bc4
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 createContainer from 'constate';
+import { useMemo } from 'react';
+import { useInfraMLModule } from '../../infra_ml_module';
+import { useInfraMLModuleConfiguration } from '../../infra_ml_module_configuration';
+import { useInfraMLModuleDefinition } from '../../infra_ml_module_definition';
+import { ModuleSourceConfiguration } from '../../infra_ml_module_types';
+import { metricHostsModule } from './module_descriptor';
+
+export const useMetricHostsModule = ({
+ indexPattern,
+ sourceId,
+ spaceId,
+ timestampField,
+}: {
+ indexPattern: string;
+ sourceId: string;
+ spaceId: string;
+ timestampField: string;
+}) => {
+ const sourceConfiguration: ModuleSourceConfiguration = useMemo(
+ () => ({
+ indices: indexPattern.split(','),
+ sourceId,
+ spaceId,
+ timestampField,
+ }),
+ [indexPattern, sourceId, spaceId, timestampField]
+ );
+
+ const infraMLModule = useInfraMLModule({
+ moduleDescriptor: metricHostsModule,
+ sourceConfiguration,
+ });
+
+ const { getIsJobConfigurationOutdated } = useInfraMLModuleConfiguration({
+ sourceConfiguration,
+ moduleDescriptor: metricHostsModule,
+ });
+
+ const { fetchModuleDefinition, getIsJobDefinitionOutdated } = useInfraMLModuleDefinition({
+ sourceConfiguration,
+ moduleDescriptor: metricHostsModule,
+ });
+
+ const hasOutdatedJobConfigurations = useMemo(
+ () => infraMLModule.jobSummaries.some(getIsJobConfigurationOutdated),
+ [getIsJobConfigurationOutdated, infraMLModule.jobSummaries]
+ );
+
+ const hasOutdatedJobDefinitions = useMemo(
+ () => infraMLModule.jobSummaries.some(getIsJobDefinitionOutdated),
+ [getIsJobDefinitionOutdated, infraMLModule.jobSummaries]
+ );
+
+ const hasStoppedJobs = useMemo(
+ () =>
+ Object.values(infraMLModule.jobStatus).some(
+ (currentJobStatus) => currentJobStatus === 'stopped'
+ ),
+ [infraMLModule.jobStatus]
+ );
+
+ return {
+ ...infraMLModule,
+ fetchModuleDefinition,
+ hasOutdatedJobConfigurations,
+ hasOutdatedJobDefinitions,
+ hasStoppedJobs,
+ };
+};
+
+export const [MetricHostsModuleProvider, useMetricHostsModuleContext] = createContainer(
+ useMetricHostsModule
+);
diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts
new file mode 100644
index 0000000000000..cec87fb1144e3
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts
@@ -0,0 +1,126 @@
+/*
+ * 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';
+import { ModuleDescriptor, ModuleSourceConfiguration } from '../../infra_ml_module_types';
+import { cleanUpJobsAndDatafeeds } from '../../infra_ml_cleanup';
+import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api';
+import { callGetMlModuleAPI } from '../../api/ml_get_module';
+import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api';
+import { callValidateIndicesAPI } from '../../../logs/log_analysis/api/validate_indices';
+import { callValidateDatasetsAPI } from '../../../logs/log_analysis/api/validate_datasets';
+import {
+ metricsHostsJobTypes,
+ getJobId,
+ MetricsHostsJobType,
+ DatasetFilter,
+ bucketSpan,
+ partitionField,
+} from '../../../../../common/infra_ml';
+
+const moduleId = 'metrics_ui_hosts';
+const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', {
+ defaultMessage: 'Metrics anomanly detection',
+});
+const moduleDescription = i18n.translate('xpack.infra.ml.metricsHostModuleDescription', {
+ defaultMessage: 'Use Machine Learning to automatically detect anomalous log entry rates.',
+});
+
+const getJobIds = (spaceId: string, sourceId: string) =>
+ metricsHostsJobTypes.reduce(
+ (accumulatedJobIds, jobType) => ({
+ ...accumulatedJobIds,
+ [jobType]: getJobId(spaceId, sourceId, jobType),
+ }),
+ {} as Record
+ );
+
+const getJobSummary = async (spaceId: string, sourceId: string) => {
+ const response = await callJobsSummaryAPI(spaceId, sourceId, metricsHostsJobTypes);
+ const jobIds = Object.values(getJobIds(spaceId, sourceId));
+
+ return response.filter((jobSummary) => jobIds.includes(jobSummary.id));
+};
+
+const getModuleDefinition = async () => {
+ return await callGetMlModuleAPI(moduleId);
+};
+
+const setUpModule = async (
+ start: number | undefined,
+ end: number | undefined,
+ datasetFilter: DatasetFilter,
+ { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration,
+ pField?: string
+) => {
+ const indexNamePattern = indices.join(',');
+ const jobIds = ['hosts_memory_usage', 'hosts_network_in', 'hosts_network_out'];
+ const jobOverrides = jobIds.map((id) => ({
+ job_id: id,
+ data_description: {
+ time_field: timestampField,
+ },
+ custom_settings: {
+ metrics_source_config: {
+ indexPattern: indexNamePattern,
+ timestampField,
+ bucketSpan,
+ },
+ },
+ }));
+
+ return callSetupMlModuleAPI(
+ moduleId,
+ start,
+ end,
+ spaceId,
+ sourceId,
+ indexNamePattern,
+ jobOverrides,
+ []
+ );
+};
+
+const cleanUpModule = async (spaceId: string, sourceId: string) => {
+ return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsHostsJobTypes);
+};
+
+const validateSetupIndices = async (indices: string[], timestampField: string) => {
+ return await callValidateIndicesAPI(indices, [
+ {
+ name: timestampField,
+ validTypes: ['date'],
+ },
+ {
+ name: partitionField,
+ validTypes: ['keyword'],
+ },
+ ]);
+};
+
+const validateSetupDatasets = async (
+ indices: string[],
+ timestampField: string,
+ startTime: number,
+ endTime: number
+) => {
+ return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime);
+};
+
+export const metricHostsModule: ModuleDescriptor = {
+ moduleId,
+ moduleName,
+ moduleDescription,
+ jobTypes: metricsHostsJobTypes,
+ bucketSpan,
+ getJobIds,
+ getJobSummary,
+ getModuleDefinition,
+ setUpModule,
+ cleanUpModule,
+ validateSetupDatasets,
+ validateSetupIndices,
+};
diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx
new file mode 100644
index 0000000000000..07c8ab02f17ee
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx
@@ -0,0 +1,80 @@
+/*
+ * 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 createContainer from 'constate';
+import { useMemo } from 'react';
+import { useInfraMLModule } from '../../infra_ml_module';
+import { useInfraMLModuleConfiguration } from '../../infra_ml_module_configuration';
+import { useInfraMLModuleDefinition } from '../../infra_ml_module_definition';
+import { ModuleSourceConfiguration } from '../../infra_ml_module_types';
+import { metricHostsModule } from './module_descriptor';
+
+export const useMetricK8sModule = ({
+ indexPattern,
+ sourceId,
+ spaceId,
+ timestampField,
+}: {
+ indexPattern: string;
+ sourceId: string;
+ spaceId: string;
+ timestampField: string;
+}) => {
+ const sourceConfiguration: ModuleSourceConfiguration = useMemo(
+ () => ({
+ indices: indexPattern.split(','),
+ sourceId,
+ spaceId,
+ timestampField,
+ }),
+ [indexPattern, sourceId, spaceId, timestampField]
+ );
+
+ const infraMLModule = useInfraMLModule({
+ moduleDescriptor: metricHostsModule,
+ sourceConfiguration,
+ });
+
+ const { getIsJobConfigurationOutdated } = useInfraMLModuleConfiguration({
+ sourceConfiguration,
+ moduleDescriptor: metricHostsModule,
+ });
+
+ const { fetchModuleDefinition, getIsJobDefinitionOutdated } = useInfraMLModuleDefinition({
+ sourceConfiguration,
+ moduleDescriptor: metricHostsModule,
+ });
+
+ const hasOutdatedJobConfigurations = useMemo(
+ () => infraMLModule.jobSummaries.some(getIsJobConfigurationOutdated),
+ [getIsJobConfigurationOutdated, infraMLModule.jobSummaries]
+ );
+
+ const hasOutdatedJobDefinitions = useMemo(
+ () => infraMLModule.jobSummaries.some(getIsJobDefinitionOutdated),
+ [getIsJobDefinitionOutdated, infraMLModule.jobSummaries]
+ );
+
+ const hasStoppedJobs = useMemo(
+ () =>
+ Object.values(infraMLModule.jobStatus).some(
+ (currentJobStatus) => currentJobStatus === 'stopped'
+ ),
+ [infraMLModule.jobStatus]
+ );
+
+ return {
+ ...infraMLModule,
+ fetchModuleDefinition,
+ hasOutdatedJobConfigurations,
+ hasOutdatedJobDefinitions,
+ hasStoppedJobs,
+ };
+};
+
+export const [MetricK8sModuleProvider, useMetricK8sModuleContext] = createContainer(
+ useMetricK8sModule
+);
diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts
new file mode 100644
index 0000000000000..cbcff1c307af6
--- /dev/null
+++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts
@@ -0,0 +1,129 @@
+/*
+ * 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';
+import { ModuleDescriptor, ModuleSourceConfiguration } from '../../infra_ml_module_types';
+import { cleanUpJobsAndDatafeeds } from '../../infra_ml_cleanup';
+import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api';
+import { callGetMlModuleAPI } from '../../api/ml_get_module';
+import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api';
+import { callValidateIndicesAPI } from '../../../logs/log_analysis/api/validate_indices';
+import { callValidateDatasetsAPI } from '../../../logs/log_analysis/api/validate_datasets';
+import {
+ metricsK8SJobTypes,
+ getJobId,
+ MetricK8sJobType,
+ DatasetFilter,
+ bucketSpan,
+ partitionField,
+} from '../../../../../common/infra_ml';
+
+const moduleId = 'metrics_ui_k8s';
+const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', {
+ defaultMessage: 'Metrics anomanly detection',
+});
+const moduleDescription = i18n.translate('xpack.infra.ml.metricsHostModuleDescription', {
+ defaultMessage: 'Use Machine Learning to automatically detect anomalous log entry rates.',
+});
+
+const getJobIds = (spaceId: string, sourceId: string) =>
+ metricsK8SJobTypes.reduce(
+ (accumulatedJobIds, jobType) => ({
+ ...accumulatedJobIds,
+ [jobType]: getJobId(spaceId, sourceId, jobType),
+ }),
+ {} as Record
+ );
+
+const getJobSummary = async (spaceId: string, sourceId: string) => {
+ const response = await callJobsSummaryAPI(spaceId, sourceId, metricsK8SJobTypes);
+ const jobIds = Object.values(getJobIds(spaceId, sourceId));
+
+ return response.filter((jobSummary) => jobIds.includes(jobSummary.id));
+};
+
+const getModuleDefinition = async () => {
+ return await callGetMlModuleAPI(moduleId);
+};
+
+const setUpModule = async (
+ start: number | undefined,
+ end: number | undefined,
+ datasetFilter: DatasetFilter,
+ { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration,
+ pField?: string
+) => {
+ const indexNamePattern = indices.join(',');
+ const jobIds = ['k8s_memory_usage', 'k8s_network_in', 'k8s_network_out'];
+ const jobOverrides = jobIds.map((id) => ({
+ job_id: id,
+ analysis_config: {
+ bucket_span: `${bucketSpan}ms`,
+ },
+ data_description: {
+ time_field: timestampField,
+ },
+ custom_settings: {
+ metrics_source_config: {
+ indexPattern: indexNamePattern,
+ timestampField,
+ bucketSpan,
+ },
+ },
+ }));
+
+ return callSetupMlModuleAPI(
+ moduleId,
+ start,
+ end,
+ spaceId,
+ sourceId,
+ indexNamePattern,
+ jobOverrides,
+ []
+ );
+};
+
+const cleanUpModule = async (spaceId: string, sourceId: string) => {
+ return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsK8SJobTypes);
+};
+
+const validateSetupIndices = async (indices: string[], timestampField: string) => {
+ return await callValidateIndicesAPI(indices, [
+ {
+ name: timestampField,
+ validTypes: ['date'],
+ },
+ {
+ name: partitionField,
+ validTypes: ['keyword'],
+ },
+ ]);
+};
+
+const validateSetupDatasets = async (
+ indices: string[],
+ timestampField: string,
+ startTime: number,
+ endTime: number
+) => {
+ return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime);
+};
+
+export const metricHostsModule: ModuleDescriptor = {
+ moduleId,
+ moduleName,
+ moduleDescription,
+ jobTypes: metricsK8SJobTypes,
+ bucketSpan,
+ getJobIds,
+ getJobSummary,
+ getModuleDefinition,
+ setUpModule,
+ cleanUpModule,
+ validateSetupDatasets,
+ validateSetupIndices,
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx
index 3b3ed80f9e731..ac2c87248ae77 100644
--- a/x-pack/plugins/infra/public/pages/metrics/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx
@@ -38,6 +38,8 @@ import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components
import { SavedView } from '../../containers/saved_view/saved_view';
import { SourceConfigurationFields } from '../../graphql/types';
import { AlertPrefillProvider } from '../../alerting/use_alert_prefill';
+import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities';
+import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout';
const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', {
defaultMessage: 'Add data',
@@ -55,110 +57,118 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => {
-
-
+
+
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
- {ADD_DATA_LABEL}
-
-
-
-
-
-
-
- (
-
- {({ configuration, createDerivedIndexPattern }) => (
-
-
- {configuration ? (
-
- ) : (
-
- )}
-
- )}
-
+ }
)}
- />
-
-
-
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {ADD_DATA_LABEL}
+
+
+
+
+
+
+
+ (
+
+ {({ configuration, createDerivedIndexPattern }) => (
+
+
+ {configuration ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ )}
+ />
+
+
+
+
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx
new file mode 100644
index 0000000000000..b063713fa2c97
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, useCallback } from 'react';
+import { EuiButtonEmpty, EuiFlyout } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { FlyoutHome } from './flyout_home';
+import { JobSetupScreen } from './job_setup_screen';
+import { useInfraMLCapabilities } from '../../../../../../containers/ml/infra_ml_capabilities';
+import { MetricHostsModuleProvider } from '../../../../../../containers/ml/modules/metrics_hosts/module';
+import { MetricK8sModuleProvider } from '../../../../../../containers/ml/modules/metrics_k8s/module';
+import { useSourceViaHttp } from '../../../../../../containers/source/use_source_via_http';
+import { useActiveKibanaSpace } from '../../../../../../hooks/use_kibana_space';
+
+export const AnomalyDetectionFlyout = () => {
+ const { hasInfraMLSetupCapabilities } = useInfraMLCapabilities();
+ const [showFlyout, setShowFlyout] = useState(false);
+ const [screenName, setScreenName] = useState<'home' | 'setup'>('home');
+ const [screenParams, setScreenParams] = useState(null);
+ const { source } = useSourceViaHttp({
+ sourceId: 'default',
+ type: 'metrics',
+ });
+
+ const { space } = useActiveKibanaSpace();
+
+ const openFlyout = useCallback(() => {
+ setScreenName('home');
+ setShowFlyout(true);
+ }, []);
+
+ const openJobSetup = useCallback(
+ (jobType: 'hosts' | 'kubernetes') => {
+ setScreenName('setup');
+ setScreenParams({ jobType });
+ },
+ [setScreenName]
+ );
+
+ const closeFlyout = useCallback(() => {
+ setShowFlyout(false);
+ }, []);
+
+ if (source?.configuration.metricAlias == null || space == null) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+ {showFlyout && (
+
+
+
+ {screenName === 'home' && (
+
+ )}
+ {screenName === 'setup' && (
+
+ )}
+
+
+
+ )}
+ >
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx
new file mode 100644
index 0000000000000..9cf898b684336
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx
@@ -0,0 +1,333 @@
+/*
+ * 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, useEffect } from 'react';
+import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { EuiCallOut } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
+import { EuiButtonEmpty } from '@elastic/eui';
+import moment from 'moment';
+import { useInfraMLCapabilitiesContext } from '../../../../../../containers/ml/infra_ml_capabilities';
+import { SubscriptionSplashContent } from './subscription_splash_content';
+import {
+ MissingResultsPrivilegesPrompt,
+ MissingSetupPrivilegesPrompt,
+} from '../../../../../../components/logging/log_analysis_setup';
+import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module';
+import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module';
+import { LoadingPage } from '../../../../../../components/loading_page';
+import { useLinkProps } from '../../../../../../hooks/use_link_props';
+
+interface Props {
+ hasSetupCapabilities: boolean;
+ goToSetup(type: 'hosts' | 'kubernetes'): void;
+}
+
+export const FlyoutHome = (props: Props) => {
+ const [tab, setTab] = useState<'jobs' | 'anomalies'>('jobs');
+ const { goToSetup } = props;
+ const {
+ fetchJobStatus: fetchHostJobStatus,
+ setupStatus: hostSetupStatus,
+ jobSummaries: hostJobSummaries,
+ } = useMetricHostsModuleContext();
+ const {
+ fetchJobStatus: fetchK8sJobStatus,
+ setupStatus: k8sSetupStatus,
+ jobSummaries: k8sJobSummaries,
+ } = useMetricK8sModuleContext();
+ const {
+ hasInfraMLCapabilites,
+ hasInfraMLReadCapabilities,
+ hasInfraMLSetupCapabilities,
+ } = useInfraMLCapabilitiesContext();
+
+ const createHosts = useCallback(() => {
+ goToSetup('hosts');
+ }, [goToSetup]);
+
+ const createK8s = useCallback(() => {
+ goToSetup('kubernetes');
+ }, [goToSetup]);
+
+ const goToJobs = useCallback(() => {
+ setTab('jobs');
+ }, []);
+
+ const jobIds = [
+ ...(k8sJobSummaries || []).map((k) => k.id),
+ ...(hostJobSummaries || []).map((h) => h.id),
+ ];
+ const anomaliesUrl = useLinkProps({
+ app: 'ml',
+ pathname: `/explorer?_g=${createResultsUrl(jobIds)}`,
+ });
+
+ useEffect(() => {
+ if (hasInfraMLReadCapabilities) {
+ fetchHostJobStatus();
+ fetchK8sJobStatus();
+ }
+ }, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]);
+
+ if (!hasInfraMLCapabilites) {
+ return ;
+ } else if (!hasInfraMLReadCapabilities) {
+ return ;
+ } else if (hostSetupStatus.type === 'initializing' || k8sSetupStatus.type === 'initializing') {
+ return (
+
+ );
+ } else if (!hasInfraMLSetupCapabilities) {
+ return ;
+ } else {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {hostJobSummaries.length > 0 && (
+ <>
+ 0}
+ hasK8sJobs={k8sJobSummaries.length > 0}
+ />
+
+ >
+ )}
+ {tab === 'jobs' && (
+ 0}
+ hasK8sJobs={k8sJobSummaries.length > 0}
+ hasSetupCapabilities={props.hasSetupCapabilities}
+ createHosts={createHosts}
+ createK8s={createK8s}
+ />
+ )}
+
+ >
+ );
+ }
+};
+
+interface CalloutProps {
+ hasHostJobs: boolean;
+ hasK8sJobs: boolean;
+}
+const JobsEnabledCallout = (props: CalloutProps) => {
+ let target = '';
+ if (props.hasHostJobs && props.hasK8sJobs) {
+ target = `${i18n.translate('xpack.infra.ml.anomalyFlyout.create.hostTitle', {
+ defaultMessage: 'Hosts',
+ })} and ${i18n.translate('xpack.infra.ml.anomalyFlyout.create.k8sSuccessTitle', {
+ defaultMessage: 'Kubernetes',
+ })}`;
+ } else if (props.hasHostJobs) {
+ target = i18n.translate('xpack.infra.ml.anomalyFlyout.create.hostSuccessTitle', {
+ defaultMessage: 'Hosts',
+ });
+ } else if (props.hasK8sJobs) {
+ target = i18n.translate('xpack.infra.ml.anomalyFlyout.create.k8sSuccessTitle', {
+ defaultMessage: 'Kubernetes',
+ });
+ }
+
+ const manageJobsLinkProps = useLinkProps({
+ app: 'ml',
+ pathname: '/jobs',
+ });
+
+ return (
+ <>
+
+ }
+ iconType="check"
+ />
+
+
+
+
+ >
+ );
+};
+
+interface CreateJobTab {
+ hasSetupCapabilities: boolean;
+ hasHostJobs: boolean;
+ hasK8sJobs: boolean;
+ createHosts(): void;
+ createK8s(): void;
+}
+
+const CreateJobTab = (props: CreateJobTab) => {
+ return (
+ <>
+
+
+
+
+
+ }
+ // title="Hosts"
+ title={
+
+ }
+ description={
+
+ }
+ footer={
+ <>
+ {props.hasHostJobs && (
+
+
+
+ )}
+ {!props.hasHostJobs && (
+
+
+
+ )}
+ >
+ }
+ />
+
+
+ }
+ title={
+
+ }
+ description={
+
+ }
+ footer={
+ <>
+ {props.hasK8sJobs && (
+
+
+
+ )}
+ {!props.hasK8sJobs && (
+
+
+
+ )}
+ >
+ }
+ />
+
+
+ >
+ );
+};
+
+function createResultsUrl(jobIds: string[], mode = 'absolute') {
+ const idString = jobIds.map((j) => `'${j}'`).join(',');
+ let path = '';
+
+ const from = moment().subtract(4, 'weeks').toISOString();
+ const to = moment().toISOString();
+
+ path += `(ml:(jobIds:!(${idString}))`;
+ path += `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${from}'`;
+ path += `,to:'${to}'`;
+ if (mode === 'invalid') {
+ path += `,mode:invalid`;
+ }
+ path += "))&_a=(query:(query_string:(analyze_wildcard:!t,query:'*')))";
+
+ return path;
+}
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx
new file mode 100644
index 0000000000000..730cd7b6e9ef5
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.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, { useState, useCallback, useMemo, useEffect } from 'react';
+import { EuiForm, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui';
+import { EuiText, EuiSpacer } from '@elastic/eui';
+import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFlyoutFooter } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
+import moment, { Moment } from 'moment';
+import { EuiComboBox } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { EuiLoadingSpinner } from '@elastic/eui';
+import { useSourceViaHttp } from '../../../../../../containers/source/use_source_via_http';
+import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module';
+import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module';
+import { FixedDatePicker } from '../../../../../../components/fixed_datepicker';
+
+interface Props {
+ jobType: 'hosts' | 'kubernetes';
+ closeFlyout(): void;
+ goHome(): void;
+}
+
+export const JobSetupScreen = (props: Props) => {
+ const [now] = useState(() => moment());
+ const { goHome } = props;
+ const [startDate, setStartDate] = useState(now.clone().subtract(4, 'weeks'));
+ const [partitionField, setPartitionField] = useState(null);
+ const h = useMetricHostsModuleContext();
+ const k = useMetricK8sModuleContext();
+ const { createDerivedIndexPattern } = useSourceViaHttp({
+ sourceId: 'default',
+ type: 'metrics',
+ });
+
+ const indicies = h.sourceConfiguration.indices;
+
+ const setupStatus = useMemo(() => {
+ if (props.jobType === 'kubernetes') {
+ return k.setupStatus;
+ } else {
+ return h.setupStatus;
+ }
+ }, [props.jobType, k.setupStatus, h.setupStatus]);
+
+ const cleanUpAndSetUpModule = useMemo(() => {
+ if (props.jobType === 'kubernetes') {
+ return k.cleanUpAndSetUpModule;
+ } else {
+ return h.cleanUpAndSetUpModule;
+ }
+ }, [props.jobType, k.cleanUpAndSetUpModule, h.cleanUpAndSetUpModule]);
+
+ const setUpModule = useMemo(() => {
+ if (props.jobType === 'kubernetes') {
+ return k.setUpModule;
+ } else {
+ return h.setUpModule;
+ }
+ }, [props.jobType, k.setUpModule, h.setUpModule]);
+
+ const hasSummaries = useMemo(() => {
+ if (props.jobType === 'kubernetes') {
+ return k.jobSummaries.length > 0;
+ } else {
+ return h.jobSummaries.length > 0;
+ }
+ }, [props.jobType, k.jobSummaries, h.jobSummaries]);
+
+ const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
+ createDerivedIndexPattern,
+ ]);
+
+ const updateStart = useCallback((date: Moment) => {
+ setStartDate(date);
+ }, []);
+
+ const createJobs = useCallback(() => {
+ if (hasSummaries) {
+ cleanUpAndSetUpModule(
+ indicies,
+ moment(startDate).toDate().getTime(),
+ undefined,
+ { type: 'includeAll' },
+ partitionField ? partitionField[0] : undefined
+ );
+ } else {
+ setUpModule(
+ indicies,
+ moment(startDate).toDate().getTime(),
+ undefined,
+ { type: 'includeAll' },
+ partitionField ? partitionField[0] : undefined
+ );
+ }
+ }, [cleanUpAndSetUpModule, setUpModule, hasSummaries, indicies, partitionField, startDate]);
+
+ const onPartitionFieldChange = useCallback((value: Array<{ label: string }>) => {
+ setPartitionField(value.map((v) => v.label));
+ }, []);
+
+ useEffect(() => {
+ if (props.jobType === 'kubernetes') {
+ setPartitionField(['kubernetes.namespace']);
+ }
+ }, [props.jobType]);
+
+ useEffect(() => {
+ if (setupStatus.type === 'succeeded') {
+ goHome();
+ }
+ }, [setupStatus, goHome]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {setupStatus.type === 'pending' ? (
+
+
+
+
+
+
+
+
+ ) : setupStatus.type === 'failed' ? (
+ <>
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ }
+ description={
+
+ }
+ >
+
+ }
+ >
+
+
+
+
+
+
+
+ }
+ description={
+
+ }
+ >
+
+ }
+ compressed
+ >
+ ({ label: p })) : undefined
+ }
+ options={derivedIndexPattern.fields
+ .filter((f) => f.aggregatable && f.type === 'string')
+ .map((f) => ({ label: f.name }))}
+ onChange={onPartitionFieldChange}
+ isClearable={true}
+ />
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx
new file mode 100644
index 0000000000000..f07c37f5e7ea2
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx
@@ -0,0 +1,172 @@
+/*
+ * 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, { useEffect } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiSpacer,
+ EuiTitle,
+ EuiText,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiImage,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { LoadingPage } from '../../../../../../components/loading_page';
+import { useTrialStatus } from '../../../../../../hooks/use_trial_status';
+import { useKibana } from '../../../../../../../../../../src/plugins/kibana_react/public';
+import { euiStyled } from '../../../../../../../../observability/public';
+import { HttpStart } from '../../../../../../../../../../src/core/public';
+
+export const SubscriptionSplashContent: React.FC = () => {
+ const { services } = useKibana<{ http: HttpStart }>();
+ const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus();
+
+ useEffect(() => {
+ checkTrialAvailability();
+ }, [checkTrialAvailability]);
+
+ if (loadState === 'pending') {
+ return (
+
+ );
+ }
+
+ const canStartTrial = isTrialAvailable && loadState === 'resolved';
+
+ let title;
+ let description;
+ let cta;
+
+ if (canStartTrial) {
+ title = (
+
+ );
+
+ description = (
+
+ );
+
+ cta = (
+
+
+
+ );
+ } else {
+ title = (
+
+ );
+
+ description = (
+
+ );
+
+ cta = (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+
+ {description}
+
+
+ {cta}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const SubscriptionPage = euiStyled(EuiPage)`
+ height: 100%
+`;
+
+const SubscriptionPageContent = euiStyled(EuiPageContent)`
+ max-width: 768px !important;
+`;
+
+const SubscriptionPageFooter = euiStyled.div`
+ background: ${(props) => props.theme.eui.euiColorLightestShade};
+ margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) =>
+ props.theme.eui.paddingSizes.l};
+ padding: ${(props) => props.theme.eui.paddingSizes.l};
+`;
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx
index 6e031c8396f07..dbbfb0f49c0e9 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx
@@ -22,7 +22,7 @@ export const IntervalLabel = ({ intervalAsString }: Props) => {
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts
new file mode 100644
index 0000000000000..f755057d0b76d
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts
@@ -0,0 +1,318 @@
+/*
+ * 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 { useMemo, useState, useCallback, useEffect, useReducer } from 'react';
+import {
+ INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH,
+ Sort,
+ Pagination,
+ PaginationCursor,
+ getMetricsHostsAnomaliesRequestPayloadRT,
+ MetricsHostsAnomaly,
+ getMetricsHostsAnomaliesSuccessReponsePayloadRT,
+} from '../../../../../common/http_api/infra_ml';
+import { useTrackedPromise } from '../../../../utils/use_tracked_promise';
+import { npStart } from '../../../../legacy_singletons';
+import { decodeOrThrow } from '../../../../../common/runtime_types';
+
+export type SortOptions = Sort;
+export type PaginationOptions = Pick;
+export type Page = number;
+export type FetchNextPage = () => void;
+export type FetchPreviousPage = () => void;
+export type ChangeSortOptions = (sortOptions: Sort) => void;
+export type ChangePaginationOptions = (paginationOptions: PaginationOptions) => void;
+export type MetricsHostsAnomalies = MetricsHostsAnomaly[];
+interface PaginationCursors {
+ previousPageCursor: PaginationCursor;
+ nextPageCursor: PaginationCursor;
+}
+
+interface ReducerState {
+ page: number;
+ lastReceivedCursors: PaginationCursors | undefined;
+ paginationCursor: Pagination['cursor'] | undefined;
+ hasNextPage: boolean;
+ paginationOptions: PaginationOptions;
+ sortOptions: Sort;
+ timeRange: {
+ start: number;
+ end: number;
+ };
+ filteredDatasets?: string[];
+}
+
+type ReducerStateDefaults = Pick<
+ ReducerState,
+ 'page' | 'lastReceivedCursors' | 'paginationCursor' | 'hasNextPage'
+>;
+
+type ReducerAction =
+ | { type: 'changePaginationOptions'; payload: { paginationOptions: PaginationOptions } }
+ | { type: 'changeSortOptions'; payload: { sortOptions: Sort } }
+ | { type: 'fetchNextPage' }
+ | { type: 'fetchPreviousPage' }
+ | { type: 'changeHasNextPage'; payload: { hasNextPage: boolean } }
+ | { type: 'changeLastReceivedCursors'; payload: { lastReceivedCursors: PaginationCursors } }
+ | { type: 'changeTimeRange'; payload: { timeRange: { start: number; end: number } } }
+ | { type: 'changeFilteredDatasets'; payload: { filteredDatasets?: string[] } };
+
+const stateReducer = (state: ReducerState, action: ReducerAction): ReducerState => {
+ const resetPagination = {
+ page: 1,
+ paginationCursor: undefined,
+ };
+ switch (action.type) {
+ case 'changePaginationOptions':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ case 'changeSortOptions':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ case 'changeHasNextPage':
+ return {
+ ...state,
+ ...action.payload,
+ };
+ case 'changeLastReceivedCursors':
+ return {
+ ...state,
+ ...action.payload,
+ };
+ case 'fetchNextPage':
+ return state.lastReceivedCursors
+ ? {
+ ...state,
+ page: state.page + 1,
+ paginationCursor: { searchAfter: state.lastReceivedCursors.nextPageCursor },
+ }
+ : state;
+ case 'fetchPreviousPage':
+ return state.lastReceivedCursors
+ ? {
+ ...state,
+ page: state.page - 1,
+ paginationCursor: { searchBefore: state.lastReceivedCursors.previousPageCursor },
+ }
+ : state;
+ case 'changeTimeRange':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ case 'changeFilteredDatasets':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ default:
+ return state;
+ }
+};
+
+const STATE_DEFAULTS: ReducerStateDefaults = {
+ // NOTE: This piece of state is purely for the client side, it could be extracted out of the hook.
+ page: 1,
+ // Cursor from the last request
+ lastReceivedCursors: undefined,
+ // Cursor to use for the next request. For the first request, and therefore not paging, this will be undefined.
+ paginationCursor: undefined,
+ hasNextPage: false,
+};
+
+export const useMetricsHostsAnomaliesResults = ({
+ endTime,
+ startTime,
+ sourceId,
+ defaultSortOptions,
+ defaultPaginationOptions,
+ onGetMetricsHostsAnomaliesDatasetsError,
+ filteredDatasets,
+}: {
+ endTime: number;
+ startTime: number;
+ sourceId: string;
+ defaultSortOptions: Sort;
+ defaultPaginationOptions: Pick;
+ onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void;
+ filteredDatasets?: string[];
+}) => {
+ const initStateReducer = (stateDefaults: ReducerStateDefaults): ReducerState => {
+ return {
+ ...stateDefaults,
+ paginationOptions: defaultPaginationOptions,
+ sortOptions: defaultSortOptions,
+ filteredDatasets,
+ timeRange: {
+ start: startTime,
+ end: endTime,
+ },
+ };
+ };
+
+ const [reducerState, dispatch] = useReducer(stateReducer, STATE_DEFAULTS, initStateReducer);
+
+ const [metricsHostsAnomalies, setMetricsHostsAnomalies] = useState([]);
+
+ const [getMetricsHostsAnomaliesRequest, getMetricsHostsAnomalies] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'creation',
+ createPromise: async () => {
+ const {
+ timeRange: { start: queryStartTime, end: queryEndTime },
+ sortOptions,
+ paginationOptions,
+ paginationCursor,
+ } = reducerState;
+ return await callGetMetricHostsAnomaliesAPI(
+ sourceId,
+ queryStartTime,
+ queryEndTime,
+ sortOptions,
+ {
+ ...paginationOptions,
+ cursor: paginationCursor,
+ }
+ );
+ },
+ onResolve: ({ data: { anomalies, paginationCursors: requestCursors, hasMoreEntries } }) => {
+ const { paginationCursor } = reducerState;
+ if (requestCursors) {
+ dispatch({
+ type: 'changeLastReceivedCursors',
+ payload: { lastReceivedCursors: requestCursors },
+ });
+ }
+ // Check if we have more "next" entries. "Page" covers the "previous" scenario,
+ // since we need to know the page we're on anyway.
+ if (!paginationCursor || (paginationCursor && 'searchAfter' in paginationCursor)) {
+ dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: hasMoreEntries } });
+ } else if (paginationCursor && 'searchBefore' in paginationCursor) {
+ // We've requested a previous page, therefore there is a next page.
+ dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: true } });
+ }
+ setMetricsHostsAnomalies(anomalies);
+ },
+ },
+ [
+ sourceId,
+ dispatch,
+ reducerState.timeRange,
+ reducerState.sortOptions,
+ reducerState.paginationOptions,
+ reducerState.paginationCursor,
+ reducerState.filteredDatasets,
+ ]
+ );
+
+ const changeSortOptions = useCallback(
+ (nextSortOptions: Sort) => {
+ dispatch({ type: 'changeSortOptions', payload: { sortOptions: nextSortOptions } });
+ },
+ [dispatch]
+ );
+
+ const changePaginationOptions = useCallback(
+ (nextPaginationOptions: PaginationOptions) => {
+ dispatch({
+ type: 'changePaginationOptions',
+ payload: { paginationOptions: nextPaginationOptions },
+ });
+ },
+ [dispatch]
+ );
+
+ // Time range has changed
+ useEffect(() => {
+ dispatch({
+ type: 'changeTimeRange',
+ payload: { timeRange: { start: startTime, end: endTime } },
+ });
+ }, [startTime, endTime]);
+
+ // Selected datasets have changed
+ useEffect(() => {
+ dispatch({
+ type: 'changeFilteredDatasets',
+ payload: { filteredDatasets },
+ });
+ }, [filteredDatasets]);
+
+ useEffect(() => {
+ getMetricsHostsAnomalies();
+ }, [getMetricsHostsAnomalies]); // TODO: FIgure out the deps here.
+
+ const handleFetchNextPage = useCallback(() => {
+ if (reducerState.lastReceivedCursors) {
+ dispatch({ type: 'fetchNextPage' });
+ }
+ }, [dispatch, reducerState]);
+
+ const handleFetchPreviousPage = useCallback(() => {
+ if (reducerState.lastReceivedCursors) {
+ dispatch({ type: 'fetchPreviousPage' });
+ }
+ }, [dispatch, reducerState]);
+
+ const isLoadingMetricsHostsAnomalies = useMemo(
+ () => getMetricsHostsAnomaliesRequest.state === 'pending',
+ [getMetricsHostsAnomaliesRequest.state]
+ );
+
+ const hasFailedLoadingMetricsHostsAnomalies = useMemo(
+ () => getMetricsHostsAnomaliesRequest.state === 'rejected',
+ [getMetricsHostsAnomaliesRequest.state]
+ );
+
+ return {
+ metricsHostsAnomalies,
+ getMetricsHostsAnomalies,
+ isLoadingMetricsHostsAnomalies,
+ hasFailedLoadingMetricsHostsAnomalies,
+ changeSortOptions,
+ sortOptions: reducerState.sortOptions,
+ changePaginationOptions,
+ paginationOptions: reducerState.paginationOptions,
+ fetchPreviousPage: reducerState.page > 1 ? handleFetchPreviousPage : undefined,
+ fetchNextPage: reducerState.hasNextPage ? handleFetchNextPage : undefined,
+ page: reducerState.page,
+ };
+};
+
+export const callGetMetricHostsAnomaliesAPI = async (
+ sourceId: string,
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) => {
+ const response = await npStart.http.fetch(INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, {
+ method: 'POST',
+ body: JSON.stringify(
+ getMetricsHostsAnomaliesRequestPayloadRT.encode({
+ data: {
+ sourceId,
+ timeRange: {
+ startTime,
+ endTime,
+ },
+ sort,
+ pagination,
+ },
+ })
+ ),
+ });
+
+ return decodeOrThrow(getMetricsHostsAnomaliesSuccessReponsePayloadRT)(response);
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts
new file mode 100644
index 0000000000000..4a7b78e1fdf92
--- /dev/null
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts
@@ -0,0 +1,322 @@
+/*
+ * 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 { useMemo, useState, useCallback, useEffect, useReducer } from 'react';
+import {
+ Sort,
+ Pagination,
+ PaginationCursor,
+ INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH,
+ getMetricsK8sAnomaliesSuccessReponsePayloadRT,
+ getMetricsK8sAnomaliesRequestPayloadRT,
+ MetricsK8sAnomaly,
+} from '../../../../../common/http_api/infra_ml';
+import { useTrackedPromise } from '../../../../utils/use_tracked_promise';
+import { npStart } from '../../../../legacy_singletons';
+import { decodeOrThrow } from '../../../../../common/runtime_types';
+
+export type SortOptions = Sort;
+export type PaginationOptions = Pick;
+export type Page = number;
+export type FetchNextPage = () => void;
+export type FetchPreviousPage = () => void;
+export type ChangeSortOptions = (sortOptions: Sort) => void;
+export type ChangePaginationOptions = (paginationOptions: PaginationOptions) => void;
+export type MetricsK8sAnomalies = MetricsK8sAnomaly[];
+interface PaginationCursors {
+ previousPageCursor: PaginationCursor;
+ nextPageCursor: PaginationCursor;
+}
+
+interface ReducerState {
+ page: number;
+ lastReceivedCursors: PaginationCursors | undefined;
+ paginationCursor: Pagination['cursor'] | undefined;
+ hasNextPage: boolean;
+ paginationOptions: PaginationOptions;
+ sortOptions: Sort;
+ timeRange: {
+ start: number;
+ end: number;
+ };
+ filteredDatasets?: string[];
+}
+
+type ReducerStateDefaults = Pick<
+ ReducerState,
+ 'page' | 'lastReceivedCursors' | 'paginationCursor' | 'hasNextPage'
+>;
+
+type ReducerAction =
+ | { type: 'changePaginationOptions'; payload: { paginationOptions: PaginationOptions } }
+ | { type: 'changeSortOptions'; payload: { sortOptions: Sort } }
+ | { type: 'fetchNextPage' }
+ | { type: 'fetchPreviousPage' }
+ | { type: 'changeHasNextPage'; payload: { hasNextPage: boolean } }
+ | { type: 'changeLastReceivedCursors'; payload: { lastReceivedCursors: PaginationCursors } }
+ | { type: 'changeTimeRange'; payload: { timeRange: { start: number; end: number } } }
+ | { type: 'changeFilteredDatasets'; payload: { filteredDatasets?: string[] } };
+
+const stateReducer = (state: ReducerState, action: ReducerAction): ReducerState => {
+ const resetPagination = {
+ page: 1,
+ paginationCursor: undefined,
+ };
+ switch (action.type) {
+ case 'changePaginationOptions':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ case 'changeSortOptions':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ case 'changeHasNextPage':
+ return {
+ ...state,
+ ...action.payload,
+ };
+ case 'changeLastReceivedCursors':
+ return {
+ ...state,
+ ...action.payload,
+ };
+ case 'fetchNextPage':
+ return state.lastReceivedCursors
+ ? {
+ ...state,
+ page: state.page + 1,
+ paginationCursor: { searchAfter: state.lastReceivedCursors.nextPageCursor },
+ }
+ : state;
+ case 'fetchPreviousPage':
+ return state.lastReceivedCursors
+ ? {
+ ...state,
+ page: state.page - 1,
+ paginationCursor: { searchBefore: state.lastReceivedCursors.previousPageCursor },
+ }
+ : state;
+ case 'changeTimeRange':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ case 'changeFilteredDatasets':
+ return {
+ ...state,
+ ...resetPagination,
+ ...action.payload,
+ };
+ default:
+ return state;
+ }
+};
+
+const STATE_DEFAULTS: ReducerStateDefaults = {
+ // NOTE: This piece of state is purely for the client side, it could be extracted out of the hook.
+ page: 1,
+ // Cursor from the last request
+ lastReceivedCursors: undefined,
+ // Cursor to use for the next request. For the first request, and therefore not paging, this will be undefined.
+ paginationCursor: undefined,
+ hasNextPage: false,
+};
+
+export const useMetricsK8sAnomaliesResults = ({
+ endTime,
+ startTime,
+ sourceId,
+ defaultSortOptions,
+ defaultPaginationOptions,
+ onGetMetricsHostsAnomaliesDatasetsError,
+ filteredDatasets,
+}: {
+ endTime: number;
+ startTime: number;
+ sourceId: string;
+ defaultSortOptions: Sort;
+ defaultPaginationOptions: Pick;
+ onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void;
+ filteredDatasets?: string[];
+}) => {
+ const initStateReducer = (stateDefaults: ReducerStateDefaults): ReducerState => {
+ return {
+ ...stateDefaults,
+ paginationOptions: defaultPaginationOptions,
+ sortOptions: defaultSortOptions,
+ filteredDatasets,
+ timeRange: {
+ start: startTime,
+ end: endTime,
+ },
+ };
+ };
+
+ const [reducerState, dispatch] = useReducer(stateReducer, STATE_DEFAULTS, initStateReducer);
+
+ const [metricsK8sAnomalies, setMetricsK8sAnomalies] = useState([]);
+
+ const [getMetricsK8sAnomaliesRequest, getMetricsK8sAnomalies] = useTrackedPromise(
+ {
+ cancelPreviousOn: 'creation',
+ createPromise: async () => {
+ const {
+ timeRange: { start: queryStartTime, end: queryEndTime },
+ sortOptions,
+ paginationOptions,
+ paginationCursor,
+ filteredDatasets: queryFilteredDatasets,
+ } = reducerState;
+ return await callGetMetricsK8sAnomaliesAPI(
+ sourceId,
+ queryStartTime,
+ queryEndTime,
+ sortOptions,
+ {
+ ...paginationOptions,
+ cursor: paginationCursor,
+ },
+ queryFilteredDatasets
+ );
+ },
+ onResolve: ({ data: { anomalies, paginationCursors: requestCursors, hasMoreEntries } }) => {
+ const { paginationCursor } = reducerState;
+ if (requestCursors) {
+ dispatch({
+ type: 'changeLastReceivedCursors',
+ payload: { lastReceivedCursors: requestCursors },
+ });
+ }
+ // Check if we have more "next" entries. "Page" covers the "previous" scenario,
+ // since we need to know the page we're on anyway.
+ if (!paginationCursor || (paginationCursor && 'searchAfter' in paginationCursor)) {
+ dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: hasMoreEntries } });
+ } else if (paginationCursor && 'searchBefore' in paginationCursor) {
+ // We've requested a previous page, therefore there is a next page.
+ dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: true } });
+ }
+ setMetricsK8sAnomalies(anomalies);
+ },
+ },
+ [
+ sourceId,
+ dispatch,
+ reducerState.timeRange,
+ reducerState.sortOptions,
+ reducerState.paginationOptions,
+ reducerState.paginationCursor,
+ reducerState.filteredDatasets,
+ ]
+ );
+
+ const changeSortOptions = useCallback(
+ (nextSortOptions: Sort) => {
+ dispatch({ type: 'changeSortOptions', payload: { sortOptions: nextSortOptions } });
+ },
+ [dispatch]
+ );
+
+ const changePaginationOptions = useCallback(
+ (nextPaginationOptions: PaginationOptions) => {
+ dispatch({
+ type: 'changePaginationOptions',
+ payload: { paginationOptions: nextPaginationOptions },
+ });
+ },
+ [dispatch]
+ );
+
+ // Time range has changed
+ useEffect(() => {
+ dispatch({
+ type: 'changeTimeRange',
+ payload: { timeRange: { start: startTime, end: endTime } },
+ });
+ }, [startTime, endTime]);
+
+ // Selected datasets have changed
+ useEffect(() => {
+ dispatch({
+ type: 'changeFilteredDatasets',
+ payload: { filteredDatasets },
+ });
+ }, [filteredDatasets]);
+
+ useEffect(() => {
+ getMetricsK8sAnomalies();
+ }, [getMetricsK8sAnomalies]);
+
+ const handleFetchNextPage = useCallback(() => {
+ if (reducerState.lastReceivedCursors) {
+ dispatch({ type: 'fetchNextPage' });
+ }
+ }, [dispatch, reducerState]);
+
+ const handleFetchPreviousPage = useCallback(() => {
+ if (reducerState.lastReceivedCursors) {
+ dispatch({ type: 'fetchPreviousPage' });
+ }
+ }, [dispatch, reducerState]);
+
+ const isLoadingMetricsK8sAnomalies = useMemo(
+ () => getMetricsK8sAnomaliesRequest.state === 'pending',
+ [getMetricsK8sAnomaliesRequest.state]
+ );
+
+ const hasFailedLoadingMetricsK8sAnomalies = useMemo(
+ () => getMetricsK8sAnomaliesRequest.state === 'rejected',
+ [getMetricsK8sAnomaliesRequest.state]
+ );
+
+ return {
+ metricsK8sAnomalies,
+ getMetricsK8sAnomalies,
+ isLoadingMetricsK8sAnomalies,
+ hasFailedLoadingMetricsK8sAnomalies,
+ changeSortOptions,
+ sortOptions: reducerState.sortOptions,
+ changePaginationOptions,
+ paginationOptions: reducerState.paginationOptions,
+ fetchPreviousPage: reducerState.page > 1 ? handleFetchPreviousPage : undefined,
+ fetchNextPage: reducerState.hasNextPage ? handleFetchNextPage : undefined,
+ page: reducerState.page,
+ };
+};
+
+export const callGetMetricsK8sAnomaliesAPI = async (
+ sourceId: string,
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination,
+ datasets?: string[]
+) => {
+ const response = await npStart.http.fetch(INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH, {
+ method: 'POST',
+ body: JSON.stringify(
+ getMetricsK8sAnomaliesRequestPayloadRT.encode({
+ data: {
+ sourceId,
+ timeRange: {
+ startTime,
+ endTime,
+ },
+ sort,
+ pagination,
+ datasets,
+ },
+ })
+ ),
+ });
+
+ return decodeOrThrow(getMetricsK8sAnomaliesSuccessReponsePayloadRT)(response);
+};
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts
index 650eda0362d9e..acf9011ac7ddd 100644
--- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts
+++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts
@@ -28,21 +28,28 @@ const ONE_MINUTE = 60;
const ONE_HOUR = ONE_MINUTE * 60;
const ONE_DAY = ONE_HOUR * 24;
const ONE_WEEK = ONE_DAY * 7;
+const ONE_MONTH = ONE_DAY * 30;
+
+const getDisplayInterval = (interval: string | undefined) => {
+ if (interval) {
+ const intervalInSeconds = getIntervalInSeconds(interval);
+ if (intervalInSeconds < 300) return '5m';
+ }
+ return interval;
+};
const getTimeLengthFromInterval = (interval: string | undefined) => {
if (interval) {
const intervalInSeconds = getIntervalInSeconds(interval);
- const multiplier =
- intervalInSeconds < ONE_MINUTE
- ? ONE_HOUR / intervalInSeconds
- : intervalInSeconds < ONE_HOUR
- ? 60
- : intervalInSeconds < ONE_DAY
- ? 7
- : intervalInSeconds < ONE_WEEK
- ? 30
- : 1;
- const timeLength = intervalInSeconds * multiplier;
+ // Get up to 288 datapoints based on interval
+ const timeLength =
+ intervalInSeconds <= ONE_MINUTE * 15
+ ? ONE_DAY
+ : intervalInSeconds <= ONE_MINUTE * 35
+ ? ONE_DAY * 3
+ : intervalInSeconds <= ONE_HOUR * 2.5
+ ? ONE_WEEK
+ : ONE_MONTH;
return { timeLength, intervalInSeconds };
} else {
return { timeLength: 0, intervalInSeconds: 0 };
@@ -67,15 +74,19 @@ export function useTimeline(
);
};
- const timeLengthResult = useMemo(() => getTimeLengthFromInterval(interval), [interval]);
+ const displayInterval = useMemo(() => getDisplayInterval(interval), [interval]);
+
+ const timeLengthResult = useMemo(() => getTimeLengthFromInterval(displayInterval), [
+ displayInterval,
+ ]);
const { timeLength, intervalInSeconds } = timeLengthResult;
const timerange: InfraTimerangeInput = {
- interval: interval ?? '',
+ interval: displayInterval ?? '',
to: currentTime + intervalInSeconds * 1000,
from: currentTime - timeLength * 1000,
- lookbackSize: 0,
ignoreLookback: true,
+ forceInterval: true,
};
const { error, loading, response, makeRequest } = useHTTPRequest(
diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts
index a72e40e25b479..206fffdd2e188 100644
--- a/x-pack/plugins/infra/server/infra_server.ts
+++ b/x-pack/plugins/infra/server/infra_server.ts
@@ -21,6 +21,8 @@ import {
initGetLogEntryAnomaliesRoute,
initGetLogEntryAnomaliesDatasetsRoute,
} from './routes/log_analysis';
+import { initGetK8sAnomaliesRoute } from './routes/infra_ml';
+import { initGetHostsAnomaliesRoute } from './routes/infra_ml';
import { initMetricExplorerRoute } from './routes/metrics_explorer';
import { initMetadataRoute } from './routes/metadata';
import { initSnapshotRoute } from './routes/snapshot';
@@ -56,6 +58,8 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initGetLogEntryRateRoute(libs);
initGetLogEntryAnomaliesRoute(libs);
initGetLogEntryAnomaliesDatasetsRoute(libs);
+ initGetK8sAnomaliesRoute(libs);
+ initGetHostsAnomaliesRoute(libs);
initSnapshotRoute(libs);
initNodeDetailsRoute(libs);
initSourceRoute(libs);
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/common.ts
new file mode 100644
index 0000000000000..4d2be94c7cd62
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/common.ts
@@ -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 type { MlAnomalyDetectors, MlSystem } from '../../types';
+import { NoLogAnalysisMlJobError } from './errors';
+
+import {
+ CompositeDatasetKey,
+ createLogEntryDatasetsQuery,
+ LogEntryDatasetBucket,
+ logEntryDatasetsResponseRT,
+} from './queries/log_entry_data_sets';
+import { decodeOrThrow } from '../../../common/runtime_types';
+import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing';
+
+export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) {
+ const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES');
+ const {
+ jobs: [mlJob],
+ } = await mlAnomalyDetectors.jobs(jobId);
+
+ const mlGetJobSpan = finalizeMlGetJobSpan();
+
+ if (mlJob == null) {
+ throw new NoLogAnalysisMlJobError(`Failed to find ml job ${jobId}.`);
+ }
+
+ return {
+ mlJob,
+ timing: {
+ spans: [mlGetJobSpan],
+ },
+ };
+}
+
+const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000;
+
+// Finds datasets related to ML job ids
+export async function getLogEntryDatasets(
+ mlSystem: MlSystem,
+ startTime: number,
+ endTime: number,
+ jobIds: string[]
+) {
+ const finalizeLogEntryDatasetsSpan = startTracingSpan('get data sets');
+
+ let logEntryDatasetBuckets: LogEntryDatasetBucket[] = [];
+ let afterLatestBatchKey: CompositeDatasetKey | undefined;
+ let esSearchSpans: TracingSpan[] = [];
+
+ while (true) {
+ const finalizeEsSearchSpan = startTracingSpan('fetch log entry dataset batch from ES');
+
+ const logEntryDatasetsResponse = decodeOrThrow(logEntryDatasetsResponseRT)(
+ await mlSystem.mlAnomalySearch(
+ createLogEntryDatasetsQuery(
+ jobIds,
+ startTime,
+ endTime,
+ COMPOSITE_AGGREGATION_BATCH_SIZE,
+ afterLatestBatchKey
+ )
+ )
+ );
+
+ const { after_key: afterKey, buckets: latestBatchBuckets = [] } =
+ logEntryDatasetsResponse.aggregations?.dataset_buckets ?? {};
+
+ logEntryDatasetBuckets = [...logEntryDatasetBuckets, ...latestBatchBuckets];
+ afterLatestBatchKey = afterKey;
+ esSearchSpans = [...esSearchSpans, finalizeEsSearchSpan()];
+
+ if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) {
+ break;
+ }
+ }
+
+ const logEntryDatasetsSpan = finalizeLogEntryDatasetsSpan();
+
+ return {
+ data: logEntryDatasetBuckets.map((logEntryDatasetBucket) => logEntryDatasetBucket.key.dataset),
+ timing: {
+ spans: [logEntryDatasetsSpan, ...esSearchSpans],
+ },
+ };
+}
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/errors.ts b/x-pack/plugins/infra/server/lib/infra_ml/errors.ts
new file mode 100644
index 0000000000000..ad46ebf710266
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/errors.ts
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable max-classes-per-file */
+
+import {
+ UnknownMLCapabilitiesError,
+ InsufficientMLCapabilities,
+ MLPrivilegesUninitialized,
+} from '../../../../ml/server';
+
+export class NoLogAnalysisMlJobError extends Error {
+ constructor(message?: string) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+export class InsufficientLogAnalysisMlJobConfigurationError extends Error {
+ constructor(message?: string) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+export class UnknownCategoryError extends Error {
+ constructor(categoryId: number) {
+ super(`Unknown ml category ${categoryId}`);
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+export class InsufficientAnomalyMlJobsConfigured extends Error {
+ constructor(message?: string) {
+ super(message);
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+export const isMlPrivilegesError = (error: any) => {
+ return (
+ error instanceof UnknownMLCapabilitiesError ||
+ error instanceof InsufficientMLCapabilities ||
+ error instanceof MLPrivilegesUninitialized
+ );
+};
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/index.ts
new file mode 100644
index 0000000000000..536f0a44d5890
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './errors';
+export * from './metrics_hosts_anomalies';
+export * from './metrics_k8s_anomalies';
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts
new file mode 100644
index 0000000000000..e0afa458aac88
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts
@@ -0,0 +1,289 @@
+/*
+ * 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 { RequestHandlerContext } from 'src/core/server';
+import { InfraRequestHandlerContext } from '../../types';
+import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
+import { fetchMlJob, getLogEntryDatasets } from './common';
+import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml';
+import { Sort, Pagination } from '../../../common/http_api/infra_ml';
+import type { MlSystem, MlAnomalyDetectors } from '../../types';
+import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
+import { decodeOrThrow } from '../../../common/runtime_types';
+import {
+ metricsHostsAnomaliesResponseRT,
+ createMetricsHostsAnomaliesQuery,
+} from './queries/metrics_hosts_anomalies';
+
+interface MappedAnomalyHit {
+ id: string;
+ anomalyScore: number;
+ dataset: string;
+ typical: number;
+ actual: number;
+ jobId: string;
+ startTime: number;
+ duration: number;
+ hostName: string[];
+ categoryId?: string;
+}
+
+async function getCompatibleAnomaliesJobIds(
+ spaceId: string,
+ sourceId: string,
+ mlAnomalyDetectors: MlAnomalyDetectors
+) {
+ const metricsHostsJobIds = metricsHostsJobTypes.map((jt) => getJobId(spaceId, sourceId, jt));
+
+ const jobIds: string[] = [];
+ let jobSpans: TracingSpan[] = [];
+
+ try {
+ await Promise.all(
+ metricsHostsJobIds.map((id) => {
+ return (async () => {
+ const {
+ timing: { spans },
+ } = await fetchMlJob(mlAnomalyDetectors, id);
+ jobIds.push(id);
+ jobSpans = [...jobSpans, ...spans];
+ })();
+ })
+ );
+ } catch (e) {
+ if (isMlPrivilegesError(e)) {
+ throw e;
+ }
+ // An error is also thrown when no jobs are found
+ }
+
+ return {
+ jobIds,
+ timing: { spans: jobSpans },
+ };
+}
+
+export async function getMetricsHostsAnomalies(
+ context: RequestHandlerContext & { infra: Required },
+ sourceId: string,
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) {
+ const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies');
+
+ const {
+ jobIds,
+ timing: { spans: jobSpans },
+ } = await getCompatibleAnomaliesJobIds(
+ context.infra.spaceId,
+ sourceId,
+ context.infra.mlAnomalyDetectors
+ );
+
+ if (jobIds.length === 0) {
+ throw new InsufficientAnomalyMlJobsConfigured(
+ 'Metrics Hosts ML jobs need to be configured to search anomalies'
+ );
+ }
+
+ try {
+ const {
+ anomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing: { spans: fetchLogEntryAnomaliesSpans },
+ } = await fetchMetricsHostsAnomalies(
+ context.infra.mlSystem,
+ jobIds,
+ startTime,
+ endTime,
+ sort,
+ pagination
+ );
+
+ const data = anomalies.map((anomaly) => {
+ const { jobId } = anomaly;
+
+ return parseAnomalyResult(anomaly, jobId);
+ });
+
+ const metricsHostsAnomaliesSpan = finalizeMetricsHostsAnomaliesSpan();
+
+ return {
+ data,
+ paginationCursors,
+ hasMoreEntries,
+ timing: {
+ spans: [metricsHostsAnomaliesSpan, ...jobSpans, ...fetchLogEntryAnomaliesSpans],
+ },
+ };
+ } catch (e) {
+ throw new Error(e);
+ }
+}
+
+const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
+ const {
+ id,
+ anomalyScore,
+ dataset,
+ typical,
+ actual,
+ duration,
+ hostName,
+ startTime: anomalyStartTime,
+ } = anomaly;
+
+ return {
+ id,
+ anomalyScore,
+ dataset,
+ typical,
+ actual,
+ duration,
+ hostName,
+ startTime: anomalyStartTime,
+ type: 'metrics_hosts' as const,
+ jobId,
+ };
+};
+
+async function fetchMetricsHostsAnomalies(
+ mlSystem: MlSystem,
+ jobIds: string[],
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) {
+ // We'll request 1 extra entry on top of our pageSize to determine if there are
+ // more entries to be fetched. This avoids scenarios where the client side can't
+ // determine if entries.length === pageSize actually means there are more entries / next page
+ // or not.
+ const expandedPagination = { ...pagination, pageSize: pagination.pageSize + 1 };
+
+ const finalizeFetchLogEntryAnomaliesSpan = startTracingSpan('fetch metrics hosts anomalies');
+
+ // console.log(
+ // 'data',
+ // JSON.stringify(
+ // await mlSystem.mlAnomalySearch(
+ // createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination)
+ // ),
+ // null,
+ // 2
+ // )
+ // );
+ const results = decodeOrThrow(metricsHostsAnomaliesResponseRT)(
+ await mlSystem.mlAnomalySearch(
+ createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination)
+ )
+ );
+
+ const {
+ hits: { hits },
+ } = results;
+ const hasMoreEntries = hits.length > pagination.pageSize;
+
+ // An extra entry was found and hasMoreEntries has been determined, the extra entry can be removed.
+ if (hasMoreEntries) {
+ hits.pop();
+ }
+
+ // To "search_before" the sort order will have been reversed for ES.
+ // The results are now reversed back, to match the requested sort.
+ if (pagination.cursor && 'searchBefore' in pagination.cursor) {
+ hits.reverse();
+ }
+
+ const paginationCursors =
+ hits.length > 0
+ ? {
+ previousPageCursor: hits[0].sort,
+ nextPageCursor: hits[hits.length - 1].sort,
+ }
+ : undefined;
+
+ const anomalies = hits.map((result) => {
+ const {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ job_id,
+ record_score: anomalyScore,
+ typical,
+ actual,
+ bucket_span: duration,
+ timestamp: anomalyStartTime,
+ by_field_value: categoryId,
+ } = result._source;
+
+ return {
+ id: result._id,
+ anomalyScore,
+ dataset: '',
+ typical: typical[0],
+ actual: actual[0],
+ jobId: job_id,
+ hostName: result._source['host.name'],
+ startTime: anomalyStartTime,
+ duration: duration * 1000,
+ categoryId,
+ };
+ });
+
+ const fetchLogEntryAnomaliesSpan = finalizeFetchLogEntryAnomaliesSpan();
+
+ return {
+ anomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing: {
+ spans: [fetchLogEntryAnomaliesSpan],
+ },
+ };
+}
+
+// TODO: FIgure out why we need datasets
+export async function getMetricsHostsAnomaliesDatasets(
+ context: {
+ infra: {
+ mlSystem: MlSystem;
+ mlAnomalyDetectors: MlAnomalyDetectors;
+ spaceId: string;
+ };
+ },
+ sourceId: string,
+ startTime: number,
+ endTime: number
+) {
+ const {
+ jobIds,
+ timing: { spans: jobSpans },
+ } = await getCompatibleAnomaliesJobIds(
+ context.infra.spaceId,
+ sourceId,
+ context.infra.mlAnomalyDetectors
+ );
+
+ if (jobIds.length === 0) {
+ throw new InsufficientAnomalyMlJobsConfigured(
+ 'Log rate or categorisation ML jobs need to be configured to search for anomaly datasets'
+ );
+ }
+
+ const {
+ data: datasets,
+ timing: { spans: datasetsSpans },
+ } = await getLogEntryDatasets(context.infra.mlSystem, startTime, endTime, jobIds);
+
+ return {
+ datasets,
+ timing: {
+ spans: [...jobSpans, ...datasetsSpans],
+ },
+ };
+}
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts
new file mode 100644
index 0000000000000..29507900e1847
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts
@@ -0,0 +1,272 @@
+/*
+ * 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 { RequestHandlerContext } from 'src/core/server';
+import { InfraRequestHandlerContext } from '../../types';
+import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing';
+import { fetchMlJob, getLogEntryDatasets } from './common';
+import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml';
+import { Sort, Pagination } from '../../../common/http_api/infra_ml';
+import type { MlSystem, MlAnomalyDetectors } from '../../types';
+import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors';
+import { decodeOrThrow } from '../../../common/runtime_types';
+import {
+ metricsK8sAnomaliesResponseRT,
+ createMetricsK8sAnomaliesQuery,
+} from './queries/metrics_k8s_anomalies';
+
+interface MappedAnomalyHit {
+ id: string;
+ anomalyScore: number;
+ // dataset: string;
+ typical: number;
+ actual: number;
+ jobId: string;
+ startTime: number;
+ duration: number;
+ categoryId?: string;
+}
+
+async function getCompatibleAnomaliesJobIds(
+ spaceId: string,
+ sourceId: string,
+ mlAnomalyDetectors: MlAnomalyDetectors
+) {
+ const metricsK8sJobIds = metricsK8SJobTypes.map((jt) => getJobId(spaceId, sourceId, jt));
+
+ const jobIds: string[] = [];
+ let jobSpans: TracingSpan[] = [];
+
+ try {
+ await Promise.all(
+ metricsK8sJobIds.map((id) => {
+ return (async () => {
+ const {
+ timing: { spans },
+ } = await fetchMlJob(mlAnomalyDetectors, id);
+ jobIds.push(id);
+ jobSpans = [...jobSpans, ...spans];
+ })();
+ })
+ );
+ } catch (e) {
+ if (isMlPrivilegesError(e)) {
+ throw e;
+ }
+ // An error is also thrown when no jobs are found
+ }
+
+ return {
+ jobIds,
+ timing: { spans: jobSpans },
+ };
+}
+
+export async function getMetricK8sAnomalies(
+ context: RequestHandlerContext & { infra: Required },
+ sourceId: string,
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) {
+ const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies');
+
+ const {
+ jobIds,
+ timing: { spans: jobSpans },
+ } = await getCompatibleAnomaliesJobIds(
+ context.infra.spaceId,
+ sourceId,
+ context.infra.mlAnomalyDetectors
+ );
+
+ if (jobIds.length === 0) {
+ throw new InsufficientAnomalyMlJobsConfigured(
+ 'Log rate or categorisation ML jobs need to be configured to search anomalies'
+ );
+ }
+
+ const {
+ anomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing: { spans: fetchLogEntryAnomaliesSpans },
+ } = await fetchMetricK8sAnomalies(
+ context.infra.mlSystem,
+ jobIds,
+ startTime,
+ endTime,
+ sort,
+ pagination
+ );
+
+ const data = anomalies.map((anomaly) => {
+ const { jobId } = anomaly;
+
+ return parseAnomalyResult(anomaly, jobId);
+ });
+
+ const metricsK8sAnomaliesSpan = finalizeMetricsK8sAnomaliesSpan();
+
+ return {
+ data,
+ paginationCursors,
+ hasMoreEntries,
+ timing: {
+ spans: [metricsK8sAnomaliesSpan, ...jobSpans, ...fetchLogEntryAnomaliesSpans],
+ },
+ };
+}
+
+const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => {
+ const {
+ id,
+ anomalyScore,
+ // dataset,
+ typical,
+ actual,
+ duration,
+ startTime: anomalyStartTime,
+ } = anomaly;
+
+ return {
+ id,
+ anomalyScore,
+ // dataset,
+ typical,
+ actual,
+ duration,
+ startTime: anomalyStartTime,
+ type: 'metrics_k8s' as const,
+ jobId,
+ };
+};
+
+async function fetchMetricK8sAnomalies(
+ mlSystem: MlSystem,
+ jobIds: string[],
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) {
+ // We'll request 1 extra entry on top of our pageSize to determine if there are
+ // more entries to be fetched. This avoids scenarios where the client side can't
+ // determine if entries.length === pageSize actually means there are more entries / next page
+ // or not.
+ const expandedPagination = { ...pagination, pageSize: pagination.pageSize + 1 };
+
+ const finalizeFetchLogEntryAnomaliesSpan = startTracingSpan('fetch metrics k8s anomalies');
+
+ const results = decodeOrThrow(metricsK8sAnomaliesResponseRT)(
+ await mlSystem.mlAnomalySearch(
+ createMetricsK8sAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination)
+ )
+ );
+
+ const {
+ hits: { hits },
+ } = results;
+ const hasMoreEntries = hits.length > pagination.pageSize;
+
+ // An extra entry was found and hasMoreEntries has been determined, the extra entry can be removed.
+ if (hasMoreEntries) {
+ hits.pop();
+ }
+
+ // To "search_before" the sort order will have been reversed for ES.
+ // The results are now reversed back, to match the requested sort.
+ if (pagination.cursor && 'searchBefore' in pagination.cursor) {
+ hits.reverse();
+ }
+
+ const paginationCursors =
+ hits.length > 0
+ ? {
+ previousPageCursor: hits[0].sort,
+ nextPageCursor: hits[hits.length - 1].sort,
+ }
+ : undefined;
+
+ const anomalies = hits.map((result) => {
+ const {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ job_id,
+ record_score: anomalyScore,
+ typical,
+ actual,
+ // partition_field_value: dataset,
+ bucket_span: duration,
+ timestamp: anomalyStartTime,
+ by_field_value: categoryId,
+ } = result._source;
+
+ return {
+ id: result._id,
+ anomalyScore,
+ // dataset,
+ typical: typical[0],
+ actual: actual[0],
+ jobId: job_id,
+ startTime: anomalyStartTime,
+ duration: duration * 1000,
+ categoryId,
+ };
+ });
+
+ const fetchLogEntryAnomaliesSpan = finalizeFetchLogEntryAnomaliesSpan();
+
+ return {
+ anomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing: {
+ spans: [fetchLogEntryAnomaliesSpan],
+ },
+ };
+}
+
+// TODO: FIgure out why we need datasets
+export async function getMetricK8sAnomaliesDatasets(
+ context: {
+ infra: {
+ mlSystem: MlSystem;
+ mlAnomalyDetectors: MlAnomalyDetectors;
+ spaceId: string;
+ };
+ },
+ sourceId: string,
+ startTime: number,
+ endTime: number
+) {
+ const {
+ jobIds,
+ timing: { spans: jobSpans },
+ } = await getCompatibleAnomaliesJobIds(
+ context.infra.spaceId,
+ sourceId,
+ context.infra.mlAnomalyDetectors
+ );
+
+ if (jobIds.length === 0) {
+ throw new InsufficientAnomalyMlJobsConfigured(
+ 'Log rate or categorisation ML jobs need to be configured to search for anomaly datasets'
+ );
+ }
+
+ const {
+ data: datasets,
+ timing: { spans: datasetsSpans },
+ } = await getLogEntryDatasets(context.infra.mlSystem, startTime, endTime, jobIds);
+
+ return {
+ datasets,
+ timing: {
+ spans: [...jobSpans, ...datasetsSpans],
+ },
+ };
+}
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts
new file mode 100644
index 0000000000000..63e39ef022392
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.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.
+ */
+
+export const defaultRequestParameters = {
+ allowNoIndices: true,
+ ignoreUnavailable: true,
+ trackScores: false,
+ trackTotalHits: false,
+};
+
+export const createJobIdFilters = (jobId: string) => [
+ {
+ term: {
+ job_id: {
+ value: jobId,
+ },
+ },
+ },
+];
+
+export const createJobIdsFilters = (jobIds: string[]) => [
+ {
+ terms: {
+ job_id: jobIds,
+ },
+ },
+];
+
+export const createTimeRangeFilters = (startTime: number, endTime: number) => [
+ {
+ range: {
+ timestamp: {
+ gte: startTime,
+ lte: endTime,
+ },
+ },
+ },
+];
+
+export const createResultTypeFilters = (resultTypes: Array<'model_plot' | 'record'>) => [
+ {
+ terms: {
+ result_type: resultTypes,
+ },
+ },
+];
+
+export const createCategoryIdFilters = (categoryIds: number[]) => [
+ {
+ terms: {
+ category_id: categoryIds,
+ },
+ },
+];
+
+export const createDatasetsFilters = (datasets?: string[]) =>
+ datasets && datasets.length > 0
+ ? [
+ {
+ terms: {
+ partition_field_value: datasets,
+ },
+ },
+ ]
+ : [];
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/index.ts
new file mode 100644
index 0000000000000..5a42011e1cea1
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 * from './metrics_k8s_anomalies';
+export * from './metrics_hosts_anomalies';
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/log_entry_data_sets.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/log_entry_data_sets.ts
new file mode 100644
index 0000000000000..53971a91d86b1
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/log_entry_data_sets.ts
@@ -0,0 +1,84 @@
+/*
+ * 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 rt from 'io-ts';
+import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
+import {
+ createJobIdsFilters,
+ createResultTypeFilters,
+ createTimeRangeFilters,
+ defaultRequestParameters,
+} from './common';
+
+export const createLogEntryDatasetsQuery = (
+ jobIds: string[],
+ startTime: number,
+ endTime: number,
+ size: number,
+ afterKey?: CompositeDatasetKey
+) => ({
+ ...defaultRequestParameters,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ ...createJobIdsFilters(jobIds),
+ ...createTimeRangeFilters(startTime, endTime),
+ ...createResultTypeFilters(['model_plot']),
+ ],
+ },
+ },
+ aggs: {
+ dataset_buckets: {
+ composite: {
+ after: afterKey,
+ size,
+ sources: [
+ {
+ dataset: {
+ terms: {
+ field: 'partition_field_value',
+ order: 'asc',
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ size: 0,
+});
+
+const compositeDatasetKeyRT = rt.type({
+ dataset: rt.string,
+});
+
+export type CompositeDatasetKey = rt.TypeOf;
+
+const logEntryDatasetBucketRT = rt.type({
+ key: compositeDatasetKeyRT,
+});
+
+export type LogEntryDatasetBucket = rt.TypeOf;
+
+export const logEntryDatasetsResponseRT = rt.intersection([
+ commonSearchSuccessResponseFieldsRT,
+ rt.partial({
+ aggregations: rt.type({
+ dataset_buckets: rt.intersection([
+ rt.type({
+ buckets: rt.array(logEntryDatasetBucketRT),
+ }),
+ rt.partial({
+ after_key: compositeDatasetKeyRT,
+ }),
+ ]),
+ }),
+ }),
+]);
+
+export type LogEntryDatasetsResponse = rt.TypeOf;
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts
new file mode 100644
index 0000000000000..b61119b60bc18
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts
@@ -0,0 +1,131 @@
+/*
+ * 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 rt from 'io-ts';
+import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
+import {
+ createJobIdsFilters,
+ createTimeRangeFilters,
+ createResultTypeFilters,
+ defaultRequestParameters,
+} from './common';
+import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
+
+// TODO: Reassess validity of this against ML docs
+const TIEBREAKER_FIELD = '_doc';
+
+const sortToMlFieldMap = {
+ dataset: 'partition_field_value',
+ anomalyScore: 'record_score',
+ startTime: 'timestamp',
+};
+
+export const createMetricsHostsAnomaliesQuery = (
+ jobIds: string[],
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) => {
+ const { field } = sort;
+ const { pageSize } = pagination;
+
+ const filters = [
+ ...createJobIdsFilters(jobIds),
+ ...createTimeRangeFilters(startTime, endTime),
+ ...createResultTypeFilters(['record']),
+ ];
+
+ const sourceFields = [
+ 'job_id',
+ 'record_score',
+ 'typical',
+ 'actual',
+ 'partition_field_value',
+ 'timestamp',
+ 'bucket_span',
+ 'by_field_value',
+ 'host.name',
+ 'influencers.influencer_field_name',
+ 'influencers.influencer_field_values',
+ ];
+
+ const { querySortDirection, queryCursor } = parsePaginationCursor(sort, pagination);
+
+ const sortOptions = [
+ { [sortToMlFieldMap[field]]: querySortDirection },
+ { [TIEBREAKER_FIELD]: querySortDirection }, // Tiebreaker
+ ];
+
+ const resultsQuery = {
+ ...defaultRequestParameters,
+ body: {
+ query: {
+ bool: {
+ filter: filters,
+ },
+ },
+ search_after: queryCursor,
+ sort: sortOptions,
+ size: pageSize,
+ _source: sourceFields,
+ },
+ };
+
+ return resultsQuery;
+};
+
+export const metricsHostsAnomalyHitRT = rt.type({
+ _id: rt.string,
+ _source: rt.intersection([
+ rt.type({
+ job_id: rt.string,
+ record_score: rt.number,
+ typical: rt.array(rt.number),
+ actual: rt.array(rt.number),
+ 'host.name': rt.array(rt.string),
+ bucket_span: rt.number,
+ timestamp: rt.number,
+ }),
+ rt.partial({
+ by_field_value: rt.string,
+ }),
+ ]),
+ sort: rt.tuple([rt.union([rt.string, rt.number]), rt.union([rt.string, rt.number])]),
+});
+
+export type MetricsHostsAnomalyHit = rt.TypeOf;
+
+export const metricsHostsAnomaliesResponseRT = rt.intersection([
+ commonSearchSuccessResponseFieldsRT,
+ rt.type({
+ hits: rt.type({
+ hits: rt.array(metricsHostsAnomalyHitRT),
+ }),
+ }),
+]);
+
+export type MetricsHostsAnomaliesResponseRT = rt.TypeOf;
+
+const parsePaginationCursor = (sort: Sort, pagination: Pagination) => {
+ const { cursor } = pagination;
+ const { direction } = sort;
+
+ if (!cursor) {
+ return { querySortDirection: direction, queryCursor: undefined };
+ }
+
+ // We will always use ES's search_after to paginate, to mimic "search_before" behaviour we
+ // need to reverse the user's chosen search direction for the ES query.
+ if ('searchBefore' in cursor) {
+ return {
+ querySortDirection: direction === 'desc' ? 'asc' : 'desc',
+ queryCursor: cursor.searchBefore,
+ };
+ } else {
+ return { querySortDirection: direction, queryCursor: cursor.searchAfter };
+ }
+};
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts
new file mode 100644
index 0000000000000..84ed8b064c5ca
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts
@@ -0,0 +1,128 @@
+/*
+ * 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 rt from 'io-ts';
+import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types';
+import {
+ createJobIdsFilters,
+ createTimeRangeFilters,
+ createResultTypeFilters,
+ defaultRequestParameters,
+} from './common';
+import { Sort, Pagination } from '../../../../common/http_api/infra_ml';
+
+// TODO: Reassess validity of this against ML docs
+const TIEBREAKER_FIELD = '_doc';
+
+const sortToMlFieldMap = {
+ dataset: 'partition_field_value',
+ anomalyScore: 'record_score',
+ startTime: 'timestamp',
+};
+
+export const createMetricsK8sAnomaliesQuery = (
+ jobIds: string[],
+ startTime: number,
+ endTime: number,
+ sort: Sort,
+ pagination: Pagination
+) => {
+ const { field } = sort;
+ const { pageSize } = pagination;
+
+ const filters = [
+ ...createJobIdsFilters(jobIds),
+ ...createTimeRangeFilters(startTime, endTime),
+ ...createResultTypeFilters(['record']),
+ ];
+
+ const sourceFields = [
+ 'job_id',
+ 'record_score',
+ 'typical',
+ 'actual',
+ 'partition_field_value',
+ 'timestamp',
+ 'bucket_span',
+ 'by_field_value',
+ ];
+
+ const { querySortDirection, queryCursor } = parsePaginationCursor(sort, pagination);
+
+ const sortOptions = [
+ { [sortToMlFieldMap[field]]: querySortDirection },
+ { [TIEBREAKER_FIELD]: querySortDirection }, // Tiebreaker
+ ];
+
+ const resultsQuery = {
+ ...defaultRequestParameters,
+ body: {
+ query: {
+ bool: {
+ filter: filters,
+ },
+ },
+ search_after: queryCursor,
+ sort: sortOptions,
+ size: pageSize,
+ _source: sourceFields,
+ },
+ };
+
+ return resultsQuery;
+};
+
+export const metricsK8sAnomalyHitRT = rt.type({
+ _id: rt.string,
+ _source: rt.intersection([
+ rt.type({
+ job_id: rt.string,
+ record_score: rt.number,
+ typical: rt.array(rt.number),
+ actual: rt.array(rt.number),
+ // partition_field_value: rt.string,
+ bucket_span: rt.number,
+ timestamp: rt.number,
+ }),
+ rt.partial({
+ by_field_value: rt.string,
+ }),
+ ]),
+ sort: rt.tuple([rt.union([rt.string, rt.number]), rt.union([rt.string, rt.number])]),
+});
+
+export type MetricsK8sAnomalyHit = rt.TypeOf;
+
+export const metricsK8sAnomaliesResponseRT = rt.intersection([
+ commonSearchSuccessResponseFieldsRT,
+ rt.type({
+ hits: rt.type({
+ hits: rt.array(metricsK8sAnomalyHitRT),
+ }),
+ }),
+]);
+
+export type MetricsK8sAnomaliesResponseRT = rt.TypeOf;
+
+const parsePaginationCursor = (sort: Sort, pagination: Pagination) => {
+ const { cursor } = pagination;
+ const { direction } = sort;
+
+ if (!cursor) {
+ return { querySortDirection: direction, queryCursor: undefined };
+ }
+
+ // We will always use ES's search_after to paginate, to mimic "search_before" behaviour we
+ // need to reverse the user's chosen search direction for the ES query.
+ if ('searchBefore' in cursor) {
+ return {
+ querySortDirection: direction === 'desc' ? 'asc' : 'desc',
+ queryCursor: cursor.searchBefore,
+ };
+ } else {
+ return { querySortDirection: direction, queryCursor: cursor.searchAfter };
+ }
+};
diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/ml_jobs.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/ml_jobs.ts
new file mode 100644
index 0000000000000..ee4ccbfaeb5a7
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/ml_jobs.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 rt from 'io-ts';
+
+export const createMlJobsQuery = (jobIds: string[]) => ({
+ method: 'GET',
+ path: `/_ml/anomaly_detectors/${jobIds.join(',')}`,
+ query: {
+ allow_no_jobs: true,
+ },
+});
+
+export const mlJobRT = rt.type({
+ job_id: rt.string,
+ custom_settings: rt.unknown,
+});
+
+export const mlJobsResponseRT = rt.type({
+ jobs: rt.array(mlJobRT),
+});
diff --git a/x-pack/plugins/infra/server/routes/infra_ml/index.ts b/x-pack/plugins/infra/server/routes/infra_ml/index.ts
new file mode 100644
index 0000000000000..38684cb22e237
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/infra_ml/index.ts
@@ -0,0 +1,7 @@
+/*
+ * 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 * from './results';
diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/index.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/index.ts
new file mode 100644
index 0000000000000..82e30291faa20
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/infra_ml/results/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export * from './metrics_hosts_anomalies';
+export * from './metrics_k8s_anomalies';
diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts
new file mode 100644
index 0000000000000..29122ae159cdc
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts
@@ -0,0 +1,125 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import Boom from 'boom';
+import { InfraBackendLibs } from '../../../lib/infra_types';
+import {
+ INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH,
+ getMetricsHostsAnomaliesSuccessReponsePayloadRT,
+ getMetricsHostsAnomaliesRequestPayloadRT,
+ GetMetricsHostsAnomaliesRequestPayload,
+ Sort,
+ Pagination,
+} from '../../../../common/http_api/infra_ml';
+import { createValidationFunction } from '../../../../common/runtime_types';
+import { assertHasInfraMlPlugins } from '../../../utils/request_context';
+
+import { isMlPrivilegesError } from '../../../lib/infra_ml/errors';
+import { getMetricsHostsAnomalies } from '../../../lib/infra_ml';
+
+export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
+ framework.registerRoute(
+ {
+ method: 'post',
+ path: INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH,
+ validate: {
+ body: createValidationFunction(getMetricsHostsAnomaliesRequestPayloadRT),
+ },
+ },
+ framework.router.handleLegacyErrors(async (requestContext, request, response) => {
+ const {
+ data: {
+ sourceId,
+ timeRange: { startTime, endTime },
+ sort: sortParam,
+ pagination: paginationParam,
+ },
+ } = request.body;
+
+ const { sort, pagination } = getSortAndPagination(sortParam, paginationParam);
+
+ try {
+ assertHasInfraMlPlugins(requestContext);
+
+ const {
+ data: anomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing,
+ } = await getMetricsHostsAnomalies(
+ requestContext,
+ sourceId,
+ startTime,
+ endTime,
+ sort,
+ pagination
+ );
+
+ // console.log('---- anomalies', anomalies);
+
+ return response.ok({
+ body: getMetricsHostsAnomaliesSuccessReponsePayloadRT.encode({
+ data: {
+ anomalies,
+ hasMoreEntries,
+ paginationCursors,
+ },
+ timing,
+ }),
+ });
+ } catch (error) {
+ if (Boom.isBoom(error)) {
+ throw error;
+ }
+
+ if (isMlPrivilegesError(error)) {
+ return response.customError({
+ statusCode: 403,
+ body: {
+ message: error.message,
+ },
+ });
+ }
+
+ return response.customError({
+ statusCode: error.statusCode ?? 500,
+ body: {
+ message: error.message ?? 'An unexpected error occurred',
+ },
+ });
+ }
+ })
+ );
+};
+
+const getSortAndPagination = (
+ sort: Partial = {},
+ pagination: Partial = {}
+): {
+ sort: Sort;
+ pagination: Pagination;
+} => {
+ const sortDefaults = {
+ field: 'anomalyScore' as const,
+ direction: 'desc' as const,
+ };
+
+ const sortWithDefaults = {
+ ...sortDefaults,
+ ...sort,
+ };
+
+ const paginationDefaults = {
+ pageSize: 50,
+ };
+
+ const paginationWithDefaults = {
+ ...paginationDefaults,
+ ...pagination,
+ };
+
+ return { sort: sortWithDefaults, pagination: paginationWithDefaults };
+};
diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts
new file mode 100644
index 0000000000000..5260c55836c59
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts
@@ -0,0 +1,122 @@
+/*
+ * 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 Boom from 'boom';
+import { InfraBackendLibs } from '../../../lib/infra_types';
+import {
+ INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH,
+ getMetricsK8sAnomaliesSuccessReponsePayloadRT,
+ getMetricsK8sAnomaliesRequestPayloadRT,
+ GetMetricsK8sAnomaliesRequestPayload,
+ Sort,
+ Pagination,
+} from '../../../../common/http_api/infra_ml';
+import { createValidationFunction } from '../../../../common/runtime_types';
+import { assertHasInfraMlPlugins } from '../../../utils/request_context';
+import { getMetricK8sAnomalies } from '../../../lib/infra_ml';
+import { isMlPrivilegesError } from '../../../lib/infra_ml/errors';
+
+export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => {
+ framework.registerRoute(
+ {
+ method: 'post',
+ path: INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH,
+ validate: {
+ body: createValidationFunction(getMetricsK8sAnomaliesRequestPayloadRT),
+ },
+ },
+ framework.router.handleLegacyErrors(async (requestContext, request, response) => {
+ const {
+ data: {
+ sourceId,
+ timeRange: { startTime, endTime },
+ sort: sortParam,
+ pagination: paginationParam,
+ },
+ } = request.body;
+
+ const { sort, pagination } = getSortAndPagination(sortParam, paginationParam);
+
+ try {
+ assertHasInfraMlPlugins(requestContext);
+
+ const {
+ data: anomalies,
+ paginationCursors,
+ hasMoreEntries,
+ timing,
+ } = await getMetricK8sAnomalies(
+ requestContext,
+ sourceId,
+ startTime,
+ endTime,
+ sort,
+ pagination
+ );
+
+ return response.ok({
+ body: getMetricsK8sAnomaliesSuccessReponsePayloadRT.encode({
+ data: {
+ anomalies,
+ hasMoreEntries,
+ paginationCursors,
+ },
+ timing,
+ }),
+ });
+ } catch (error) {
+ if (Boom.isBoom(error)) {
+ throw error;
+ }
+
+ if (isMlPrivilegesError(error)) {
+ return response.customError({
+ statusCode: 403,
+ body: {
+ message: error.message,
+ },
+ });
+ }
+
+ return response.customError({
+ statusCode: error.statusCode ?? 500,
+ body: {
+ message: error.message ?? 'An unexpected error occurred',
+ },
+ });
+ }
+ })
+ );
+};
+
+const getSortAndPagination = (
+ sort: Partial = {},
+ pagination: Partial = {}
+): {
+ sort: Sort;
+ pagination: Pagination;
+} => {
+ const sortDefaults = {
+ field: 'anomalyScore' as const,
+ direction: 'desc' as const,
+ };
+
+ const sortWithDefaults = {
+ ...sortDefaults,
+ ...sort,
+ };
+
+ const paginationDefaults = {
+ pageSize: 50,
+ };
+
+ const paginationWithDefaults = {
+ ...paginationDefaults,
+ ...pagination,
+ };
+
+ return { sort: sortWithDefaults, pagination: paginationWithDefaults };
+};
diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts
index d899739a74ef0..69672dfb9ec6c 100644
--- a/x-pack/plugins/ingest_manager/common/constants/routes.ts
+++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts
@@ -90,6 +90,7 @@ export const AGENT_API_ROUTES = {
REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/reassign`,
BULK_REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/bulk_reassign`,
STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`,
+ UPGRADE_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/upgrade`,
};
export const ENROLLMENT_API_KEY_ROUTES = {
diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts
index fe4e094e1bb22..70f4d7f9344f9 100644
--- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts
+++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts
@@ -19,6 +19,9 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta
if (!agent.last_checkin) {
return 'enrolling';
}
+ if (agent.upgrade_started_at && !agent.upgraded_at) {
+ return 'upgrading';
+ }
const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn;
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
index a204373fe2e56..7110fd4ce52ea 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
@@ -19,10 +19,10 @@ export type AgentStatus =
| 'warning'
| 'enrolling'
| 'unenrolling'
+ | 'upgrading'
| 'degraded';
-export type AgentActionType = 'CONFIG_CHANGE' | 'UNENROLL';
-
+export type AgentActionType = 'CONFIG_CHANGE' | 'UNENROLL' | 'UPGRADE';
export interface NewAgentAction {
type: AgentActionType;
data?: any;
@@ -65,7 +65,6 @@ export type AgentPolicyActionSOAttributes = CommonAgentActionSOAttributes & {
policy_id: string;
policy_revision: number;
};
-
export type BaseAgentActionSOAttributes = AgentActionSOAttributes | AgentPolicyActionSOAttributes;
export interface NewAgentEvent {
@@ -110,6 +109,8 @@ interface AgentBase {
enrolled_at: string;
unenrolled_at?: string;
unenrollment_started_at?: string;
+ upgraded_at?: string;
+ upgrade_started_at?: string;
shared_id?: string;
access_api_key_id?: string;
default_api_key?: string;
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
index 1a10d4930656f..ab4c372c4e1d6 100644
--- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
@@ -113,6 +113,11 @@ export interface PostAgentUnenrollRequest {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PostAgentUnenrollResponse {}
+export interface PostAgentUpgradeRequest {
+ params: {
+ agentId: string;
+ };
+}
export interface PostBulkAgentUnenrollRequest {
body: {
agents: string[] | string;
@@ -120,6 +125,8 @@ export interface PostBulkAgentUnenrollRequest {
};
}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface PostAgentUpgradeResponse {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PostBulkAgentUnenrollResponse {}
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
index eafc726ea166d..73ed276ba02e7 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts
@@ -29,6 +29,7 @@ import {
PutAgentReassignRequestSchema,
PostBulkAgentReassignRequestSchema,
PostAgentEnrollRequestBodyJSONSchema,
+ PostAgentUpgradeRequestSchema,
} from '../../types';
import {
getAgentsHandler,
@@ -48,6 +49,7 @@ import { postNewAgentActionHandlerBuilder } from './actions_handlers';
import { appContextService } from '../../services';
import { postAgentUnenrollHandler, postBulkAgentsUnenrollHandler } from './unenroll_handler';
import { IngestManagerConfigType } from '../..';
+import { postAgentUpgradeHandler } from './upgrade_handler';
const ajv = new Ajv({
coerceTypes: true,
@@ -215,7 +217,15 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType)
},
getAgentStatusForAgentPolicyHandler
);
-
+ // upgrade agent
+ router.post(
+ {
+ path: AGENT_API_ROUTES.UPGRADE_PATTERN,
+ validate: PostAgentUpgradeRequestSchema,
+ options: { tags: [`access:${PLUGIN_ID}-all`] },
+ },
+ postAgentUpgradeHandler
+ );
// Bulk reassign
router.post(
{
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts
new file mode 100644
index 0000000000000..e5d7a44c00768
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { RequestHandler } from 'src/core/server';
+import { TypeOf } from '@kbn/config-schema';
+import { PostAgentUpgradeResponse } from '../../../common/types';
+import { PostAgentUpgradeRequestSchema } from '../../types';
+import * as AgentService from '../../services/agents';
+import { appContextService } from '../../services';
+import { defaultIngestErrorHandler } from '../../errors';
+
+export const postAgentUpgradeHandler: RequestHandler<
+ TypeOf,
+ undefined,
+ TypeOf
+> = async (context, request, response) => {
+ const soClient = context.core.savedObjects.client;
+ const { version, source_uri: sourceUri } = request.body;
+
+ // temporarily only allow upgrading to the same version as the installed kibana version
+ const kibanaVersion = appContextService.getKibanaVersion();
+ if (kibanaVersion !== version) {
+ return response.customError({
+ statusCode: 400,
+ body: {
+ message: `cannot upgrade agent to ${version} because it is different than the installed kibana version ${kibanaVersion}`,
+ },
+ });
+ }
+
+ try {
+ await AgentService.sendUpgradeAgentAction({
+ soClient,
+ agentId: request.params.agentId,
+ version,
+ sourceUri,
+ });
+
+ const body: PostAgentUpgradeResponse = {};
+ return response.ok({ body });
+ } catch (error) {
+ return defaultIngestErrorHandler({ error, response });
+ }
+};
diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
index e86f7b24e2c78..fd08b76a3916b 100644
--- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
+++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
@@ -68,6 +68,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
enrolled_at: { type: 'date' },
unenrolled_at: { type: 'date' },
unenrollment_started_at: { type: 'date' },
+ upgraded_at: { type: 'date' },
+ upgrade_started_at: { type: 'date' },
access_api_key_id: { type: 'keyword' },
version: { type: 'keyword' },
user_provided_metadata: { type: 'flattened' },
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
index 1392710eb0eff..e22ee4256b0e2 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts
@@ -28,6 +28,7 @@ import {
} from '../../constants';
import { getAgentActionByIds } from './actions';
import { forceUnenrollAgent } from './unenroll';
+import { ackAgentUpgraded } from './upgrade';
const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT'];
@@ -80,6 +81,11 @@ export async function acknowledgeAgentActions(
await forceUnenrollAgent(soClient, agent.id);
}
+ const upgradeAction = actions.find((action) => action.type === 'UPGRADE');
+ if (upgradeAction) {
+ await ackAgentUpgraded(soClient, upgradeAction);
+ }
+
const configChangeAction = getLatestConfigChangePolicyActionIfUpdated(agent, actions);
await soClient.bulkUpdate([
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts
index 1d4db44edf88a..f018eea61e4f3 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts
@@ -225,7 +225,11 @@ export async function getAgentPolicyActionByIds(
);
}
-export async function getNewActionsSince(soClient: SavedObjectsClientContract, timestamp: string) {
+export async function getNewActionsSince(
+ soClient: SavedObjectsClientContract,
+ timestamp: string,
+ decryptData: boolean = true
+) {
const filter = nodeTypes.function.buildNode('and', [
nodeTypes.function.buildNode(
'not',
@@ -243,14 +247,33 @@ export async function getNewActionsSince(soClient: SavedObjectsClientContract, t
}
),
]);
- const res = await soClient.find({
- type: AGENT_ACTION_SAVED_OBJECT_TYPE,
- filter,
- });
- return res.saved_objects
+ const actions = (
+ await soClient.find({
+ type: AGENT_ACTION_SAVED_OBJECT_TYPE,
+ filter,
+ })
+ ).saved_objects
.filter(isAgentActionSavedObject)
.map((so) => savedObjectToAgentAction(so));
+
+ if (!decryptData) {
+ return actions;
+ }
+
+ return await Promise.all(
+ actions.map(async (action) => {
+ // Get decrypted actions
+ return savedObjectToAgentAction(
+ await appContextService
+ .getEncryptedSavedObjects()
+ .getDecryptedAsInternalUser(
+ AGENT_ACTION_SAVED_OBJECT_TYPE,
+ action.id
+ )
+ );
+ })
+ );
}
export async function getLatestConfigChangeAction(
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts
index 400c099af4e93..c878b666bde88 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts
@@ -9,6 +9,7 @@ export * from './events';
export * from './checkin';
export * from './enroll';
export * from './unenroll';
+export * from './upgrade';
export * from './status';
export * from './crud';
export * from './update';
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts b/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts
new file mode 100644
index 0000000000000..cee3bc69f25db
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObjectsClientContract } from 'src/core/server';
+import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types';
+import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants';
+import { createAgentAction } from './actions';
+
+export async function sendUpgradeAgentAction({
+ soClient,
+ agentId,
+ version,
+ sourceUri,
+}: {
+ soClient: SavedObjectsClientContract;
+ agentId: string;
+ version: string;
+ sourceUri: string;
+}) {
+ const now = new Date().toISOString();
+ const data = {
+ version,
+ source_uri: sourceUri,
+ };
+ await createAgentAction(soClient, {
+ agent_id: agentId,
+ created_at: now,
+ data,
+ ack_data: data,
+ type: 'UPGRADE',
+ });
+ await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, {
+ upgraded_at: undefined,
+ upgrade_started_at: now,
+ });
+}
+
+export async function ackAgentUpgraded(
+ soClient: SavedObjectsClientContract,
+ agentAction: AgentAction
+) {
+ const {
+ attributes: { ack_data: ackData },
+ } = await soClient.get(AGENT_ACTION_SAVED_OBJECT_TYPE, agentAction.id);
+ if (!ackData) throw new Error('data missing from UPGRADE action');
+ const { version } = JSON.parse(ackData);
+ if (!version) throw new Error('version missing from UPGRADE action');
+ await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentAction.agent_id, {
+ upgraded_at: new Date().toISOString(),
+ local_metadata: {
+ elastic: {
+ agent: {
+ version,
+ },
+ },
+ },
+ });
+}
diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent.ts b/x-pack/plugins/ingest_manager/server/types/models/agent.ts
index b249705fe6c2f..15004e60a6fa4 100644
--- a/x-pack/plugins/ingest_manager/server/types/models/agent.ts
+++ b/x-pack/plugins/ingest_manager/server/types/models/agent.ts
@@ -62,7 +62,12 @@ export const AgentEventSchema = schema.object({
});
export const NewAgentActionSchema = schema.object({
- type: schema.oneOf([schema.literal('CONFIG_CHANGE'), schema.literal('UNENROLL')]),
+ type: schema.oneOf([
+ schema.literal('CONFIG_CHANGE'),
+ schema.literal('UNENROLL'),
+ schema.literal('UPGRADE'),
+ ]),
data: schema.maybe(schema.any()),
+ ack_data: schema.maybe(schema.any()),
sent_at: schema.maybe(schema.string()),
});
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
index 4aefa56e0ca0a..3866ef095563e 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
@@ -172,6 +172,16 @@ export const PostAgentUnenrollRequestSchema = {
),
};
+export const PostAgentUpgradeRequestSchema = {
+ params: schema.object({
+ agentId: schema.string(),
+ }),
+ body: schema.object({
+ source_uri: schema.string(),
+ version: schema.string(),
+ }),
+};
+
export const PostBulkAgentUnenrollRequestSchema = {
body: schema.object({
agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]),
diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts
index 9321aa769423f..a93d2817fbbb3 100644
--- a/x-pack/plugins/security_solution/common/constants.ts
+++ b/x-pack/plugins/security_solution/common/constants.ts
@@ -11,7 +11,6 @@ export const APP_ICON = 'securityAnalyticsApp';
export const APP_ICON_SOLUTION = 'logoSecurity';
export const APP_PATH = `/app/security`;
export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`;
-export const ADD_INDEX_PATH = `/app/management/kibana/indexPatterns/create`;
export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern';
export const DEFAULT_DATE_FORMAT = 'dateFormat';
export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz';
@@ -58,6 +57,8 @@ export const APP_TIMELINES_PATH = `${APP_PATH}/timelines`;
export const APP_CASES_PATH = `${APP_PATH}/cases`;
export const APP_MANAGEMENT_PATH = `${APP_PATH}/administration`;
+export const DETECTIONS_SUB_PLUGIN_ID = `${APP_ID}:${SecurityPageName.detections}`;
+
/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */
export const DEFAULT_INDEX_PATTERN = [
'apm-*-transaction*',
diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
index 48437e12f75a5..0c1f13dac2e69 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts
@@ -71,7 +71,7 @@ export interface PaginationInputPaginated {
export interface DocValueFields {
field: string;
- format: string;
+ format?: string | null;
}
export interface Explanation {
diff --git a/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts
new file mode 100644
index 0000000000000..259a767f8cf70
--- /dev/null
+++ b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { IIndexPattern } from 'src/plugins/data/public';
+import {
+ IEsSearchRequest,
+ IEsSearchResponse,
+ IFieldSubType,
+} from '../../../../../../src/plugins/data/common';
+import { DocValueFields, Maybe } from '../common';
+
+export type BeatFieldsFactoryQueryType = 'beatFields';
+
+interface FieldInfo {
+ category: string;
+ description?: string;
+ example?: string | number;
+ format?: string;
+ name: string;
+ type?: string;
+}
+
+export interface IndexField {
+ /** Where the field belong */
+ category: string;
+ /** Example of field's value */
+ example?: Maybe;
+ /** whether the field's belong to an alias index */
+ indexes: Array>;
+ /** The name of the field */
+ name: string;
+ /** The type of the field's values as recognized by Kibana */
+ type: string;
+ /** Whether the field's values can be efficiently searched for */
+ searchable: boolean;
+ /** Whether the field's values can be aggregated */
+ aggregatable: boolean;
+ /** Description of the field */
+ description?: Maybe;
+ format?: Maybe;
+ /** the elastic type as mapped in the index */
+ esTypes?: string[];
+ subType?: IFieldSubType;
+ readFromDocValues: boolean;
+}
+
+export type BeatFields = Record;
+
+export interface IndexFieldsStrategyRequest extends IEsSearchRequest {
+ indices: string[];
+ onlyCheckIfIndicesExist: boolean;
+}
+
+export interface IndexFieldsStrategyResponse extends IEsSearchResponse {
+ indexFields: IndexField[];
+ indicesExist: string[];
+}
+
+export interface BrowserField {
+ aggregatable: boolean;
+ category: string;
+ description: string | null;
+ example: string | number | null;
+ fields: Readonly>>;
+ format: string;
+ indexes: string[];
+ name: string;
+ searchable: boolean;
+ type: string;
+}
+
+export type BrowserFields = Readonly>>;
+
+export const EMPTY_BROWSER_FIELDS = {};
+export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = [];
+export const EMPTY_INDEX_PATTERN: IIndexPattern = {
+ fields: [],
+ title: '',
+};
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts
index 569ed611bd35b..4416cbb023f10 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts
@@ -10,7 +10,7 @@ import { RequestBasicOptions } from '../..';
export type HostOverviewRequestOptions = RequestBasicOptions;
-export interface HostOverviewStrategyResponse extends IEsSearchResponse {
+export interface HostsOverviewStrategyResponse extends IEsSearchResponse {
inspect?: Maybe;
overviewHost: {
auditbeatAuditd?: Maybe;
diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
index af9faef89af46..39443e596273a 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts
@@ -9,7 +9,7 @@ import { ESQuery } from '../../typed_json';
import {
HostDetailsStrategyResponse,
HostDetailsRequestOptions,
- HostOverviewStrategyResponse,
+ HostsOverviewStrategyResponse,
HostAuthenticationsRequestOptions,
HostAuthenticationsStrategyResponse,
HostOverviewRequestOptions,
@@ -107,7 +107,7 @@ export type StrategyResponseType = T extends HostsQ
: T extends HostsQueries.details
? HostDetailsStrategyResponse
: T extends HostsQueries.overview
- ? HostOverviewStrategyResponse
+ ? HostsOverviewStrategyResponse
: T extends HostsQueries.authentications
? HostAuthenticationsStrategyResponse
: T extends HostsQueries.firstLastSeen
diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
index 6f9192be40150..9fa7f96599deb 100644
--- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
+++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts
@@ -22,7 +22,6 @@ export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse
export interface TimelineEventsDetailsRequestOptions
extends Partial {
- defaultIndex: string[];
indexName: string;
eventId: string;
}
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 84a007e322f11..3888d37a547f7 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -239,6 +239,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({
excludedRowRendererIds: unionWithNullType(runtimeTypes.array(RowRendererIdRuntimeType)),
favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)),
filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)),
+ indexNames: unionWithNullType(runtimeTypes.array(runtimeTypes.string)),
kqlMode: unionWithNullType(runtimeTypes.string),
kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType),
title: unionWithNullType(runtimeTypes.string),
@@ -398,3 +399,5 @@ export const importTimelineResultSchema = runtimeTypes.exact(
);
export type ImportTimelineResultSchema = runtimeTypes.TypeOf;
+
+export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom';
diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts
index 0b302efd655a8..06a8d3a79c3cd 100644
--- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts
@@ -94,7 +94,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery);
cy.url().should(
'include',
- '/app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
+ '/app/security/network/ip/127.0.0.1/source?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
);
});
@@ -102,7 +102,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery);
cy.url().should(
'include',
- '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
+ '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
);
});
@@ -110,7 +110,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery);
cy.url().should(
'include',
- 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27))'
+ 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27))'
);
});
@@ -118,7 +118,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery);
cy.url().should(
'include',
- '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
+ '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
);
});
@@ -126,7 +126,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery);
cy.url().should(
'include',
- '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
+ '/app/security/network/flows?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
);
});
@@ -134,7 +134,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery);
cy.url().should(
'include',
- '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
+ '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))'
);
});
@@ -142,7 +142,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery);
cy.url().should(
'include',
- '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
+ '/app/security/hosts/siem-windows/anomalies?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
);
});
@@ -150,7 +150,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable);
cy.url().should(
'include',
- '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
+ '/app/security/hosts/siem-windows/anomalies?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
);
});
@@ -158,7 +158,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery);
cy.url().should(
'include',
- '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
+ '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
);
});
@@ -166,7 +166,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery);
cy.url().should(
'include',
- '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
+ '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
);
});
@@ -174,7 +174,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery);
cy.url().should(
'include',
- '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
+ '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
);
});
@@ -182,7 +182,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery);
cy.url().should(
'include',
- '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
+ '/app/security/hosts/anomalies?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
);
});
@@ -190,7 +190,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery);
cy.url().should(
'include',
- '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
+ '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))'
);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts
index 5dc3182cd9f83..fdccf164c7465 100644
--- a/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts
@@ -35,6 +35,7 @@ describe('Pagination', () => {
.then((processNameFirstPage) => {
goToThirdPage();
waitForUncommonProcessesToBeLoaded();
+ cy.wait(1500);
cy.get(PROCESS_NAME_FIELD)
.first()
.invoke('text')
diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts
index 6c1d73492f30a..2588c580dedd3 100644
--- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts
@@ -51,7 +51,8 @@ const ABSOLUTE_DATE = {
startTimeTimeline: '2019-08-02T20:03:29.186Z',
};
-describe('url state', () => {
+// FLAKY: https://github.com/elastic/kibana/issues/61612
+describe.skip('url state', () => {
it('sets the global start and end dates from the url', () => {
loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url);
cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should(
@@ -165,7 +166,7 @@ describe('url state', () => {
cy.get(NETWORK).should(
'have.attr',
'href',
- `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))`
+ `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))`
);
});
@@ -178,12 +179,12 @@ describe('url state', () => {
cy.get(HOSTS).should(
'have.attr',
'href',
- `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
+ `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
);
cy.get(NETWORK).should(
'have.attr',
'href',
- `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
+ `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
);
cy.get(HOSTS_NAMES).first().invoke('text').should('eq', 'siem-kibana');
@@ -194,21 +195,21 @@ describe('url state', () => {
cy.get(ANOMALIES_TAB).should(
'have.attr',
'href',
- "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))"
+ "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!('auditbeat-*'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))"
);
cy.get(BREADCRUMBS)
.eq(1)
.should(
'have.attr',
'href',
- `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
+ `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
);
cy.get(BREADCRUMBS)
.eq(2)
.should(
'have.attr',
'href',
- `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
+ `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
);
});
diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json
index 6982c200a5afd..6d79557fdaa28 100644
--- a/x-pack/plugins/security_solution/package.json
+++ b/x-pack/plugins/security_solution/package.json
@@ -6,6 +6,7 @@
"license": "Elastic-License",
"scripts": {
"extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix",
+ "build-beat-doc": "node scripts/beat_docs/build.js && node ../../../scripts/eslint ./server/utils/beat_schema/fields.ts --fix",
"build-graphql-types": "node scripts/generate_types_from_graphql.js",
"cypress:open": "cypress open --config-file ./cypress/cypress.json",
"cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts",
diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx
index b4e9ba3dd7a71..54b02c374e43f 100644
--- a/x-pack/plugins/security_solution/public/app/app.tsx
+++ b/x-pack/plugins/security_solution/public/app/app.tsx
@@ -28,8 +28,6 @@ import { ApolloClientContext } from '../common/utils/apollo_context';
import { ManageGlobalTimeline } from '../timelines/components/manage_timeline';
import { StartServices } from '../types';
import { PageRouter } from './routes';
-import { ManageSource } from '../common/containers/sourcerer';
-
interface StartAppComponent extends AppFrontendLibs {
children: React.ReactNode;
history: History;
@@ -56,15 +54,13 @@ const StartAppComponent: FC = ({ children, apolloClient, hist
-
-
-
-
- {children}
-
-
-
-
+
+
+
+ {children}
+
+
+
diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx
index b48ae4e6e2d75..e0dea199e78ff 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useMemo } from 'react';
+import React, { useRef } from 'react';
import styled from 'styled-components';
import { TimelineId } from '../../../common/types/timeline';
@@ -14,11 +14,12 @@ import { HeaderGlobal } from '../../common/components/header_global';
import { HelpMenu } from '../../common/components/help_menu';
import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning';
import { UseUrlState } from '../../common/components/url_state';
-import { useWithSource } from '../../common/containers/source';
import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline';
import { navTabs } from './home_navigations';
-import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index';
-import { useUserInfo } from '../../detections/components/user_info';
+import { useInitSourcerer, useSourcererScope } from '../../common/containers/sourcerer';
+import { useKibana } from '../../common/lib/kibana';
+import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants';
+import { SourcererScopeName } from '../../common/store/sourcerer/model';
const SecuritySolutionAppWrapper = styled.div`
display: flex;
@@ -42,20 +43,21 @@ interface HomePageProps {
}
const HomePageComponent: React.FC = ({ children }) => {
- const { signalIndexExists, signalIndexName } = useSignalIndex();
+ const { application } = useKibana().services;
+ const subPluginId = useRef('');
- const indexToAdd = useMemo(() => {
- if (signalIndexExists && signalIndexName != null) {
- return [signalIndexName];
- }
- return null;
- }, [signalIndexExists, signalIndexName]);
+ application.currentAppId$.subscribe((appId) => {
+ subPluginId.current = appId ?? '';
+ });
+ useInitSourcerer(
+ subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
+ ? SourcererScopeName.detections
+ : SourcererScopeName.default
+ );
const [showTimeline] = useShowTimeline();
- const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd);
- // side effect: this will attempt to create the signals index if it doesn't exist
- useUserInfo();
+ const { browserFields, indexPattern, indicesExist } = useSourcererScope();
return (
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx
index 0ac6093f2ee04..4f7b17a730b6a 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx
@@ -9,7 +9,9 @@ import React from 'react';
import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page';
import * as i18n from './translations';
-const CaseHeaderPageComponent: React.FC = (props) => ;
+const CaseHeaderPageComponent: React.FC = (props) => (
+
+);
CaseHeaderPageComponent.defaultProps = {
badgeOptions: {
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx
index b23169af6ceb3..ad113d3e7e737 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx
@@ -294,6 +294,7 @@ export const CaseComponent = React.memo(
= ({
defaultModel={alertsDefaultModel}
end={endDate}
id={timelineId}
+ scopeId={SourcererScopeName.default}
start={startDate}
/>
);
diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx
index d522e372d7734..0dcd29a2d965b 100644
--- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx
@@ -24,6 +24,7 @@ const AlertsViewComponent: React.FC = ({
deleteQuery,
endDate,
filterQuery,
+ indexNames,
pageFilters,
setQuery,
startDate,
@@ -62,6 +63,7 @@ const AlertsViewComponent: React.FC = ({
endDate={endDate}
filterQuery={filterQuery}
id={ID}
+ indexNames={indexNames}
setQuery={setQuery}
startDate={startDate}
{...alertsHistogramConfigs}
diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts
index b2637eeb2c65e..280b9111042d0 100644
--- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts
@@ -15,7 +15,7 @@ type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps;
export interface AlertsComponentsProps
extends Pick<
CommonQueryProps,
- 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate'
+ 'deleteQuery' | 'endDate' | 'filterQuery' | 'indexNames' | 'skip' | 'setQuery' | 'startDate'
> {
timelineId: TimelineIdLiteral;
pageFilters: Filter[];
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
index 9e8bde8d9ff92..eaaba90b35634 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx
@@ -6,9 +6,8 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
-import { MockedProvider } from 'react-apollo/test-utils';
-import { mockBrowserFields, mocksSource } from '../../containers/source/mock';
+import { mockBrowserFields } from '../../containers/source/mock';
import { TestProviders } from '../../mock';
import { DragDropContextWrapper } from './drag_drop_context_wrapper';
@@ -20,11 +19,9 @@ describe('DragDropContextWrapper', () => {
const wrapper = shallow(
-
-
- {message}
-
-
+
+ {message}
+
);
expect(wrapper.find('DragDropContextWrapper')).toMatchSnapshot();
@@ -35,11 +32,9 @@ describe('DragDropContextWrapper', () => {
const wrapper = mount(
-
-
- {message}
-
-
+
+ {message}
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
index ebfa9ac22bdc7..46e7298677f49 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx
@@ -6,11 +6,10 @@
import { shallow } from 'enzyme';
import React from 'react';
-import { MockedProvider } from 'react-apollo/test-utils';
import { DraggableStateSnapshot, DraggingStyle } from 'react-beautiful-dnd';
import '../../mock/match_media';
-import { mockBrowserFields, mocksSource } from '../../containers/source/mock';
+import { mockBrowserFields } from '../../containers/source/mock';
import { TestProviders } from '../../mock';
import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers';
import { DragDropContextWrapper } from './drag_drop_context_wrapper';
@@ -30,11 +29,9 @@ describe('DraggableWrapper', () => {
test('it renders against the snapshot', () => {
const wrapper = shallow(
-
-
- message} />
-
-
+
+ message} />
+
);
@@ -44,11 +41,9 @@ describe('DraggableWrapper', () => {
test('it renders the children passed to the render prop', () => {
const wrapper = mount(
-
-
- message} />
-
-
+
+ message} />
+
);
@@ -58,11 +53,9 @@ describe('DraggableWrapper', () => {
test('it does NOT render hover actions when the mouse is NOT over the draggable wrapper', () => {
const wrapper = mount(
-
-
- message} />
-
-
+
+ message} />
+
);
@@ -72,11 +65,9 @@ describe('DraggableWrapper', () => {
test('it renders hover actions when the mouse is over the text of draggable wrapper', () => {
const wrapper = mount(
-
-
- message} />
-
-
+
+ message} />
+
);
@@ -92,11 +83,9 @@ describe('DraggableWrapper', () => {
test('it applies text truncation styling when truncate IS specified (implicit: and the user is not dragging)', () => {
const wrapper = mount(
-
-
- message} truncate />
-
-
+
+ message} truncate />
+
);
@@ -108,11 +97,9 @@ describe('DraggableWrapper', () => {
test('it does NOT apply text truncation styling when truncate is NOT specified', () => {
const wrapper = mount(
-
-
- message} />
-
-
+
+ message} />
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
index b53da42da55f8..8aa926a36988b 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx
@@ -8,14 +8,13 @@ import { mount, ReactWrapper } from 'enzyme';
import React from 'react';
import { coreMock } from '../../../../../../../src/core/public/mocks';
-import { useWithSource } from '../../containers/source';
import { mockBrowserFields } from '../../containers/source/mock';
import '../../mock/match_media';
import { useKibana } from '../../lib/kibana';
import { TestProviders } from '../../mock';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { useAddToTimeline } from '../../hooks/use_add_to_timeline';
-
+import { useSourcererScope } from '../../containers/sourcerer';
import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content';
import {
ManageGlobalTimeline,
@@ -26,12 +25,12 @@ import { TimelineId } from '../../../../common/types/timeline';
jest.mock('../link_to');
jest.mock('../../lib/kibana');
-jest.mock('../../containers/source', () => {
- const original = jest.requireActual('../../containers/source');
+jest.mock('../../containers/sourcerer', () => {
+ const original = jest.requireActual('../../containers/sourcerer');
return {
...original,
- useWithSource: jest.fn(),
+ useSourcererScope: jest.fn(),
};
});
@@ -79,8 +78,10 @@ describe('DraggableWrapperHoverContent', () => {
beforeAll(() => {
// our mock implementation of the useAddToTimeline hook returns a mock startDragToTimeline function:
(useAddToTimeline as jest.Mock).mockReturnValue(jest.fn());
- (useWithSource as jest.Mock).mockReturnValue({
+ (useSourcererScope as jest.Mock).mockReturnValue({
browserFields: mockBrowserFields,
+ selectedPatterns: [],
+ indexPattern: {},
});
});
@@ -203,7 +204,7 @@ describe('DraggableWrapperHoverContent', () => {
wrapper = mount(
);
@@ -311,7 +312,7 @@ describe('DraggableWrapperHoverContent', () => {
{...{
...defaultProps,
onFilterAdded,
- timelineId: 'not-active-timeline',
+ timelineId: TimelineId.test,
value: '',
}}
/>
@@ -606,9 +607,7 @@ describe('DraggableWrapperHoverContent', () => {
test('filter manager, not active timeline', () => {
mount(
-
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
index a951bfa98d64b..8c68551ddd981 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx
@@ -8,7 +8,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { DraggableId } from 'react-beautiful-dnd';
-import { getAllFieldsByName, useWithSource } from '../../containers/source';
+import { getAllFieldsByName } from '../../containers/source';
import { useAddToTimeline } from '../../hooks/use_add_to_timeline';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
import { useKibana } from '../../lib/kibana';
@@ -20,6 +20,8 @@ import * as i18n from './translations';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { TimelineId } from '../../../../common/types/timeline';
import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../../../timelines/components/timeline/styles';
+import { SourcererScopeName } from '../../store/sourcerer/model';
+import { useSourcererScope } from '../../containers/sourcerer';
interface Props {
closePopOver?: () => void;
@@ -49,7 +51,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({
const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [
kibana.services.data.query.filterManager,
]);
- const { getManageTimelineById, getTimelineFilterManager } = useManageTimeline();
+ const { getTimelineFilterManager } = useManageTimeline();
const filterManager = useMemo(
() =>
@@ -65,13 +67,16 @@ const DraggableWrapperHoverContentComponent: React.FC = ({
// this component is rendered in the context of the active timeline. This
// behavior enables the 'All events' view by appending the alerts index
// to the index pattern.
- const { indexToAdd } = useMemo(
- () =>
- timelineId === TimelineId.active
- ? getManageTimelineById(TimelineId.active)
- : { indexToAdd: null },
- [getManageTimelineById, timelineId]
- );
+ const activeScope: SourcererScopeName =
+ timelineId === TimelineId.active
+ ? SourcererScopeName.timeline
+ : timelineId != null &&
+ [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes(
+ timelineId as TimelineId
+ )
+ ? SourcererScopeName.detections
+ : SourcererScopeName.default;
+ const { browserFields, indexPattern, selectedPatterns } = useSourcererScope(activeScope);
const handleStartDragToTimeline = useCallback(() => {
startDragToTimeline();
@@ -121,8 +126,6 @@ const DraggableWrapperHoverContentComponent: React.FC = ({
}
}, [goGetTimelineId, timelineId]);
- const { browserFields, indexPattern } = useWithSource('default', indexToAdd);
-
return (
<>
{!showTopN && value != null && (
@@ -187,7 +190,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({
browserFields={browserFields}
field={field}
indexPattern={indexPattern}
- indexToAdd={indexToAdd}
+ indexNames={selectedPatterns}
onFilterAdded={onFilterAdded}
timelineId={timelineId ?? undefined}
toggleTopN={toggleTopN}
diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
index bd2f01721290f..14d1c37efb8cf 100644
--- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx
@@ -6,9 +6,8 @@
import { shallow } from 'enzyme';
import React from 'react';
-import { MockedProvider } from 'react-apollo/test-utils';
-import { mockBrowserFields, mocksSource } from '../../containers/source/mock';
+import { mockBrowserFields } from '../../containers/source/mock';
import { TestProviders } from '../../mock';
import { DragDropContextWrapper } from './drag_drop_context_wrapper';
@@ -24,11 +23,9 @@ describe('DroppableWrapper', () => {
const wrapper = shallow(
-
-
- {message}
-
-
+
+ {message}
+
);
@@ -40,11 +37,9 @@ describe('DroppableWrapper', () => {
const wrapper = mount(
-
-
- {message}
-
-
+
+ {message}
+
);
@@ -56,13 +51,11 @@ describe('DroppableWrapper', () => {
const wrapper = mount(
-
-
- null} droppableId="testing">
- {message}
-
-
-
+
+ null} droppableId="testing">
+ {message}
+
+
);
@@ -72,14 +65,12 @@ describe('DroppableWrapper', () => {
test('it renders the render prop contents when a render prop is provided', () => {
const wrapper = mount(
-
-
- {`isDraggingOver is: ${isDraggingOver}`}
}
- droppableId="testing"
- />
-
-
+
+ {`isDraggingOver is: ${isDraggingOver}`}
}
+ droppableId="testing"
+ />
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
index 037655f594241..aac1f4f2687eb 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
@@ -8,15 +8,13 @@ import React from 'react';
import useResizeObserver from 'use-resize-observer/polyfilled';
import '../../mock/match_media';
-import { mockIndexPattern, TestProviders } from '../../mock';
-// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
-import { wait as waitFor } from '@testing-library/react';
+import { mockIndexNames, mockIndexPattern, TestProviders } from '../../mock';
import { mockEventViewerResponse } from './mock';
import { StatefulEventsViewer } from '.';
import { EventsViewer } from './events_viewer';
import { defaultHeaders } from './default_headers';
-import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns';
+import { useSourcererScope } from '../../containers/sourcerer';
import { mockBrowserFields, mockDocValueFields } from '../../containers/source/mock';
import { eventsDefaultModel } from './default_model';
import { useMountAppended } from '../../utils/use_mount_appended';
@@ -25,6 +23,7 @@ import { TimelineId } from '../../../../common/types/timeline';
import { KqlMode } from '../../../timelines/store/timeline/model';
import { SortDirection } from '../../../timelines/components/timeline/body/sort';
import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group';
+import { SourcererScopeName } from '../../store/sourcerer/model';
import { useTimelineEvents } from '../../../timelines/containers';
jest.mock('../../../timelines/containers', () => ({
@@ -33,8 +32,8 @@ jest.mock('../../../timelines/containers', () => ({
jest.mock('../../components/url_state/normalize_time_range.ts');
-const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock;
-jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns');
+const mockUseSourcererScope: jest.Mock = useSourcererScope as jest.Mock;
+jest.mock('../../containers/sourcerer');
const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock;
jest.mock('use-resize-observer/polyfilled');
@@ -45,9 +44,10 @@ const to = '2019-08-27T22:10:56.794Z';
const defaultMocks = {
browserFields: mockBrowserFields,
- indexPatterns: mockIndexPattern,
docValueFields: mockDocValueFields,
- isLoading: false,
+ indexPattern: mockIndexPattern,
+ loading: false,
+ selectedPatterns: mockIndexNames,
};
const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => (
@@ -63,6 +63,7 @@ const eventsViewerDefaultProps = {
end: to,
filters: [],
id: TimelineId.detectionsPage,
+ indexNames: mockIndexNames,
indexPattern: mockIndexPattern,
isLive: false,
isLoadingIndexPattern: false,
@@ -79,6 +80,7 @@ const eventsViewerDefaultProps = {
columnId: 'foo',
sortDirection: 'none' as SortDirection,
},
+ scopeId: SourcererScopeName.timeline,
toggleColumn: jest.fn(),
utilityBar,
};
@@ -86,154 +88,57 @@ const eventsViewerDefaultProps = {
describe('EventsViewer', () => {
const mount = useMountAppended();
+ let testProps = {
+ defaultModel: eventsDefaultModel,
+ end: to,
+ id: 'test-stateful-events-viewer',
+ start: from,
+ scopeId: SourcererScopeName.timeline,
+ };
+
beforeEach(() => {
(useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]);
- mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks }]);
});
-
- test('it renders the "Showing..." subtitle with the expected event count', async () => {
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual(
- 'Showing: 12 events'
- );
- });
+ beforeAll(() => {
+ mockUseSourcererScope.mockImplementation(() => defaultMocks);
});
-
- test('it does NOT render fetch index pattern is loading', async () => {
- mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]);
-
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
- false
+ describe('rendering', () => {
+ test('it renders the "Showing..." subtitle with the expected event count', () => {
+ const wrapper = mount(
+
+
+
);
- });
- });
-
- test('it does NOT render when start is empty', async () => {
- mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]);
-
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
- false
+ expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual(
+ 'Showing: 12 events'
);
});
- });
- test('it does NOT render when end is empty', async () => {
- mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]);
-
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
- false
+ test('it renders the Fields Browser as a settings gear', () => {
+ const wrapper = mount(
+
+
+
);
- });
- });
-
- test('it renders the Fields Browser as a settings gear', async () => {
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- wrapper.update();
-
expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true);
});
- });
-
- test('it renders the footer containing the Load More button', async () => {
- const wrapper = mount(
-
-
-
- );
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="timeline-pagination"]`).first().exists()).toBe(true);
- });
- });
-
- defaultHeaders.forEach((header) => {
- test(`it renders the ${header.id} default EventsViewer column header`, async () => {
+ // TO DO sourcerer @X
+ test('it renders the footer containing the pagination', () => {
const wrapper = mount(
-
+
);
+ expect(wrapper.find(`[data-test-subj="timeline-pagination"]`).first().exists()).toBe(true);
+ });
- await waitFor(() => {
- wrapper.update();
+ defaultHeaders.forEach((header) => {
+ test(`it renders the ${header.id} default EventsViewer column header`, () => {
+ const wrapper = mount(
+
+
+
+ );
defaultHeaders.forEach((h) =>
expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe(
@@ -242,10 +147,58 @@ describe('EventsViewer', () => {
);
});
});
+ describe('loading', () => {
+ beforeAll(() => {
+ mockUseSourcererScope.mockImplementation(() => ({ ...defaultMocks, loading: true }));
+ });
+ test('it does NOT render fetch index pattern is loading', () => {
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
+ false
+ );
+ });
+
+ test('it does NOT render when start is empty', () => {
+ testProps = {
+ ...testProps,
+ start: '',
+ };
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
+ false
+ );
+ });
+
+ test('it does NOT render when end is empty', () => {
+ testProps = {
+ ...testProps,
+ end: '',
+ };
+ const wrapper = mount(
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
+ false
+ );
+ });
+ });
});
describe('headerFilterGroup', () => {
- test('it renders the provided headerFilterGroup', async () => {
+ test('it renders the provided headerFilterGroup', () => {
const wrapper = mount(
{
/>
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true);
- });
+ expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true);
});
- test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is undefined', async () => {
+ test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is undefined', () => {
const wrapper = mount(
{
/>
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(
- wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
- ).not.toHaveStyleRule('visibility', 'hidden');
- });
+ expect(
+ wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
+ ).not.toHaveStyleRule('visibility', 'hidden');
});
- test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is an empty string', async () => {
+ test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is an empty string', () => {
const wrapper = mount(
{
/>
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(
- wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
- ).not.toHaveStyleRule('visibility', 'hidden');
- });
+ expect(
+ wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
+ ).not.toHaveStyleRule('visibility', 'hidden');
});
- test('it does NOT have a visible HeaderFilterGroupWrapper when Resolver is showing, because graphEventId is a valid id', async () => {
+ test('it does NOT have a visible HeaderFilterGroupWrapper when Resolver is showing, because graphEventId is a valid id', () => {
const wrapper = mount(
{
/>
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(
- wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
- ).toHaveStyleRule('visibility', 'hidden');
- });
+ expect(
+ wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
+ ).toHaveStyleRule('visibility', 'hidden');
});
- test('it (still) renders an invisible headerFilterGroup (to maintain state while hidden) when Resolver is showing, because graphEventId is a valid id', async () => {
+ test('it (still) renders an invisible headerFilterGroup (to maintain state while hidden) when Resolver is showing, because graphEventId is a valid id', () => {
const wrapper = mount(
{
/>
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true);
- });
+ expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true);
});
});
describe('utilityBar', () => {
- test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', async () => {
+ test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', () => {
const wrapper = mount(
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true);
- });
+ expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true);
});
- test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is an empty string', async () => {
+ test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is an empty string', () => {
const wrapper = mount(
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true);
- });
+ expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true);
});
- test('it does NOT render the provided utilityBar when Resolver is showing, because graphEventId is a valid id', async () => {
+ test('it does NOT render the provided utilityBar when Resolver is showing, because graphEventId is a valid id', () => {
const wrapper = mount(
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(false);
- });
+ expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(false);
});
});
describe('header inspect button', () => {
- test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', async () => {
+ test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', () => {
const wrapper = mount(
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true);
- });
+ expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true);
});
- test('it renders the inspect button when Resolver is NOT showing, because graphEventId is an empty string', async () => {
+ test('it renders the inspect button when Resolver is NOT showing, because graphEventId is an empty string', () => {
const wrapper = mount(
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true);
- });
+ expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true);
});
- test('it does NOT render the inspect button when Resolver is showing, because graphEventId is a valid id', async () => {
+ test('it does NOT render the inspect button when Resolver is showing, because graphEventId is a valid id', () => {
const wrapper = mount(
);
-
- await waitFor(() => {
- wrapper.update();
-
- expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(false);
- });
+ expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(false);
});
});
});
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 2998bd031d674..2c8c8136a4733 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
@@ -95,6 +95,7 @@ interface Props {
headerFilterGroup?: React.ReactNode;
height?: number;
id: string;
+ indexNames: string[];
indexPattern: IIndexPattern;
isLive: boolean;
isLoadingIndexPattern: boolean;
@@ -121,6 +122,7 @@ const EventsViewerComponent: React.FC = ({
filters,
headerFilterGroup,
id,
+ indexNames,
indexPattern,
isLive,
isLoadingIndexPattern,
@@ -213,7 +215,7 @@ const EventsViewerComponent: React.FC = ({
fields,
filterQuery: combinedQueries!.filterQuery,
id,
- indexPattern,
+ indexNames,
limit: itemsPerPage,
sort: sortField,
startDate: start,
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
index 8c61281422c2a..9a3c0fa1cad2e 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx
@@ -10,14 +10,13 @@ import useResizeObserver from 'use-resize-observer/polyfilled';
import '../../mock/match_media';
// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
import { wait as waitFor } from '@testing-library/react';
-import { mockIndexPattern, TestProviders } from '../../mock';
+import { TestProviders } from '../../mock';
import { useMountAppended } from '../../utils/use_mount_appended';
import { mockEventViewerResponse } from './mock';
import { StatefulEventsViewer } from '.';
-import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns';
-import { mockBrowserFields } from '../../containers/source/mock';
import { eventsDefaultModel } from './default_model';
+import { SourcererScopeName } from '../../store/sourcerer/model';
import { useTimelineEvents } from '../../../timelines/containers';
jest.mock('../../../timelines/containers', () => ({
@@ -26,15 +25,6 @@ jest.mock('../../../timelines/containers', () => ({
jest.mock('../../components/url_state/normalize_time_range.ts');
-const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock;
-jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns');
-mockUseFetchIndexPatterns.mockImplementation(() => [
- {
- browserFields: mockBrowserFields,
- indexPatterns: mockIndexPattern,
- },
-]);
-
const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock;
jest.mock('use-resize-observer/polyfilled');
mockUseResizeObserver.mockImplementation(() => ({}));
@@ -42,6 +32,14 @@ mockUseResizeObserver.mockImplementation(() => ({}));
const from = '2019-08-27T22:10:56.794Z';
const to = '2019-08-26T22:10:56.791Z';
+const testProps = {
+ defaultModel: eventsDefaultModel,
+ end: to,
+ indexNames: [],
+ id: 'test-stateful-events-viewer',
+ scopeId: SourcererScopeName.default,
+ start: from,
+};
describe('StatefulEventsViewer', () => {
const mount = useMountAppended();
@@ -50,12 +48,7 @@ describe('StatefulEventsViewer', () => {
test('it renders the events viewer', async () => {
const wrapper = mount(
-
+
);
@@ -70,12 +63,7 @@ describe('StatefulEventsViewer', () => {
test('it renders InspectButtonContainer', async () => {
const wrapper = mount(
-
+
);
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
index e4520dab4626a..cd43c7e493065 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx
@@ -9,7 +9,6 @@ import { connect, ConnectedProps } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import styled from 'styled-components';
-import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
import { inputsModel, inputsSelectors, State } from '../../store';
import { inputsActions } from '../../store/actions';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
@@ -20,11 +19,11 @@ import {
} from '../../../timelines/store/timeline/model';
import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/events';
import { Filter } from '../../../../../../../src/plugins/data/public';
-import { useUiSetting } from '../../lib/kibana';
import { EventsViewer } from './events_viewer';
-import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns';
import { InspectButtonContainer } from '../inspect';
import { useFullScreen } from '../../containers/use_full_screen';
+import { SourcererScopeName } from '../../store/sourcerer/model';
+import { useSourcererScope } from '../../containers/sourcerer';
const DEFAULT_EVENTS_VIEWER_HEIGHT = 652;
@@ -35,10 +34,10 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
`;
export interface OwnProps {
- defaultIndices?: string[];
defaultModel: SubsetTimelineModel;
end: string;
id: string;
+ scopeId: SourcererScopeName;
start: string;
headerFilterGroup?: React.ReactNode;
pageFilters?: Filter[];
@@ -52,7 +51,6 @@ const StatefulEventsViewerComponent: React.FC = ({
columns,
dataProviders,
deletedEventIds,
- defaultIndices,
deleteEventQuery,
end,
excludedRowRendererIds,
@@ -67,6 +65,7 @@ const StatefulEventsViewerComponent: React.FC = ({
query,
removeColumn,
start,
+ scopeId,
showCheckboxes,
sort,
updateItemsPerPage,
@@ -75,13 +74,13 @@ const StatefulEventsViewerComponent: React.FC = ({
// If truthy, the graph viewer (Resolver) is showing
graphEventId,
}) => {
- const [
- { docValueFields, browserFields, indexPatterns, isLoading: isLoadingIndexPattern },
- ] = useFetchIndexPatterns(
- defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY),
- 'events_viewer'
- );
-
+ const {
+ browserFields,
+ docValueFields,
+ indexPattern,
+ selectedPatterns,
+ loading: isLoadingIndexPattern,
+ } = useSourcererScope(scopeId);
const { globalFullScreen } = useFullScreen();
useEffect(() => {
@@ -90,6 +89,7 @@ const StatefulEventsViewerComponent: React.FC = ({
id,
columns,
excludedRowRendererIds,
+ indexNames: selectedPatterns,
sort,
itemsPerPage,
showCheckboxes,
@@ -144,7 +144,8 @@ const StatefulEventsViewerComponent: React.FC = ({
isLoadingIndexPattern={isLoadingIndexPattern}
filters={globalFilters}
headerFilterGroup={headerFilterGroup}
- indexPattern={indexPatterns}
+ indexNames={selectedPatterns}
+ indexPattern={indexPattern}
isLive={isLive}
itemsPerPage={itemsPerPage!}
itemsPerPageOptions={itemsPerPageOptions!}
@@ -222,8 +223,8 @@ export const StatefulEventsViewer = connector(
StatefulEventsViewerComponent,
(prevProps, nextProps) =>
prevProps.id === nextProps.id &&
+ prevProps.scopeId === nextProps.scopeId &&
deepEqual(prevProps.columns, nextProps.columns) &&
- deepEqual(prevProps.defaultIndices, nextProps.defaultIndices) &&
deepEqual(prevProps.dataProviders, nextProps.dataProviders) &&
deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) &&
prevProps.deletedEventIds === nextProps.deletedEventIds &&
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
index 691a7d99d9345..ed1c1c1cdad1f 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx
@@ -13,7 +13,7 @@ import { act } from 'react-dom/test-utils';
import { AddExceptionModal } from './';
import { useCurrentUser } from '../../../../common/lib/kibana';
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
-import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
+import { useFetchIndex } from '../../../containers/source';
import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub';
import { useAddOrUpdateException } from '../use_add_exception';
import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list';
@@ -28,6 +28,7 @@ import { ExceptionListItemSchema } from '../../../../../../lists/common';
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
jest.mock('../../../../common/lib/kibana');
+jest.mock('../../../containers/source');
jest.mock('../../../../detections/containers/detection_engine/rules');
jest.mock('../use_add_exception');
jest.mock('../use_fetch_or_create_rule_exception_list');
@@ -59,9 +60,9 @@ describe('When the add exception modal is opened', () => {
loading: false,
signalIndexName: 'mock-siem-signals-index',
}));
- (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [
+ (useFetchIndex as jest.Mock).mockImplementation(() => [
+ false,
{
- isLoading: false,
indexPatterns: stubIndexPattern,
},
]);
@@ -77,9 +78,9 @@ describe('When the add exception modal is opened', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
// Mocks one of the hooks as loading
- (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [
+ (useFetchIndex as jest.Mock).mockImplementation(() => [
+ true,
{
- isLoading: true,
indexPatterns: stubIndexPattern,
},
]);
@@ -244,9 +245,9 @@ describe('When the add exception modal is opened', () => {
};
beforeEach(() => {
// Mocks the index patterns to contain the pre-populated endpoint fields so that the exception qualifies as bulk closable
- (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [
+ (useFetchIndex as jest.Mock).mockImplementation(() => [
+ false,
{
- isLoading: false,
indexPatterns: {
...stubIndexPattern,
fields: [
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
index 721e53732c093..e945461f53e81 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx
@@ -50,8 +50,8 @@ import {
getMappedNonEcsValue,
} from '../helpers';
import { ErrorInfo, ErrorCallout } from '../error_callout';
-import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
import { ExceptionsBuilderExceptionItem } from '../types';
+import { useFetchIndex } from '../../../containers/source';
export interface AddExceptionModalBaseProps {
ruleName: string;
@@ -122,14 +122,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({
const [fetchOrCreateListError, setFetchOrCreateListError] = useState(null);
const { addError, addSuccess, addWarning } = useAppToasts();
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
- const [
- { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns },
- ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : [], 'signals');
-
- const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(
- ruleIndices,
- 'rules'
+ const memoSignalIndexName = useMemo(() => (signalIndexName !== null ? [signalIndexName] : []), [
+ signalIndexName,
+ ]);
+ const [isSignalIndexPatternLoading, { indexPatterns: signalIndexPatterns }] = useFetchIndex(
+ memoSignalIndexName
);
+ const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices);
const onError = useCallback(
(error: Error): void => {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx
index c724e6a2c711f..d5d2091cc9bc8 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx
@@ -12,7 +12,7 @@ import { act } from 'react-dom/test-utils';
import { EditExceptionModal } from './';
import { useCurrentUser } from '../../../../common/lib/kibana';
-import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
+import { useFetchIndex } from '../../../containers/source';
import {
stubIndexPattern,
stubIndexPatternWithFields,
@@ -26,6 +26,7 @@ import * as builder from '../builder';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../detections/containers/detection_engine/rules');
jest.mock('../use_add_exception');
+jest.mock('../../../containers/source');
jest.mock('../use_fetch_or_create_rule_exception_list');
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
jest.mock('../builder');
@@ -50,9 +51,9 @@ describe('When the edit exception modal is opened', () => {
{ isLoading: false },
jest.fn(),
]);
- (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [
+ (useFetchIndex as jest.Mock).mockImplementation(() => [
+ false,
{
- isLoading: false,
indexPatterns: stubIndexPatternWithFields,
},
]);
@@ -67,9 +68,9 @@ describe('When the edit exception modal is opened', () => {
describe('when the modal is loading', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
- (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [
+ (useFetchIndex as jest.Mock).mockImplementation(() => [
+ true,
{
- isLoading: true,
indexPatterns: stubIndexPattern,
},
]);
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
index 5dbf319c3299d..128686428598c 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx
@@ -21,7 +21,8 @@ import {
EuiText,
EuiCallOut,
} from '@elastic/eui';
-import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
+
+import { useFetchIndex } from '../../../containers/source';
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async';
import {
@@ -108,15 +109,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({
>([]);
const { addError, addSuccess } = useAppToasts();
const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex();
- const [
- { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns },
- ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : [], 'signals');
-
- const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(
- ruleIndices,
- 'rules'
+ const [isSignalIndexPatternLoading, { indexPatterns: signalIndexPatterns }] = useFetchIndex(
+ signalIndexName !== null ? [signalIndexName] : []
);
+ const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices);
+
const handleExceptionUpdateError = useCallback(
(error: Error, statusCode: number | null, message: string | null) => {
if (error.message.includes('Conflict')) {
diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
index e05e3c2e9aeb1..5b4dd2e9728bb 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx
@@ -18,7 +18,6 @@ import { getAppOverviewUrl } from '../link_to';
import { MlPopover } from '../ml_popover/ml_popover';
import { SiemNavigation } from '../navigation';
import * as i18n from './translations';
-import { useWithSource } from '../../containers/source';
import { useGetUrlSearch } from '../navigation/use_get_url_search';
import { useKibana } from '../../lib/kibana';
import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants';
@@ -58,11 +57,12 @@ interface HeaderGlobalProps {
hideDetectionEngine?: boolean;
}
export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => {
- const { indicesExist } = useWithSource();
const { globalHeaderPortalNode } = useGlobalHeaderPortal();
const { globalFullScreen } = useFullScreen();
const search = useGetUrlSearch(navTabs.overview);
- const { navigateToApp } = useKibana().services.application;
+ const { application, http } = useKibana().services;
+ const { navigateToApp } = application;
+ const basePath = http.basePath.get();
const goToOverview = useCallback(
(ev) => {
ev.preventDefault();
@@ -104,7 +104,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine
- {indicesExist && window.location.pathname.includes(APP_DETECTIONS_PATH) && (
+ {window.location.pathname.includes(APP_DETECTIONS_PATH) && (
@@ -113,7 +113,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine
{i18n.BUTTON_ADD_DATA}
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
index a100f5e4f93b4..a2a36b3fe1d3b 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap
@@ -36,5 +36,8 @@ exports[`HeaderPage it renders 1`] = `
+
`;
diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
index 62880e7510cd2..0cb721bb5382f 100644
--- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx
@@ -15,6 +15,8 @@ import { Title } from './title';
import { DraggableArguments, BadgeOptions, TitleProp } from './types';
import { useFormatUrl } from '../link_to';
import { SecurityPageName } from '../../../app/types';
+import { Sourcerer } from '../sourcerer';
+import { SourcererScopeName } from '../../store/sourcerer/model';
interface HeaderProps {
border?: boolean;
@@ -72,6 +74,7 @@ export interface HeaderPageProps extends HeaderProps {
badgeOptions?: BadgeOptions;
children?: React.ReactNode;
draggableArguments?: DraggableArguments;
+ hideSourcerer?: boolean;
subtitle?: SubtitleProps['items'];
subtitle2?: SubtitleProps['items'];
title: TitleProp;
@@ -84,6 +87,7 @@ const HeaderPageComponent: React.FC = ({
border,
children,
draggableArguments,
+ hideSourcerer = false,
isLoading,
subtitle,
subtitle2,
@@ -138,6 +142,7 @@ const HeaderPageComponent: React.FC = ({
)}
+ {!hideSourcerer && }
);
};
diff --git a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx
index 9473ba67a1c4f..c2800b0705b43 100644
--- a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx
@@ -37,7 +37,7 @@ describe('Last Event Time Stat', () => {
]);
const wrapper = mount(
-
+
);
expect(wrapper.html()).toBe(
@@ -54,7 +54,7 @@ describe('Last Event Time Stat', () => {
]);
const wrapper = mount(
-
+
);
expect(wrapper.html()).toBe('Last event: 12 minutes ago');
@@ -69,7 +69,7 @@ describe('Last Event Time Stat', () => {
]);
const wrapper = mount(
-
+
);
@@ -85,7 +85,7 @@ describe('Last Event Time Stat', () => {
]);
const wrapper = mount(
-
+
);
expect(wrapper.html()).toContain(getEmptyValue());
diff --git a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.tsx b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.tsx
index e9e8e7a03017c..d508040f84239 100644
--- a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.tsx
@@ -8,58 +8,65 @@ import { EuiIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { memo } from 'react';
+import { DocValueFields } from '../../../../common/search_strategy';
import { LastEventIndexKey } from '../../../graphql/types';
import { useTimelineLastEventTime } from '../../containers/events/last_event_time';
import { getEmptyTagValue } from '../empty_value';
import { FormattedRelativePreferenceDate } from '../formatted_date';
export interface LastEventTimeProps {
+ docValueFields: DocValueFields[];
hostName?: string;
indexKey: LastEventIndexKey;
ip?: string;
+ indexNames: string[];
}
-export const LastEventTime = memo(({ hostName, indexKey, ip }) => {
- const [loading, { lastSeen, errorMessage }] = useTimelineLastEventTime({
- indexKey,
- details: {
- hostName,
- ip,
- },
- });
+export const LastEventTime = memo(
+ ({ docValueFields, hostName, indexKey, ip, indexNames }) => {
+ const [loading, { lastSeen, errorMessage }] = useTimelineLastEventTime({
+ docValueFields,
+ indexKey,
+ indexNames,
+ details: {
+ hostName,
+ ip,
+ },
+ });
+
+ if (errorMessage != null) {
+ return (
+
+
+
+ );
+ }
- if (errorMessage != null) {
return (
-
-
-
+ <>
+ {loading && }
+ {!loading && lastSeen != null && new Date(lastSeen).toString() === 'Invalid Date'
+ ? lastSeen
+ : !loading &&
+ lastSeen != null && (
+ ,
+ }}
+ />
+ )}
+ {!loading && lastSeen == null && getEmptyTagValue()}
+ >
);
}
-
- return (
- <>
- {loading && }
- {!loading && lastSeen != null && new Date(lastSeen).toString() === 'Invalid Date'
- ? lastSeen
- : !loading &&
- lastSeen != null && (
- ,
- }}
- />
- )}
- {!loading && lastSeen == null && getEmptyTagValue()}
- >
- );
-});
+);
LastEventTime.displayName = 'LastEventTime';
diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx
index 7286c6b743692..99dc8a802b33d 100644
--- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx
@@ -47,6 +47,7 @@ describe('Matrix Histogram Component', () => {
errorMessage: 'error',
histogramType: MatrixHistogramType.alerts,
id: 'mockId',
+ indexNames: [],
isInspected: false,
isPtrIncluded: false,
setQuery: jest.fn(),
diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx
index 485ca4c93133a..e7d7e60a3c408 100644
--- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx
@@ -37,7 +37,6 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps &
hideHistogramIfEmpty?: boolean;
histogramType: MatrixHistogramType;
id: string;
- indexToAdd?: string[] | null;
legendPosition?: Position;
mapping?: MatrixHistogramMappingTypes;
showSpacer?: boolean;
@@ -72,7 +71,7 @@ export const MatrixHistogramComponent: React.FC =
histogramType,
hideHistogramIfEmpty = false,
id,
- indexToAdd,
+ indexNames,
legendPosition,
mapping,
panelHeight = DEFAULT_PANEL_HEIGHT,
@@ -136,7 +135,7 @@ export const MatrixHistogramComponent: React.FC =
errorMessage,
filterQuery,
histogramType,
- indexToAdd,
+ indexNames,
startDate,
stackByField: selectedStackByOption.value,
});
diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts
index fc1df4d8ca85f..9a892110bde43 100644
--- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts
@@ -59,6 +59,7 @@ interface MatrixHistogramBasicProps {
export interface MatrixHistogramQueryProps {
endDate: string;
errorMessage: string;
+ indexNames: string[];
filterQuery?: ESQuery | string | undefined;
setAbsoluteRangeDatePicker?: ActionCreator<{
id: InputsModelId;
@@ -68,7 +69,6 @@ export interface MatrixHistogramQueryProps {
setAbsoluteRangeDatePickerTarget?: InputsModelId;
stackByField: string;
startDate: string;
- indexToAdd?: string[] | null;
histogramType: MatrixHistogramType;
}
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 89aa77106933e..da5099f61e9b2 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
@@ -105,6 +105,7 @@ const getMockObject = (
},
},
},
+ sourcerer: {},
});
const getUrlForAppMock = (appId: string, options?: { path?: string; absolute?: boolean }) =>
@@ -130,7 +131,7 @@ describe('Navigation Breadcrumbs', () => {
},
{
href:
- "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
text: 'Hosts',
},
{
@@ -150,7 +151,7 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Network',
href:
- "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: 'Flows',
@@ -169,7 +170,7 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Timelines',
href:
- "securitySolution:timelines?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
]);
});
@@ -184,12 +185,12 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Hosts',
href:
- "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: 'siem-kibana',
href:
- "securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{ text: 'Authentications', href: '' },
]);
@@ -205,11 +206,11 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Network',
href:
- "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: ipv4,
- href: `securitySolution:network/ip/${ipv4}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
+ href: `securitySolution:network/ip/${ipv4}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
{ text: 'Flows', href: '' },
]);
@@ -225,11 +226,11 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Network',
href:
- "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: ipv6,
- href: `securitySolution:network/ip/${ipv6Encoded}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
+ href: `securitySolution:network/ip/${ipv6Encoded}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
{ text: 'Flows', href: '' },
]);
@@ -245,7 +246,7 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Detections',
href:
- "securitySolution:detections?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:detections?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
]);
});
@@ -259,7 +260,7 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Cases',
href:
- "securitySolution:case?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:case?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
]);
});
@@ -280,11 +281,11 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Cases',
href:
- "securitySolution:case?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:case?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: sampleCase.name,
- href: `securitySolution:case/${sampleCase.id}?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
+ href: `securitySolution:case/${sampleCase.id}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
]);
});
@@ -311,12 +312,12 @@ describe('Navigation Breadcrumbs', () => {
{
text: 'Hosts',
href:
- "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: 'siem-kibana',
href:
- "securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "securitySolution:hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{ text: 'Authentications', href: '' },
]);
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts
index 8f5a3ac63fa1a..ed71f55fd0161 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts
@@ -19,12 +19,19 @@ import {
import { Query, Filter } from '../../../../../../../src/plugins/data/public';
import { SearchNavTab } from './types';
+import { SourcererScopePatterns } from '../../store/sourcerer/model';
export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => {
if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) {
return URL_STATE_KEYS[tab.urlKey].reduce(
(myLocation: Location, urlKey: KeyUrlState) => {
- let urlStateToReplace: UrlInputsModel | Query | Filter[] | TimelineUrl | string = '';
+ let urlStateToReplace:
+ | Filter[]
+ | Query
+ | SourcererScopePatterns
+ | TimelineUrl
+ | UrlInputsModel
+ | string = '';
if (urlKey === CONSTANTS.appQuery && urlState.query != null) {
if (urlState.query.query === '') {
@@ -40,6 +47,8 @@ export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => {
}
} else if (urlKey === CONSTANTS.timerange) {
urlStateToReplace = urlState[CONSTANTS.timerange];
+ } else if (urlKey === CONSTANTS.sourcerer) {
+ urlStateToReplace = urlState[CONSTANTS.sourcerer];
} else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) {
const timeline = urlState[CONSTANTS.timeline];
if (timeline.id === '') {
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 16cb19f5a0c14..102ed7851e57d 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
@@ -78,6 +78,7 @@ describe('SIEM Navigation', () => {
},
[CONSTANTS.appQuery]: { query: '', language: 'kuery' },
[CONSTANTS.filters]: [],
+ [CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
id: '',
isOpen: false,
@@ -145,6 +146,7 @@ describe('SIEM Navigation', () => {
pageName: 'hosts',
pathName: '/',
search: '',
+ sourcerer: {},
state: undefined,
tabName: 'authentications',
query: { query: '', language: 'kuery' },
@@ -252,6 +254,7 @@ describe('SIEM Navigation', () => {
query: { language: 'kuery', query: '' },
savedQuery: undefined,
search: '',
+ sourcerer: {},
state: undefined,
tabName: 'authentications',
timeline: { id: '', isOpen: false, graphEventId: '' },
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx
index 5ee35e7da0f3e..b149488ff38a7 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx
@@ -40,19 +40,20 @@ export const SiemNavigationComponent: React.FC<
if (pathName || pageName) {
setBreadcrumbs(
{
- query: urlState.query,
detailName,
filters: urlState.filters,
+ flowTarget,
navTabs,
pageName,
pathName,
+ query: urlState.query,
savedQuery: urlState.savedQuery,
search,
+ sourcerer: urlState.sourcerer,
+ state,
tabName,
- flowTarget,
- timerange: urlState.timerange,
timeline: urlState.timeline,
- state,
+ timerange: urlState.timerange,
},
chrome,
getUrlForApp
@@ -69,6 +70,7 @@ export const SiemNavigationComponent: React.FC<
navTabs={navTabs}
pageName={pageName}
pathName={pathName}
+ sourcerer={urlState.sourcerer}
savedQuery={urlState.savedQuery}
tabName={tabName}
timeline={urlState.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 b25cf3779801b..5c69edbabdc66 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
@@ -68,6 +68,7 @@ describe('Tab Navigation', () => {
},
[CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' },
[CONSTANTS.filters]: [],
+ [CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
id: '',
isOpen: false,
@@ -126,6 +127,7 @@ describe('Tab Navigation', () => {
},
[CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' },
[CONSTANTS.filters]: [],
+ [CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
id: '',
isOpen: false,
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx
index 217ad0e58570f..3eb66b5591b85 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx
@@ -94,10 +94,17 @@ export const TabNavigationComponent = (props: TabNavigationProps) => {
() =>
Object.values(navTabs).map((tab) => {
const isSelected = selectedTabId === tab.id;
- const { query, filters, savedQuery, timerange, timeline } = props;
- const search = getSearch(tab, { query, filters, savedQuery, timerange, timeline });
+ const { filters, query, savedQuery, sourcerer, timeline, timerange } = props;
+ const search = getSearch(tab, {
+ filters,
+ query,
+ savedQuery,
+ sourcerer,
+ timeline,
+ timerange,
+ });
const hrefWithSearch =
- tab.href + getSearch(tab, { query, filters, savedQuery, timerange, timeline });
+ tab.href + getSearch(tab, { filters, query, savedQuery, sourcerer, timeline, timerange });
return (
{
- const original = jest.requireActual('../../containers/sourcerer');
+const mockDispatch = jest.fn();
+jest.mock('react-redux', () => {
+ const original = jest.requireActual('react-redux');
return {
...original,
- useManageSource: () => mockManageSource,
+ useDispatch: () => mockDispatch,
};
});
const mockOptions = [
- { label: 'auditbeat-*', key: 'auditbeat-*-0', value: 'auditbeat-*', checked: 'on' },
- { label: 'endgame-*', key: 'endgame-*-1', value: 'endgame-*', checked: 'on' },
- { label: 'filebeat-*', key: 'filebeat-*-2', value: 'filebeat-*', checked: 'on' },
- { label: 'logs-*', key: 'logs-*-3', value: 'logs-*', checked: 'on' },
- { label: 'packetbeat-*', key: 'packetbeat-*-4', value: 'packetbeat-*', checked: undefined },
- { label: 'winlogbeat-*', key: 'winlogbeat-*-5', value: 'winlogbeat-*', checked: 'on' },
- {
- label: 'apm-*-transaction*',
- key: 'apm-*-transaction*-0',
- value: 'apm-*-transaction*',
- disabled: true,
- checked: undefined,
- },
- {
- label: 'blobbeat-*',
- key: 'blobbeat-*-1',
- value: 'blobbeat-*',
- disabled: true,
- checked: undefined,
- },
+ { label: 'apm-*-transaction*', value: 'apm-*-transaction*' },
+ { label: 'auditbeat-*', value: 'auditbeat-*' },
+ { label: 'endgame-*', value: 'endgame-*' },
+ { label: 'filebeat-*', value: 'filebeat-*' },
+ { label: 'logs-*', value: 'logs-*' },
+ { label: 'packetbeat-*', value: 'packetbeat-*' },
+ { label: 'winlogbeat-*', value: 'winlogbeat-*' },
];
+const defaultProps = {
+ scope: sourcererModel.SourcererScopeName.default,
+};
describe('Sourcerer component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
+ });
+ const state: State = mockGlobalState;
+ const { storage } = createSecuritySolutionStorageMock();
+ let store = createStore(
+ state,
+ SUB_PLUGINS_REDUCER,
+ apolloClientObservable,
+ kibanaObservable,
+ storage
+ );
+
+ beforeEach(() => {
+ store = createStore(
+ state,
+ SUB_PLUGINS_REDUCER,
+ apolloClientObservable,
+ kibanaObservable,
+ storage
+ );
+ });
+
// Using props callback instead of simulating clicks,
// because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects
- it('Mounts with correct options selected and disabled', () => {
- const wrapper = mount();
+ it('Mounts with all options selected', () => {
+ const wrapper = mount(
+
+
+
+ );
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
-
expect(
- wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('options')
+ wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('selectedOptions')
).toEqual(mockOptions);
});
- it('onChange calls updateSourceGroupIndicies', () => {
- const wrapper = mount();
- wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
-
- const switcherOnChange = wrapper
- .find(`[data-test-subj="indexPattern-switcher"]`)
- .first()
- .prop('onChange');
- // @ts-ignore
- switcherOnChange([mockOptions[0], mockOptions[1]]);
- expect(updateSourceGroupIndicies).toHaveBeenCalledWith(SecurityPageName.default, [
- mockOptions[0].value,
- mockOptions[1].value,
- ]);
- });
- it('Disabled options have icon tooltip', () => {
- const wrapper = mount();
- wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
- // @ts-ignore
- const Rendered = wrapper
- .find(`[data-test-subj="indexPattern-switcher"]`)
- .first()
- .prop('renderOption')(
- {
- label: 'blobbeat-*',
- key: 'blobbeat-*-1',
- value: 'blobbeat-*',
- disabled: true,
- checked: undefined,
+ it('Mounts with some options selected', () => {
+ const state2 = {
+ ...mockGlobalState,
+ sourcerer: {
+ ...mockGlobalState.sourcerer,
+ sourcererScopes: {
+ ...mockGlobalState.sourcerer.sourcererScopes,
+ [SourcererScopeName.default]: {
+ ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
+ loading: false,
+ selectedPatterns: [DEFAULT_INDEX_PATTERN[0]],
+ },
+ },
},
- ''
+ };
+
+ store = createStore(
+ state2,
+ SUB_PLUGINS_REDUCER,
+ apolloClientObservable,
+ kibanaObservable,
+ storage
+ );
+ const wrapper = mount(
+
+
+
);
- expect(Rendered.props.children[1].props.content).toEqual(i18n.DISABLED_INDEX_PATTERNS);
+ wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
+ expect(
+ wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('selectedOptions')
+ ).toEqual([mockOptions[0]]);
});
-
- it('Button links to index path', () => {
- const wrapper = mount();
+ it('onChange calls updateSourcererScopeIndices', async () => {
+ const wrapper = mount(
+
+
+
+ );
+ expect(true).toBeTruthy();
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
- expect(wrapper.find(`[data-test-subj="add-index"]`).first().prop('href')).toEqual(
- ADD_INDEX_PATH
+ await act(async () => {
+ ((wrapper.find(EuiComboBox).props() as unknown) as {
+ onChange: (a: EuiComboBoxOptionOption[]) => void;
+ }).onChange([mockOptions[0], mockOptions[1]]);
+ await waitFor(() => {
+ wrapper.update();
+ });
+ });
+ wrapper.find(`[data-test-subj="add-index"]`).first().simulate('click');
+
+ expect(mockDispatch).toHaveBeenCalledWith(
+ sourcererActions.setSelectedIndexPatterns({
+ id: SourcererScopeName.default,
+ selectedPatterns: [mockOptions[0].value, mockOptions[1].value],
+ })
);
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
index 6275ce19c3608..7a74f5bf2247f 100644
--- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx
@@ -4,50 +4,122 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useCallback, useMemo, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
- EuiHighlight,
- EuiIconTip,
+ EuiComboBox,
+ EuiComboBoxOptionOption,
+ EuiIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
EuiPopover,
- EuiPopoverFooter,
EuiPopoverTitle,
- EuiSelectable,
+ EuiSpacer,
+ EuiText,
+ EuiToolTip,
} from '@elastic/eui';
-import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option';
-import { useManageSource } from '../../containers/sourcerer';
+import deepEqual from 'fast-deep-equal';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import styled from 'styled-components';
+
import * as i18n from './translations';
import { SOURCERER_FEATURE_FLAG_ON } from '../../containers/sourcerer/constants';
-import { ADD_INDEX_PATH } from '../../../../common/constants';
-
-export const MaybeSourcerer = React.memo(() => {
- const {
- activeSourceGroupId,
- availableIndexPatterns,
- getManageSourceGroupById,
- isIndexPatternsLoading,
- updateSourceGroupIndicies,
- } = useManageSource();
- const { defaultPatterns, indexPatterns: selectedOptions, loading: loadingIndices } = useMemo(
- () => getManageSourceGroupById(activeSourceGroupId),
- [getManageSourceGroupById, activeSourceGroupId]
+import { sourcererActions, sourcererModel } from '../../store/sourcerer';
+import { State } from '../../store';
+import { getSourcererScopeSelector, SourcererScopeSelector } from './selectors';
+
+const PopoverContent = styled.div`
+ width: 600px;
+`;
+
+const ResetButton = styled(EuiButtonEmpty)`
+ width: fit-content;
+`;
+interface SourcererComponentProps {
+ scope: sourcererModel.SourcererScopeName;
+}
+
+export const SourcererComponent = React.memo(({ scope: scopeId }) => {
+ const dispatch = useDispatch();
+ const sourcererScopeSelector = useMemo(getSourcererScopeSelector, []);
+ const { configIndexPatterns, kibanaIndexPatterns, sourcererScope } = useSelector<
+ State,
+ SourcererScopeSelector
+ >((state) => sourcererScopeSelector(state, scopeId), deepEqual);
+ const { selectedPatterns, loading } = sourcererScope;
+ const [isPopoverOpen, setPopoverIsOpen] = useState(false);
+ const [selectedOptions, setSelectedOptions] = useState>>(
+ selectedPatterns.map((indexSelected) => ({
+ label: indexSelected,
+ value: indexSelected,
+ }))
);
- const loading = useMemo(() => loadingIndices || isIndexPatternsLoading, [
- isIndexPatternsLoading,
- loadingIndices,
- ]);
+ const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []);
const onChangeIndexPattern = useCallback(
- (newIndexPatterns: string[]) => {
- updateSourceGroupIndicies(activeSourceGroupId, newIndexPatterns);
+ (newSelectedPatterns: string[]) => {
+ dispatch(
+ sourcererActions.setSelectedIndexPatterns({
+ id: scopeId,
+ selectedPatterns: newSelectedPatterns,
+ })
+ );
},
- [activeSourceGroupId, updateSourceGroupIndicies]
+ [dispatch, scopeId]
+ );
+
+ const renderOption = useCallback(
+ (option) => {
+ const { value } = option;
+ if (kibanaIndexPatterns.some((kip) => kip.title === value)) {
+ return (
+ <>
+ {value}
+ >
+ );
+ }
+ return <>{value}>;
+ },
+ [kibanaIndexPatterns]
+ );
+
+ const onChangeCombo = useCallback((newSelectedOptions) => {
+ setSelectedOptions(newSelectedOptions);
+ }, []);
+
+ const resetDataSources = useCallback(() => {
+ setSelectedOptions(
+ configIndexPatterns.map((indexSelected) => ({
+ label: indexSelected,
+ value: indexSelected,
+ }))
+ );
+ }, [configIndexPatterns]);
+
+ const handleSaveIndices = useCallback(() => {
+ onChangeIndexPattern(selectedOptions.map((so) => so.label));
+ setPopoverIsOpen(false);
+ }, [onChangeIndexPattern, selectedOptions]);
+
+ const handleClosePopOver = useCallback(() => {
+ setPopoverIsOpen(false);
+ }, []);
+
+ const indexesPatternOptions = useMemo(
+ () =>
+ [...configIndexPatterns, ...kibanaIndexPatterns.map((kip) => kip.title)].reduce<
+ Array>
+ >((acc, index) => {
+ if (index != null && !acc.some((o) => o.label.includes(index))) {
+ return [...acc, { label: index, value: index }];
+ }
+ return acc;
+ }, []),
+ [configIndexPatterns, kibanaIndexPatterns]
);
- const [isPopoverOpen, setPopoverIsOpen] = useState(false);
- const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []);
const trigger = useMemo(
() => (
{
data-test-subj="sourcerer-trigger"
flush="left"
iconSide="right"
- iconType="indexSettings"
+ iconType="arrowDown"
+ isLoading={loading}
onClick={setPopoverIsOpenCb}
size="l"
title={i18n.SOURCERER}
@@ -63,99 +136,91 @@ export const MaybeSourcerer = React.memo(() => {
{i18n.SOURCERER}
),
- [setPopoverIsOpenCb]
- );
- const options: EuiSelectableOption[] = useMemo(
- () =>
- availableIndexPatterns.map((title, id) => ({
- label: title,
- key: `${title}-${id}`,
- value: title,
- checked: selectedOptions.includes(title) ? 'on' : undefined,
- })),
- [availableIndexPatterns, selectedOptions]
+ [setPopoverIsOpenCb, loading]
);
- const unSelectableOptions: EuiSelectableOption[] = useMemo(
- () =>
- defaultPatterns
- .filter((title) => !availableIndexPatterns.includes(title))
- .map((title, id) => ({
- label: title,
- key: `${title}-${id}`,
- value: title,
- disabled: true,
- checked: undefined,
- })),
- [availableIndexPatterns, defaultPatterns]
- );
- const renderOption = useCallback(
- (option, searchValue) => (
- <>
- {option.label}
- {option.disabled ? (
-
- ) : null}
- >
+
+ const comboBox = useMemo(
+ () => (
+
),
- []
+ [indexesPatternOptions, onChangeCombo, renderOption, selectedOptions]
);
- const onChange = useCallback(
- (choices: EuiSelectableOption[]) => {
- const choice = choices.reduce(
- (acc, { checked, label }) => (checked === 'on' ? [...acc, label] : acc),
- []
- );
- onChangeIndexPattern(choice);
- },
- [onChangeIndexPattern]
+
+ useEffect(() => {
+ const newSelecteOptions = selectedPatterns.map((indexSelected) => ({
+ label: indexSelected,
+ value: indexSelected,
+ }));
+ setSelectedOptions((prevSelectedOptions) => {
+ if (!deepEqual(newSelecteOptions, prevSelectedOptions)) {
+ return newSelecteOptions;
+ }
+ return prevSelectedOptions;
+ });
+ }, [selectedPatterns]);
+
+ const tooltipContent = useMemo(
+ () => (isPopoverOpen ? null : sourcererScope.selectedPatterns.sort().join(', ')),
+ [isPopoverOpen, sourcererScope.selectedPatterns]
);
- const allOptions = useMemo(() => [...options, ...unSelectableOptions], [
- options,
- unSelectableOptions,
- ]);
+
return (
- setPopoverIsOpen(false)}
- display="block"
- panelPaddingSize="s"
- ownFocus
- >
-
-
- <>
- {i18n.CHANGE_INDEX_PATTERNS}
-
- >
-
-
- {(list, search) => (
- <>
- {search}
- {list}
- >
- )}
-
-
-
- {i18n.ADD_INDEX_PATTERNS}
-
-
-
-
+
+
+
+
+ <>{i18n.SELECT_INDEX_PATTERNS}>
+
+
+ {i18n.INDEX_PATTERNS_SELECTION_LABEL}
+
+ {comboBox}
+
+
+
+
+ {i18n.INDEX_PATTERNS_RESET}
+
+
+
+
+ {i18n.SAVE_INDEX_PATTERNS}
+
+
+
+
+
+
);
});
-MaybeSourcerer.displayName = 'Sourcerer';
+SourcererComponent.displayName = 'Sourcerer';
-export const Sourcerer = SOURCERER_FEATURE_FLAG_ON ? MaybeSourcerer : () => null;
+export const Sourcerer = SOURCERER_FEATURE_FLAG_ON ? SourcererComponent : () => null;
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/selectors.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/selectors.tsx
new file mode 100644
index 0000000000000..6bbe24e921880
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/selectors.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { State } from '../../store';
+import { sourcererSelectors } from '../../store/sourcerer';
+import { KibanaIndexPatterns, ManageScope, SourcererScopeName } from '../../store/sourcerer/model';
+
+export interface SourcererScopeSelector {
+ configIndexPatterns: string[];
+ kibanaIndexPatterns: KibanaIndexPatterns;
+ sourcererScope: ManageScope;
+}
+
+export const getSourcererScopeSelector = () => {
+ const getKibanaIndexPatternsSelector = sourcererSelectors.kibanaIndexPatternsSelector();
+ const getScopesSelector = sourcererSelectors.scopesSelector();
+ const getConfigIndexPatternsSelector = sourcererSelectors.configIndexPatternsSelector();
+
+ const mapStateToProps = (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => {
+ const kibanaIndexPatterns = getKibanaIndexPatternsSelector(state);
+ const scope = getScopesSelector(state)[scopeId];
+ const configIndexPatterns = getConfigIndexPatternsSelector(state);
+
+ return {
+ kibanaIndexPatterns,
+ configIndexPatterns,
+ sourcererScope: scope,
+ };
+ };
+
+ return mapStateToProps;
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts
index 71b1734dad6a6..473eb43d5c4fe 100644
--- a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts
@@ -6,23 +6,26 @@
import { i18n } from '@kbn/i18n';
-export const SOURCERER = i18n.translate('xpack.securitySolution.indexPatterns.sourcerer', {
- defaultMessage: 'Sourcerer',
+export const SOURCERER = i18n.translate('xpack.securitySolution.indexPatterns.dataSourcesLabel', {
+ defaultMessage: 'Data sources',
});
-export const CHANGE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.help', {
- defaultMessage: 'Change index patterns',
+export const ALL_DEFAULT = i18n.translate('xpack.securitySolution.indexPatterns.allDefault', {
+ defaultMessage: 'All default',
});
-export const ADD_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.add', {
- defaultMessage: 'Configure Kibana index patterns',
+export const SELECT_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.help', {
+ defaultMessage: 'Data sources selection',
});
-export const CONFIGURE_INDEX_PATTERNS = i18n.translate(
- 'xpack.securitySolution.indexPatterns.configure',
+export const SAVE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.save', {
+ defaultMessage: 'Save',
+});
+
+export const INDEX_PATTERNS_SELECTION_LABEL = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.selectionLabel',
{
- defaultMessage:
- 'Configure additional Kibana index patterns to see them become available in the Security Solution',
+ defaultMessage: 'Choose the source of the data on this page',
}
);
@@ -33,3 +36,17 @@ export const DISABLED_INDEX_PATTERNS = i18n.translate(
'Disabled index patterns are recommended on this page, but first need to be configured in your Kibana index pattern settings',
}
);
+
+export const INDEX_PATTERNS_RESET = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.resetButton',
+ {
+ defaultMessage: 'Reset',
+ }
+);
+
+export const PICK_INDEX_PATTERNS = i18n.translate(
+ 'xpack.securitySolution.indexPatterns.pickIndexPatternsCombo',
+ {
+ defaultMessage: 'Pick index patterns',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts
index b654eaf17b47b..79cbd87cda201 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts
@@ -4,13 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EventType } from '../../../timelines/store/timeline/model';
+import { TimelineEventsType } from '../../../../common/types/timeline';
import * as i18n from './translations';
export interface TopNOption {
inputDisplay: string;
- value: EventType;
+ value: TimelineEventsType;
'data-test-subj': string;
}
@@ -52,8 +52,8 @@ export const defaultOptions = [...rawEvents, ...alertEvents];
* is always in sync with the `EventType` chosen by the user in
* the active timeline.
*/
-export const getOptions = (activeTimelineEventType?: EventType): TopNOption[] => {
- switch (activeTimelineEventType) {
+export const getOptions = (activeTimelineEventsType?: TimelineEventsType): TopNOption[] => {
+ switch (activeTimelineEventsType) {
case 'all':
return allEvents;
case 'raw':
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 31318122eb564..594bffbd4ff63 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
@@ -168,6 +168,17 @@ const store = createStore(
storage
);
+let testProps = {
+ browserFields: mockBrowserFields,
+ field,
+ indexNames: [],
+ indexPattern: mockIndexPattern,
+ timelineId: TimelineId.hostsPageExternalAlerts,
+ toggleTopN: jest.fn(),
+ onFilterAdded: jest.fn(),
+ value,
+};
+
describe('StatefulTopN', () => {
// Suppress warnings about "react-beautiful-dnd"
/* eslint-disable no-console */
@@ -189,16 +200,7 @@ describe('StatefulTopN', () => {
wrapper = mount(
-
+
);
@@ -277,19 +279,14 @@ describe('StatefulTopN', () => {
filterManager,
},
};
+ testProps = {
+ ...testProps,
+ timelineId: TimelineId.active,
+ };
wrapper = mount(
-
+
);
@@ -345,37 +342,33 @@ describe('StatefulTopN', () => {
expect(props.to).toEqual('2020-04-15T03:46:09.047Z');
});
});
+ describe('rendering in a NON-active timeline context', () => {
+ test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, () => {
+ const filterManager = new FilterManager(mockUiSettingsForFilterManager);
- test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, () => {
- const filterManager = new FilterManager(mockUiSettingsForFilterManager);
+ const manageTimelineForTesting = {
+ [TimelineId.active]: {
+ ...getTimelineDefaults(TimelineId.active),
+ filterManager,
+ documentType: 'alerts',
+ },
+ };
- const manageTimelineForTesting = {
- [TimelineId.active]: {
- ...getTimelineDefaults(TimelineId.active),
- filterManager,
- documentType: 'alerts',
- },
- };
-
- const wrapper = mount(
-
-
-
-
-
- );
-
- const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props;
-
- expect(props.defaultView).toEqual('alert');
+ testProps = {
+ ...testProps,
+ timelineId: TimelineId.detectionsPage,
+ };
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props;
+
+ expect(props.defaultView).toEqual('alert');
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx
index d71242329bcda..9c81cb57335a5 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx
@@ -74,7 +74,7 @@ interface OwnProps {
browserFields: BrowserFields;
field: string;
indexPattern: IIndexPattern;
- indexToAdd: string[] | null;
+ indexNames: string[];
timelineId?: string;
toggleTopN: () => void;
onFilterAdded?: () => void;
@@ -93,7 +93,7 @@ const StatefulTopNComponent: React.FC = ({
dataProviders,
field,
indexPattern,
- indexToAdd,
+ indexNames,
globalFilters = EMPTY_FILTERS,
globalQuery = EMPTY_QUERY,
kqlMode,
@@ -109,7 +109,6 @@ const StatefulTopNComponent: React.FC = ({
const options = getOptions(
timelineId === TimelineId.active ? activeTimelineEventType : undefined
);
-
return (
= ({
filters={timelineId === TimelineId.active ? EMPTY_FILTERS : globalFilters}
from={timelineId === TimelineId.active ? activeTimelineFrom : from}
indexPattern={indexPattern}
- indexToAdd={indexToAdd}
+ indexNames={indexNames}
options={options}
query={timelineId === TimelineId.active ? EMPTY_QUERY : globalQuery}
setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker}
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx
index 667d1816e8f07..829f918ddfe1b 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx
@@ -13,6 +13,8 @@ import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions';
import { allEvents, defaultOptions } from './helpers';
import { TopN } from './top_n';
+import { TimelineEventsType } from '../../../../common/types/timeline';
+import { InputsModelId } from '../../store/inputs/constants';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@@ -103,29 +105,34 @@ describe('TopN', () => {
const query = { query: '', language: 'kuery' };
+ const toggleTopN = jest.fn();
+ const eventTypes: { [id: string]: TimelineEventsType } = {
+ raw: 'raw',
+ alert: 'alert',
+ all: 'all',
+ };
+ let testProps = {
+ defaultView: eventTypes.raw,
+ field,
+ filters: [],
+ from: '2020-04-14T00:31:47.695Z',
+ indexNames: [],
+ indexPattern: mockIndexPattern,
+ options: defaultOptions,
+ query,
+ setAbsoluteRangeDatePicker,
+ setAbsoluteRangeDatePickerTarget: 'global' as InputsModelId,
+ setQuery: jest.fn(),
+ to: '2020-04-15T00:31:47.695Z',
+ toggleTopN,
+ value,
+ };
describe('common functionality', () => {
- let toggleTopN: () => void;
let wrapper: ReactWrapper;
-
beforeEach(() => {
- toggleTopN = jest.fn();
wrapper = mount(
-
+
);
});
@@ -143,28 +150,12 @@ describe('TopN', () => {
});
describe('events view', () => {
- let toggleTopN: () => void;
let wrapper: ReactWrapper;
beforeEach(() => {
- toggleTopN = jest.fn();
wrapper = mount(
-
+
);
});
@@ -181,37 +172,25 @@ describe('TopN', () => {
});
describe('alerts view', () => {
- let toggleTopN: () => void;
let wrapper: ReactWrapper;
beforeEach(() => {
- toggleTopN = jest.fn();
+ testProps = {
+ ...testProps,
+ defaultView: eventTypes.alert,
+ };
wrapper = mount(
-
+
);
});
- test(`it renders SignalsByCategory when defaultView is 'signal'`, () => {
+ test(`it renders SignalsByCategory when defaultView is 'alert'`, () => {
expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBe(true);
});
- test(`it does NOT render EventsByDataset when defaultView is 'signal'`, () => {
+ test(`it does NOT render EventsByDataset when defaultView is 'alert'`, () => {
expect(
wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists()
).toBe(false);
@@ -222,24 +201,14 @@ describe('TopN', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
+ testProps = {
+ ...testProps,
+ defaultView: eventTypes.all,
+ options: allEvents,
+ };
wrapper = mount(
-
+
);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx
index 064241a7216f4..4f0a71dcc3ebb 100644
--- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx
@@ -14,7 +14,7 @@ import { EventsByDataset } from '../../../overview/components/events_by_dataset'
import { SignalsByCategory } from '../../../overview/components/signals_by_category';
import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public';
import { InputsModelId } from '../../store/inputs/constants';
-import { EventType } from '../../../timelines/store/timeline/model';
+import { TimelineEventsType } from '../../../../common/types/timeline';
import { TopNOption } from './helpers';
import * as i18n from './translations';
@@ -45,11 +45,11 @@ const TopNContent = styled.div`
export interface Props extends Pick {
combinedQueries?: string;
- defaultView: EventType;
+ defaultView: TimelineEventsType;
field: string;
filters: Filter[];
indexPattern: IIndexPattern;
- indexToAdd?: string[] | null;
+ indexNames: string[];
options: TopNOption[];
query: Query;
setAbsoluteRangeDatePicker: ActionCreator<{
@@ -75,7 +75,7 @@ const TopNComponent: React.FC = ({
field,
from,
indexPattern,
- indexToAdd,
+ indexNames,
options,
query = DEFAULT_QUERY,
setAbsoluteRangeDatePicker,
@@ -85,8 +85,10 @@ const TopNComponent: React.FC = ({
to,
toggleTopN,
}) => {
- const [view, setView] = useState(defaultView);
- const onViewSelected = useCallback((value: string) => setView(value as EventType), [setView]);
+ const [view, setView] = useState(defaultView);
+ const onViewSelected = useCallback((value: string) => setView(value as TimelineEventsType), [
+ setView,
+ ]);
useEffect(() => {
setView(defaultView);
@@ -123,7 +125,7 @@ const TopNComponent: React.FC = ({
from={from}
headerChildren={headerChildren}
indexPattern={indexPattern}
- indexToAdd={indexToAdd}
+ indexNames={indexNames}
onlyField={field}
query={query}
setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget}
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts
index 5a4aec93dd9aa..e5c09d229808b 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts
@@ -17,6 +17,7 @@ export enum CONSTANTS {
networkPage = 'network.page',
overviewPage = 'overview.page',
savedQuery = 'savedQuery',
+ sourcerer = 'sourcerer',
timeline = 'timeline',
timelinePage = 'timeline.page',
timerange = 'timerange',
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 6052913b4183b..a915b1c9d09a7 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
@@ -22,6 +22,8 @@ import { formatDate } from '../super_date_picker';
import { NavTab } from '../navigation/types';
import { CONSTANTS, UrlStateType } from './constants';
import { ReplaceStateInLocation, UpdateUrlStateString } from './types';
+import { sourcererSelectors } from '../../store/sourcerer';
+import { SourcererScopeName, SourcererScopePatterns } from '../../store/sourcerer/model';
export const decodeRisonUrlState = (value: string | undefined): T | null => {
try {
@@ -118,6 +120,7 @@ export const makeMapStateToProps = () => {
const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector();
const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector();
const getTimeline = timelineSelectors.getTimelineByIdSelector();
+ const getSourcererScopes = sourcererSelectors.scopesSelector();
const mapStateToProps = (state: State) => {
const inputState = getInputsSelector(state);
const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global;
@@ -147,10 +150,16 @@ export const makeMapStateToProps = () => {
[CONSTANTS.savedQuery]: savedQuery.id,
};
}
+ const sourcerer = getSourcererScopes(state);
+ const activeScopes: SourcererScopeName[] = Object.keys(sourcerer) as SourcererScopeName[];
+ const selectedPatterns: SourcererScopePatterns = activeScopes
+ .filter((scope) => scope === SourcererScopeName.default)
+ .reduce((acc, scope) => ({ ...acc, [scope]: sourcerer[scope]?.selectedPatterns }), {});
return {
urlState: {
...searchAttr,
+ [CONSTANTS.sourcerer]: selectedPatterns,
[CONSTANTS.timerange]: {
global: {
[CONSTANTS.timerange]: globalTimerange,
@@ -217,6 +226,17 @@ export const updateUrlStateString = ({
urlStateKey: urlKey,
});
}
+ } else if (urlKey === CONSTANTS.sourcerer) {
+ const sourcererState = decodeRisonUrlState(newUrlStateString);
+ if (sourcererState != null && Object.keys(sourcererState).length > 0) {
+ return replaceStateInLocation({
+ history,
+ pathName,
+ search,
+ urlStateToReplace: sourcererState,
+ urlStateKey: urlKey,
+ });
+ }
} else if (urlKey === CONSTANTS.filters) {
const queryState = decodeRisonUrlState(newUrlStateString);
if (isEmpty(queryState)) {
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx
index 72df9d613abac..fc970c066e8a5 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx
@@ -161,7 +161,7 @@ describe('UrlStateContainer', () => {
).toEqual({
hash: '',
pathname: examplePath,
- search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
+ search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
state: '',
});
}
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx
index 723f2d235864f..9e845ec538aa0 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx
@@ -83,7 +83,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
hash: '',
pathname: '/network',
search:
- "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
+ "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
state: '',
});
});
@@ -114,7 +114,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
hash: '',
pathname: '/network',
search:
- "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
state: '',
});
});
@@ -147,7 +147,40 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
hash: '',
pathname: '/network',
search:
- "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)",
+ "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)",
+ state: '',
+ });
+ });
+
+ test('sourcerer redux state updates the url', () => {
+ mockProps = getMockPropsObj({
+ page: CONSTANTS.networkPage,
+ examplePath: '/network',
+ namespaceLower: 'network',
+ pageName: SecurityPageName.network,
+ detailName: undefined,
+ }).noSearch.undefinedQuery;
+
+ const wrapper = mount(
+ useUrlStateHooks(args)} />
+ );
+ const newUrlState = {
+ ...mockProps.urlState,
+ sourcerer: ['cool', 'patterns'],
+ };
+
+ wrapper.setProps({
+ hookProps: { ...mockProps, urlState: newUrlState, isInitializing: false },
+ });
+ wrapper.update();
+
+ expect(
+ mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0]
+ ).toStrictEqual({
+ hash: '',
+ pathname: '/network',
+ search:
+ "?sourcerer=!(cool,patterns)&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
state: '',
});
});
@@ -176,7 +209,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
hash: '',
pathname: examplePath,
search:
- "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
+ "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
state: '',
});
}
@@ -204,7 +237,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
expect(
mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0].search
).toEqual(
- "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
+ "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
);
wrapper.setProps({ hookProps: updatedProps });
@@ -213,7 +246,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
expect(
mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0].search
).toEqual(
- "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
+ "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
);
});
});
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 6eccf52ec72da..1e77ae7766630 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
@@ -8,7 +8,7 @@ import { get, isEmpty } from 'lodash/fp';
import { Dispatch } from 'redux';
import { Query, Filter } from '../../../../../../../src/plugins/data/public';
-import { inputsActions } from '../../store/actions';
+import { inputsActions, sourcererActions } from '../../store/actions';
import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants';
import {
UrlInputsModel,
@@ -22,6 +22,8 @@ import { decodeRisonUrlState } from './helpers';
import { normalizeTimeRange } from './normalize_time_range';
import { DispatchSetInitialStateFromUrl, SetInitialStateFromUrl } from './types';
import { queryTimelineById } from '../../../timelines/components/open_timeline/helpers';
+import { SourcererScopeName, SourcererScopePatterns } from '../../store/sourcerer/model';
+import { SecurityPageName } from '../../../../common/constants';
export const dispatchSetInitialStateFromUrl = (
dispatch: Dispatch
@@ -40,6 +42,22 @@ export const dispatchSetInitialStateFromUrl = (
if (urlKey === CONSTANTS.timerange) {
updateTimerange(newUrlStateString, dispatch);
}
+ if (urlKey === CONSTANTS.sourcerer) {
+ const sourcererState = decodeRisonUrlState(newUrlStateString);
+ if (sourcererState != null) {
+ const activeScopes: SourcererScopeName[] = Object.keys(sourcererState).filter(
+ (key) => !(key === SourcererScopeName.default && pageName === SecurityPageName.detections)
+ ) as SourcererScopeName[];
+ activeScopes.forEach((scope) =>
+ dispatch(
+ sourcererActions.setSelectedIndexPatterns({
+ id: scope,
+ selectedPatterns: sourcererState[scope] ?? [],
+ })
+ )
+ );
+ }
+ }
if (urlKey === CONSTANTS.appQuery && indexPattern != null) {
const appQuery = decodeRisonUrlState(newUrlStateString);
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts
index 8d471e843320c..6f04226fa3a19 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts
@@ -117,6 +117,7 @@ export const defaultProps: UrlStateContainerPropTypes = {
id: '',
isOpen: false,
},
+ [CONSTANTS.sourcerer]: {},
},
setInitialStateFromUrl: dispatchSetInitialStateFromUrl(mockDispatch),
updateTimeline: (jest.fn() as unknown) as DispatchUpdateTimeline,
diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts
index f383e18132385..301771a4db6b9 100644
--- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts
+++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts
@@ -22,11 +22,13 @@ import { DispatchUpdateTimeline } from '../../../timelines/components/open_timel
import { NavTab } from '../navigation/types';
import { CONSTANTS, UrlStateType } from './constants';
+import { SourcererScopePatterns } from '../../store/sourcerer/model';
export const ALL_URL_STATE_KEYS: KeyUrlState[] = [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
+ CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
];
@@ -36,6 +38,7 @@ export const URL_STATE_KEYS: Record = {
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
+ CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
@@ -43,6 +46,7 @@ export const URL_STATE_KEYS: Record = {
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
+ CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
@@ -51,6 +55,7 @@ export const URL_STATE_KEYS: Record = {
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
+ CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
@@ -58,6 +63,7 @@ export const URL_STATE_KEYS: Record = {
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
+ CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
@@ -65,6 +71,7 @@ export const URL_STATE_KEYS: Record = {
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
+ CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
@@ -72,6 +79,7 @@ export const URL_STATE_KEYS: Record = {
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
+ CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
@@ -93,6 +101,7 @@ export interface UrlState {
[CONSTANTS.appQuery]?: Query;
[CONSTANTS.filters]?: Filter[];
[CONSTANTS.savedQuery]?: string;
+ [CONSTANTS.sourcerer]: SourcererScopePatterns;
[CONSTANTS.timerange]: UrlInputsModel;
[CONSTANTS.timeline]: TimelineUrl;
}
diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx
index f6ebbb990f223..489ccb23c9b2c 100644
--- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx
@@ -29,6 +29,7 @@ const AnomaliesQueryTabBodyComponent: React.FC = ({
AnomaliesTableComponent,
flowTarget,
ip,
+ indexNames,
}) => {
const { jobs } = useInstalledSecurityJobs();
const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE);
@@ -57,6 +58,7 @@ const AnomaliesQueryTabBodyComponent: React.FC = ({
endDate={endDate}
filterQuery={mergedFilterQuery}
id={ID}
+ indexNames={indexNames}
setQuery={setQuery}
startDate={startDate}
{...histogramConfigs}
diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts
index d716df70246f7..3ce4b8b6d4494 100644
--- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts
@@ -24,6 +24,7 @@ export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & {
deleteQuery?: ({ id }: { id: string }) => void;
endDate: GlobalTimeArgs['to'];
flowTarget?: FlowTarget;
+ indexNames: string[];
narrowDateRange: NarrowDateRange;
setQuery: GlobalTimeArgs['setQuery'];
startDate: GlobalTimeArgs['from'];
diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
index 3d79c83dc42cb..dc2d6605bc292 100644
--- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts
@@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal';
import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
-import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
import { inputsModel } from '../../../../common/store';
import { useKibana } from '../../../../common/lib/kibana';
import {
@@ -23,10 +22,10 @@ import {
isCompleteResponse,
isErrorResponse,
} from '../../../../../../../../src/plugins/data/common';
-import { useWithSource } from '../../source';
import * as i18n from './translations';
+import { DocValueFields } from '../../../../../common/search_strategy';
-// const ID = 'timelineEventsLastEventTimeQuery';
+const ID = 'timelineEventsLastEventTimeQuery';
export interface UseTimelineLastEventTimeArgs {
lastSeen: string | null;
@@ -35,26 +34,29 @@ export interface UseTimelineLastEventTimeArgs {
}
interface UseTimelineLastEventTimeProps {
+ docValueFields: DocValueFields[];
indexKey: LastEventIndexKey;
+ indexNames: string[];
details: LastTimeDetails;
}
export const useTimelineLastEventTime = ({
+ docValueFields,
indexKey,
+ indexNames,
details,
}: UseTimelineLastEventTimeProps): [boolean, UseTimelineLastEventTimeArgs] => {
- const { data, notifications, uiSettings } = useKibana().services;
- const { docValueFields } = useWithSource('default');
+ const { data, notifications } = useKibana().services;
const refetch = useRef(noop);
const abortCtrl = useRef(new AbortController());
- const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY);
const [loading, setLoading] = useState(false);
const [TimelineLastEventTimeRequest, setTimelineLastEventTimeRequest] = useState<
TimelineEventsLastEventTimeRequestOptions
>({
- defaultIndex,
- factoryQueryType: TimelineEventsQueries.lastEventTime,
+ defaultIndex: indexNames,
docValueFields,
+ factoryQueryType: TimelineEventsQueries.lastEventTime,
+ id: ID,
indexKey,
details,
});
@@ -133,7 +135,8 @@ export const useTimelineLastEventTime = ({
setTimelineLastEventTimeRequest((prevRequest) => {
const myRequest = {
...prevRequest,
- defaultIndex,
+ defaultIndex: indexNames,
+ docValueFields,
indexKey,
details,
};
@@ -142,7 +145,7 @@ export const useTimelineLastEventTime = ({
}
return prevRequest;
});
- }, [defaultIndex, details, indexKey]);
+ }, [indexNames, details, docValueFields, indexKey]);
useEffect(() => {
timelineLastEventTimeSearch(TimelineLastEventTimeRequest);
diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts
index 8e0c133f95b4d..ca8bcc637717b 100644
--- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts
@@ -5,14 +5,13 @@
*/
import deepEqual from 'fast-deep-equal';
-import { isEmpty, noop } from 'lodash/fp';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { noop } from 'lodash/fp';
+import { useCallback, useEffect, useRef, useState } from 'react';
import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types';
-import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
import { inputsModel } from '../../../common/store';
import { createFilter } from '../../../common/containers/helpers';
-import { useKibana, useUiSetting$ } from '../../../common/lib/kibana';
+import { useKibana } from '../../../common/lib/kibana';
import {
MatrixHistogramQuery,
MatrixHistogramRequestOptions,
@@ -40,25 +39,18 @@ export const useMatrixHistogram = ({
errorMessage,
filterQuery,
histogramType,
- indexToAdd,
+ indexNames,
stackByField,
startDate,
}: MatrixHistogramQueryProps): [boolean, UseMatrixHistogramArgs] => {
const { data, notifications } = useKibana().services;
const refetch = useRef(noop);
const abortCtrl = useRef(new AbortController());
- const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY);
- const defaultIndex = useMemo(() => {
- if (indexToAdd != null && !isEmpty(indexToAdd)) {
- return [...configIndex, ...indexToAdd];
- }
- return configIndex;
- }, [configIndex, indexToAdd]);
const [loading, setLoading] = useState(false);
const [matrixHistogramRequest, setMatrixHistogramRequest] = useState<
MatrixHistogramRequestOptions
>({
- defaultIndex,
+ defaultIndex: indexNames,
factoryQueryType: MatrixHistogramQuery,
filterQuery: createFilter(filterQuery),
histogramType,
@@ -140,7 +132,7 @@ export const useMatrixHistogram = ({
setMatrixHistogramRequest((prevRequest) => {
const myRequest = {
...prevRequest,
- defaultIndex,
+ defaultIndex: indexNames,
filterQuery: createFilter(filterQuery),
timerange: {
interval: '12h',
@@ -153,7 +145,7 @@ export const useMatrixHistogram = ({
}
return prevRequest;
});
- }, [defaultIndex, endDate, filterQuery, startDate]);
+ }, [indexNames, endDate, filterQuery, startDate]);
useEffect(() => {
hostsSearch(matrixHistogramRequest);
diff --git a/x-pack/plugins/security_solution/public/common/containers/query_template.tsx b/x-pack/plugins/security_solution/public/common/containers/query_template.tsx
index eaa43c255a944..80791d91481a8 100644
--- a/x-pack/plugins/security_solution/public/common/containers/query_template.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/query_template.tsx
@@ -14,6 +14,7 @@ import { DocValueFields } from './source';
export { DocValueFields };
export interface QueryTemplateProps {
+ indexNames: string[];
docValueFields?: DocValueFields[];
id?: string;
endDate?: string;
diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.gql_query.ts b/x-pack/plugins/security_solution/public/common/containers/source/index.gql_query.ts
deleted file mode 100644
index 630515c5cbed4..0000000000000
--- a/x-pack/plugins/security_solution/public/common/containers/source/index.gql_query.ts
+++ /dev/null
@@ -1,31 +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 gql from 'graphql-tag';
-
-export const sourceQuery = gql`
- query SourceQuery($sourceId: ID = "default", $defaultIndex: [String!]!) {
- source(id: $sourceId) {
- id
- status {
- indicesExist(defaultIndex: $defaultIndex)
- indexFields(defaultIndex: $defaultIndex) {
- category
- description
- example
- indexes
- name
- searchable
- type
- aggregatable
- format
- esTypes
- subType
- }
- }
- }
- }
-`;
diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx
deleted file mode 100644
index 8ba7f7da7b8e3..0000000000000
--- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx
+++ /dev/null
@@ -1,109 +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 { act, renderHook } from '@testing-library/react-hooks';
-
-import { useWithSource, indicesExistOrDataTemporarilyUnavailable } from '.';
-import { NO_ALERT_INDEX } from '../../../../common/constants';
-import { mockBrowserFields, mockIndexFields, mocksSource } from './mock';
-
-jest.mock('../../lib/kibana');
-jest.mock('../../utils/apollo_context', () => ({
- useApolloClient: jest.fn().mockReturnValue({
- query: jest.fn().mockImplementation(() => Promise.resolve(mocksSource[0].result)),
- }),
-}));
-
-describe('Index Fields & Browser Fields', () => {
- test('At initialization the value of indicesExists should be true', async () => {
- const { result, waitForNextUpdate } = renderHook(() => useWithSource());
- const initialResult = result.current;
-
- await waitForNextUpdate();
-
- return expect(initialResult).toEqual({
- browserFields: {},
- docValueFields: [],
- errorMessage: null,
- indexPattern: {
- fields: [],
- title:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- },
- indicesExist: true,
- loading: true,
- });
- });
-
- test('returns memoized value', async () => {
- const { result, waitForNextUpdate, rerender } = renderHook(() => useWithSource());
- await waitForNextUpdate();
-
- const result1 = result.current;
- act(() => rerender());
- const result2 = result.current;
-
- return expect(result1).toBe(result2);
- });
-
- test('Index Fields', async () => {
- const { result, waitForNextUpdate } = renderHook(() => useWithSource());
-
- await waitForNextUpdate();
-
- return expect(result).toEqual({
- current: {
- indicesExist: true,
- browserFields: mockBrowserFields,
- docValueFields: [
- {
- field: '@timestamp',
- format: 'date_time',
- },
- {
- field: 'event.end',
- format: 'date_time',
- },
- ],
- indexPattern: {
- fields: mockIndexFields,
- title:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- },
- loading: false,
- errorMessage: null,
- },
- error: undefined,
- });
- });
-
- test('Make sure we are not querying for NO_ALERT_INDEX and it is not includes in the index pattern', async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useWithSource('default', [NO_ALERT_INDEX])
- );
-
- await waitForNextUpdate();
- return expect(result.current.indexPattern.title).toEqual(
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*'
- );
- });
-
- describe('indicesExistOrDataTemporarilyUnavailable', () => {
- test('it returns true when undefined', () => {
- let undefVar;
- const result = indicesExistOrDataTemporarilyUnavailable(undefVar);
- expect(result).toBeTruthy();
- });
- test('it returns true when true', () => {
- const result = indicesExistOrDataTemporarilyUnavailable(true);
- expect(result).toBeTruthy();
- });
- test('it returns false when false', () => {
- const result = indicesExistOrDataTemporarilyUnavailable(false);
- expect(result).toBeFalsy();
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
index ffbecf9e3d433..4b1db8a2871bd 100644
--- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx
@@ -4,42 +4,30 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { isUndefined } from 'lodash';
import { set } from '@elastic/safer-lodash-set/fp';
-import { get, keyBy, pick, isEmpty } from 'lodash/fp';
-import { useEffect, useMemo, useState } from 'react';
+import { keyBy, pick, isEmpty, isEqual, isUndefined } from 'lodash/fp';
import memoizeOne from 'memoize-one';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
import { IIndexPattern } from 'src/plugins/data/public';
-import { DEFAULT_INDEX_KEY, NO_ALERT_INDEX } from '../../../../common/constants';
-import { useUiSetting$ } from '../../lib/kibana';
+import { useKibana } from '../../lib/kibana';
+import {
+ IndexField,
+ IndexFieldsStrategyResponse,
+ IndexFieldsStrategyRequest,
+ BrowserField,
+ BrowserFields,
+} from '../../../../common/search_strategy/index_fields';
+import { AbortError } from '../../../../../../../src/plugins/data/common';
+import * as i18n from './translations';
+import { SourcererScopeName } from '../../store/sourcerer/model';
+import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
-import { IndexField, SourceQuery } from '../../../graphql/types';
+import { State } from '../../store';
+import { DocValueFields } from '../../../../common/search_strategy/common';
-import { sourceQuery } from './index.gql_query';
-import { useApolloClient } from '../../utils/apollo_context';
-
-export { sourceQuery };
-
-export interface BrowserField {
- aggregatable: boolean;
- category: string;
- description: string | null;
- example: string | number | null;
- fields: Readonly>>;
- format: string;
- indexes: string[];
- name: string;
- searchable: boolean;
- type: string;
-}
-
-export interface DocValueFields {
- field: string;
- format: string;
-}
-
-export type BrowserFields = Readonly>>;
+export { BrowserField, BrowserFields, DocValueFields };
export const getAllBrowserFields = (browserFields: BrowserFields): Array> =>
Object.values(browserFields).reduce>>(
@@ -85,14 +73,12 @@ export const getDocValueFields = memoizeOne(
(_title: string, fields: IndexField[]): DocValueFields[] =>
fields && fields.length > 0
? fields.reduce((accumulator: DocValueFields[], field: IndexField) => {
- if (field.type === 'date' && accumulator.length < 100) {
- const format: string =
- field.format != null && !isEmpty(field.format) ? field.format : 'date_time';
+ if (field.readFromDocValues && accumulator.length < 100) {
return [
...accumulator,
{
field: field.name,
- format,
+ format: field.format,
},
];
}
@@ -107,115 +93,196 @@ export const indicesExistOrDataTemporarilyUnavailable = (
indicesExist: boolean | null | undefined
) => indicesExist || isUndefined(indicesExist);
-const EMPTY_BROWSER_FIELDS = {};
-const EMPTY_DOCVALUE_FIELD: DocValueFields[] = [];
+const DEFAULT_BROWSER_FIELDS = {};
+const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' };
+const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = [];
-interface UseWithSourceState {
+interface FetchIndexReturn {
browserFields: BrowserFields;
docValueFields: DocValueFields[];
- errorMessage: string | null;
- indexPattern: IIndexPattern;
- indicesExist: boolean | undefined | null;
- loading: boolean;
+ indexes: string[];
+ indexExists: boolean;
+ indexPatterns: IIndexPattern;
}
-export const useWithSource = (
- sourceId = 'default',
- indexToAdd?: string[] | null,
- onlyCheckIndexToAdd?: boolean,
- // Fun fact: When using this hook multiple times within a component (e.g. add_exception_modal & edit_exception_modal),
- // the apolloClient will perform queryDeduplication and prevent the first query from executing. A deep compare is not
- // performed on `indices`, so another field must be passed to circumvent this.
- // For details, see https://github.com/apollographql/react-apollo/issues/2202
- queryDeduplication = 'default'
-) => {
- const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY);
- const defaultIndex = useMemo(() => {
- const filterIndexAdd = (indexToAdd ?? []).filter((item) => item !== NO_ALERT_INDEX);
- if (!isEmpty(filterIndexAdd)) {
- return onlyCheckIndexToAdd ? filterIndexAdd : [...configIndex, ...filterIndexAdd];
- }
- return configIndex;
- }, [configIndex, indexToAdd, onlyCheckIndexToAdd]);
-
- const [state, setState] = useState({
- browserFields: EMPTY_BROWSER_FIELDS,
- docValueFields: EMPTY_DOCVALUE_FIELD,
- errorMessage: null,
- indexPattern: getIndexFields(defaultIndex.join(), []),
- indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined),
- loading: true,
+export const useFetchIndex = (
+ indexNames: string[],
+ onlyCheckIfIndicesExist: boolean = false
+): [boolean, FetchIndexReturn] => {
+ const { data, notifications } = useKibana().services;
+ const abortCtrl = useRef(new AbortController());
+ const previousIndexesName = useRef([]);
+ const [isLoading, setLoading] = useState(true);
+
+ const [state, setState] = useState({
+ browserFields: DEFAULT_BROWSER_FIELDS,
+ docValueFields: DEFAULT_DOC_VALUE_FIELDS,
+ indexes: indexNames,
+ indexExists: true,
+ indexPatterns: DEFAULT_INDEX_PATTERNS,
});
- const apolloClient = useApolloClient();
+ const indexFieldsSearch = useCallback(
+ (iNames) => {
+ let didCancel = false;
+ const asyncSearch = async () => {
+ abortCtrl.current = new AbortController();
+ setLoading(true);
+ const searchSubscription$ = data.search
+ .search(
+ { indices: iNames, onlyCheckIfIndicesExist },
+ {
+ abortSignal: abortCtrl.current.signal,
+ strategy: 'securitySolutionIndexFields',
+ }
+ )
+ .subscribe({
+ next: (response) => {
+ if (!response.isPartial && !response.isRunning) {
+ if (!didCancel) {
+ const stringifyIndices = response.indicesExist.sort().join();
+ previousIndexesName.current = response.indicesExist;
+ setLoading(false);
+ setState({
+ browserFields: getBrowserFields(stringifyIndices, response.indexFields),
+ docValueFields: getDocValueFields(stringifyIndices, response.indexFields),
+ indexes: response.indicesExist,
+ indexExists: response.indicesExist.length > 0,
+ indexPatterns: getIndexFields(stringifyIndices, response.indexFields),
+ });
+ }
+ searchSubscription$.unsubscribe();
+ } else if (!didCancel && response.isPartial && !response.isRunning) {
+ setLoading(false);
+ notifications.toasts.addWarning(i18n.ERROR_BEAT_FIELDS);
+ searchSubscription$.unsubscribe();
+ }
+ },
+ error: (msg) => {
+ if (!didCancel) {
+ setLoading(false);
+ }
- useEffect(() => {
- let isSubscribed = true;
- const abortCtrl = new AbortController();
-
- async function fetchSource() {
- if (!apolloClient) return;
-
- setState((prevState) => ({ ...prevState, loading: true }));
-
- try {
- const result = await apolloClient.query<
- SourceQuery.Query,
- SourceQuery.Variables & { queryDeduplication: string }
- >({
- query: sourceQuery,
- fetchPolicy: 'cache-first',
- variables: {
- sourceId,
- defaultIndex,
- queryDeduplication,
- },
- context: {
- fetchOptions: {
- signal: abortCtrl.signal,
+ if (!(msg instanceof AbortError)) {
+ notifications.toasts.addDanger({
+ text: msg.message,
+ title: i18n.FAIL_BEAT_FIELDS,
+ });
+ }
},
- },
- });
-
- if (isSubscribed) {
- setState({
- loading: false,
- indicesExist: indicesExistOrDataTemporarilyUnavailable(
- get('data.source.status.indicesExist', result)
- ),
- browserFields: getBrowserFields(
- defaultIndex.join(),
- get('data.source.status.indexFields', result)
- ),
- docValueFields: getDocValueFields(
- defaultIndex.join(),
- get('data.source.status.indexFields', result)
- ),
- indexPattern: getIndexFields(
- defaultIndex.join(),
- get('data.source.status.indexFields', result)
- ),
- errorMessage: null,
});
- }
- } catch (error) {
- if (isSubscribed) {
- setState((prevState) => ({
- ...prevState,
- loading: false,
- errorMessage: error.message,
- }));
- }
- }
+ };
+ abortCtrl.current.abort();
+ asyncSearch();
+ return () => {
+ didCancel = true;
+ abortCtrl.current.abort();
+ };
+ },
+ [data.search, notifications.toasts, onlyCheckIfIndicesExist]
+ );
+
+ useEffect(() => {
+ if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) {
+ indexFieldsSearch(indexNames);
}
+ }, [indexNames, indexFieldsSearch, previousIndexesName]);
+
+ return [isLoading, state];
+};
+
+export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
+ const { data, notifications } = useKibana().services;
+ const abortCtrl = useRef(new AbortController());
+ const dispatch = useDispatch();
+ const previousIndexesName = useRef([]);
+
+ const indexNamesSelectedSelector = useMemo(
+ () => sourcererSelectors.getIndexNamesSelectedSelector(),
+ []
+ );
+ const indexNames = useSelector(
+ (state) => indexNamesSelectedSelector(state, sourcererScopeName),
+ shallowEqual
+ );
- fetchSource();
+ const setLoading = useCallback(
+ (loading: boolean) => {
+ dispatch(sourcererActions.setSourcererScopeLoading({ id: sourcererScopeName, loading }));
+ },
+ [dispatch, sourcererScopeName]
+ );
+
+ const indexFieldsSearch = useCallback(
+ (indicesName) => {
+ let didCancel = false;
+ const asyncSearch = async () => {
+ abortCtrl.current = new AbortController();
+ setLoading(true);
+ const searchSubscription$ = data.search
+ .search(
+ { indices: indicesName, onlyCheckIfIndicesExist: false },
+ {
+ abortSignal: abortCtrl.current.signal,
+ strategy: 'securitySolutionIndexFields',
+ }
+ )
+ .subscribe({
+ next: (response) => {
+ if (!response.isPartial && !response.isRunning) {
+ if (!didCancel) {
+ const stringifyIndices = response.indicesExist.sort().join();
+ previousIndexesName.current = response.indicesExist;
+ dispatch(
+ sourcererActions.setSource({
+ id: sourcererScopeName,
+ payload: {
+ browserFields: getBrowserFields(stringifyIndices, response.indexFields),
+ docValueFields: getDocValueFields(stringifyIndices, response.indexFields),
+ errorMessage: null,
+ id: sourcererScopeName,
+ indexPattern: getIndexFields(stringifyIndices, response.indexFields),
+ indicesExist: response.indicesExist.length > 0,
+ loading: false,
+ },
+ })
+ );
+ }
+ searchSubscription$.unsubscribe();
+ } else if (!didCancel && response.isPartial && !response.isRunning) {
+ // TODO: Make response error status clearer
+ setLoading(false);
+ notifications.toasts.addWarning(i18n.ERROR_BEAT_FIELDS);
+ searchSubscription$.unsubscribe();
+ }
+ },
+ error: (msg) => {
+ if (!didCancel) {
+ setLoading(false);
+ }
- return () => {
- isSubscribed = false;
- return abortCtrl.abort();
- };
- }, [apolloClient, sourceId, defaultIndex, queryDeduplication]);
+ if (!(msg instanceof AbortError)) {
+ notifications.toasts.addDanger({
+ text: msg.message,
+ title: i18n.FAIL_BEAT_FIELDS,
+ });
+ }
+ },
+ });
+ };
+ abortCtrl.current.abort();
+ asyncSearch();
+ return () => {
+ didCancel = true;
+ abortCtrl.current.abort();
+ };
+ },
+ [data.search, dispatch, notifications.toasts, setLoading, sourcererScopeName]
+ );
- return state;
+ useEffect(() => {
+ if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) {
+ indexFieldsSearch(indexNames);
+ }
+ }, [indexNames, indexFieldsSearch, previousIndexesName]);
};
diff --git a/x-pack/plugins/security_solution/public/common/containers/source/mock.ts b/x-pack/plugins/security_solution/public/common/containers/source/mock.ts
index bba6a15d73970..7fcd11f71f081 100644
--- a/x-pack/plugins/security_solution/public/common/containers/source/mock.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/source/mock.ts
@@ -5,347 +5,296 @@
*/
import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants';
+import { DocValueFields } from '../../../../common/search_strategy';
+import { BrowserFields } from '../../../../common/search_strategy/index_fields';
-import { BrowserFields, DocValueFields } from '.';
-import { sourceQuery } from './index.gql_query';
-
-export const mocksSource = [
- {
- request: {
- query: sourceQuery,
- variables: {
- sourceId: 'default',
- defaultIndex: DEFAULT_INDEX_PATTERN,
- },
+export const mocksSource = {
+ indexFields: [
+ {
+ 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: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: '@timestamp',
+ searchable: true,
+ type: 'date',
+ aggregatable: true,
},
- result: {
- data: {
- source: {
- id: 'default',
- configuration: {},
- status: {
- indicesExist: true,
- winlogbeatIndices: [
- 'winlogbeat-7.0.0-2019.02.17',
- 'winlogbeat-7.0.0-2019.02.18',
- 'winlogbeat-7.0.0-2019.02.19',
- 'winlogbeat-7.0.0-2019.02.20',
- 'winlogbeat-7.0.0-2019.02.21',
- 'winlogbeat-7.0.0-2019.02.21-000001',
- 'winlogbeat-7.0.0-2019.02.22',
- 'winlogbeat-8.0.0-2019.02.19-000001',
- ],
- auditbeatIndices: [
- 'auditbeat-7.0.0-2019.02.17',
- 'auditbeat-7.0.0-2019.02.18',
- 'auditbeat-7.0.0-2019.02.19',
- 'auditbeat-7.0.0-2019.02.20',
- 'auditbeat-7.0.0-2019.02.21',
- 'auditbeat-7.0.0-2019.02.21-000001',
- 'auditbeat-7.0.0-2019.02.22',
- 'auditbeat-8.0.0-2019.02.19-000001',
- ],
- filebeatIndices: [
- 'filebeat-7.0.0-iot-2019.06',
- 'filebeat-7.0.0-iot-2019.07',
- 'filebeat-7.0.0-iot-2019.08',
- 'filebeat-7.0.0-iot-2019.09',
- 'filebeat-7.0.0-iot-2019.10',
- 'filebeat-8.0.0-2019.02.19-000001',
- ],
- indexFields: [
- {
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: '@timestamp',
- searchable: true,
- type: 'date',
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'agent.ephemeral_id',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'agent',
- description: null,
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'agent.hostname',
- searchable: true,
- type: 'string',
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'agent.id',
- searchable: true,
- type: 'string',
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'agent.name',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'auditd',
- description: null,
- example: null,
- format: '',
- indexes: ['auditbeat'],
- name: 'auditd.data.a0',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'auditd',
- description: null,
- example: null,
- format: '',
- indexes: ['auditbeat'],
- name: 'auditd.data.a1',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'auditd',
- description: null,
- example: null,
- format: '',
- indexes: ['auditbeat'],
- name: 'auditd.data.a2',
- searchable: true,
- type: 'string',
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'client.address',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'client',
- description: 'Bytes sent from the client to the server.',
- example: '184',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'client.bytes',
- searchable: true,
- type: 'number',
- aggregatable: true,
- },
- {
- category: 'client',
- description: 'Client domain.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'client.domain',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'client',
- description: 'Country ISO code.',
- example: 'CA',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'client.geo.country_iso_code',
- searchable: true,
- type: 'string',
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'cloud.account.id',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'cloud',
- description: 'Availability zone in which this host is running.',
- example: 'us-east-1c',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'cloud.availability_zone',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'container',
- description: 'Unique container id.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'container.id',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'container',
- description: 'Name of the image the container was built on.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'container.image.name',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'container',
- description: 'Container image tag.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'container.image.tag',
- searchable: true,
- type: 'string',
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.address',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- category: 'destination',
- description: 'Bytes sent from the destination to the source.',
- example: '184',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.bytes',
- searchable: true,
- type: 'number',
- aggregatable: true,
- },
- {
- category: 'destination',
- description: 'Destination domain.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.domain',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- {
- aggregatable: true,
- category: 'destination',
- description:
- 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.',
- example: '',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.ip',
- searchable: true,
- type: 'ip',
- },
- {
- aggregatable: true,
- category: 'destination',
- description: 'Port of the destination.',
- example: '',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.port',
- searchable: true,
- type: 'long',
- },
- {
- aggregatable: true,
- category: 'source',
- description:
- 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.',
- example: '',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'source.ip',
- searchable: true,
- type: 'ip',
- },
- {
- aggregatable: true,
- category: 'source',
- description: 'Port of the source.',
- example: '',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'source.port',
- searchable: true,
- type: 'long',
- },
- {
- 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: DEFAULT_INDEX_PATTERN,
- name: 'event.end',
- searchable: true,
- type: 'date',
- },
- ],
- },
- },
- },
+ {
+ 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: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'agent.ephemeral_id',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
},
- },
-];
+ {
+ category: 'agent',
+ description: null,
+ example: null,
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'agent.hostname',
+ searchable: true,
+ type: 'string',
+ 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: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'agent.id',
+ searchable: true,
+ type: 'string',
+ 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: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'agent.name',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'auditd',
+ description: null,
+ example: null,
+ format: '',
+ indexes: ['auditbeat'],
+ name: 'auditd.data.a0',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'auditd',
+ description: null,
+ example: null,
+ format: '',
+ indexes: ['auditbeat'],
+ name: 'auditd.data.a1',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'auditd',
+ description: null,
+ example: null,
+ format: '',
+ indexes: ['auditbeat'],
+ name: 'auditd.data.a2',
+ searchable: true,
+ type: 'string',
+ 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: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'client.address',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'client',
+ description: 'Bytes sent from the client to the server.',
+ example: '184',
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'client.bytes',
+ searchable: true,
+ type: 'number',
+ aggregatable: true,
+ },
+ {
+ category: 'client',
+ description: 'Client domain.',
+ example: null,
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'client.domain',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'client',
+ description: 'Country ISO code.',
+ example: 'CA',
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'client.geo.country_iso_code',
+ searchable: true,
+ type: 'string',
+ 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: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'cloud.account.id',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'cloud',
+ description: 'Availability zone in which this host is running.',
+ example: 'us-east-1c',
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'cloud.availability_zone',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'container',
+ description: 'Unique container id.',
+ example: null,
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'container.id',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'container',
+ description: 'Name of the image the container was built on.',
+ example: null,
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'container.image.name',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'container',
+ description: 'Container image tag.',
+ example: null,
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'container.image.tag',
+ searchable: true,
+ type: 'string',
+ 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: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'destination.address',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ category: 'destination',
+ description: 'Bytes sent from the destination to the source.',
+ example: '184',
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'destination.bytes',
+ searchable: true,
+ type: 'number',
+ aggregatable: true,
+ },
+ {
+ category: 'destination',
+ description: 'Destination domain.',
+ example: null,
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'destination.domain',
+ searchable: true,
+ type: 'string',
+ aggregatable: true,
+ },
+ {
+ aggregatable: true,
+ category: 'destination',
+ description: 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.',
+ example: '',
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'destination.ip',
+ searchable: true,
+ type: 'ip',
+ },
+ {
+ aggregatable: true,
+ category: 'destination',
+ description: 'Port of the destination.',
+ example: '',
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'destination.port',
+ searchable: true,
+ type: 'long',
+ },
+ {
+ aggregatable: true,
+ category: 'source',
+ description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.',
+ example: '',
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'source.ip',
+ searchable: true,
+ type: 'ip',
+ },
+ {
+ aggregatable: true,
+ category: 'source',
+ description: 'Port of the source.',
+ example: '',
+ format: '',
+ indexes: ['auditbeat', 'filebeat', 'packetbeat'],
+ name: 'source.port',
+ searchable: true,
+ type: 'long',
+ },
+ {
+ 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: DEFAULT_INDEX_PATTERN,
+ name: 'event.end',
+ searchable: true,
+ type: 'date',
+ },
+ ],
+};
export const mockIndexFields = [
{ aggregatable: true, name: '@timestamp', searchable: true, type: 'date' },
diff --git a/x-pack/plugins/security_solution/public/common/containers/source/translations.ts b/x-pack/plugins/security_solution/public/common/containers/source/translations.ts
new file mode 100644
index 0000000000000..f12a9a0b41a7b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/containers/source/translations.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 ERROR_BEAT_FIELDS = i18n.translate(
+ 'xpack.securitySolution.beatFields.errorSearchDescription',
+ {
+ defaultMessage: `An error has occurred on getting beat fields`,
+ }
+);
+
+export const FAIL_BEAT_FIELDS = i18n.translate(
+ 'xpack.securitySolution.beatFields.failSearchDescription',
+ {
+ defaultMessage: `Failed to run search on beat fields`,
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts
index 106294ba54f5a..be3d074811032 100644
--- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts
@@ -4,26 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const SOURCERER_FEATURE_FLAG_ON = false;
-
-export enum SecurityPageName {
- default = 'default',
- host = 'host',
- detections = 'detections',
- timeline = 'timeline',
- network = 'network',
-}
-
-export type SourceGroupsType = keyof typeof SecurityPageName;
-
-export const sourceGroups = {
- [SecurityPageName.default]: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'winlogbeat-*',
- 'blobbeat-*',
- ],
-};
+export const SOURCERER_FEATURE_FLAG_ON = true;
diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx
deleted file mode 100644
index b8017df09b738..0000000000000
--- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx
+++ /dev/null
@@ -1,23 +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 { indicesExistOrDataTemporarilyUnavailable } from './format';
-
-describe('indicesExistOrDataTemporarilyUnavailable', () => {
- it('it returns true when undefined', () => {
- let undefVar;
- const result = indicesExistOrDataTemporarilyUnavailable(undefVar);
- expect(result).toBeTruthy();
- });
- it('it returns true when true', () => {
- const result = indicesExistOrDataTemporarilyUnavailable(true);
- expect(result).toBeTruthy();
- });
- it('it returns false when false', () => {
- const result = indicesExistOrDataTemporarilyUnavailable(false);
- expect(result).toBeFalsy();
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts
deleted file mode 100644
index 8c9a16ed705ef..0000000000000
--- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts
+++ /dev/null
@@ -1,96 +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 { isEmpty, pick } from 'lodash/fp';
-import memoizeOne from 'memoize-one';
-import { set } from '@elastic/safer-lodash-set/fp';
-import { isUndefined } from 'lodash';
-import { IndexField } from '../../../graphql/types';
-import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns';
-
-export interface BrowserField {
- aggregatable: boolean;
- category: string;
- description: string | null;
- example: string | number | null;
- fields: Readonly>>;
- format: string;
- indexes: string[];
- name: string;
- searchable: boolean;
- type: string;
-}
-
-export interface DocValueFields {
- field: string;
- format: string;
-}
-
-export type BrowserFields = Readonly>>;
-
-export const getAllBrowserFields = (browserFields: BrowserFields): Array> =>
- Object.values(browserFields).reduce>>(
- (acc, namespace) => [
- ...acc,
- ...Object.values(namespace.fields != null ? namespace.fields : {}),
- ],
- []
- );
-
-export const getIndexFields = memoizeOne(
- (title: string, fields: IndexField[]): IIndexPattern =>
- fields && fields.length > 0
- ? {
- fields: fields.map((field) =>
- pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field)
- ),
- title,
- }
- : { fields: [], title },
- (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length
-);
-
-export const getBrowserFields = memoizeOne(
- (_title: string, fields: IndexField[]): BrowserFields =>
- fields && fields.length > 0
- ? fields.reduce(
- (accumulator: BrowserFields, field: IndexField) =>
- set([field.category, 'fields', field.name], field, accumulator),
- {}
- )
- : {},
- // Update the value only if _title has changed
- (newArgs, lastArgs) => newArgs[0] === lastArgs[0]
-);
-
-export const getDocValueFields = memoizeOne(
- (_title: string, fields: IndexField[]): DocValueFields[] =>
- fields && fields.length > 0
- ? fields.reduce((accumulator: DocValueFields[], field: IndexField) => {
- if (field.type === 'date' && accumulator.length < 100) {
- const format: string =
- field.format != null && !isEmpty(field.format) ? field.format : 'date_time';
- return [
- ...accumulator,
- {
- field: field.name,
- format,
- },
- ];
- }
- return accumulator;
- }, [])
- : [],
- // Update the value only if _title has changed
- (newArgs, lastArgs) => newArgs[0] === lastArgs[0]
-);
-
-export const indicesExistOrDataTemporarilyUnavailable = (
- indicesExist: boolean | null | undefined
-) => indicesExist || isUndefined(indicesExist);
-
-export const EMPTY_BROWSER_FIELDS = {};
-export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = [];
diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx
index 38af84e0968f8..673db7af2b5e6 100644
--- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx
@@ -4,28 +4,73 @@
* you may not use this file except in compliance with the Elastic License.
*/
+/* eslint-disable react/display-name */
+
+import React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
+import { Provider } from 'react-redux';
-import { getSourceDefaults, useSourceManager, UseSourceManager } from '.';
+import { useInitSourcerer } from '.';
+import { mockPatterns, mockSource } from './mocks';
+// import { SourcererScopeName } from '../../store/sourcerer/model';
+import { RouteSpyState } from '../../utils/route/types';
+import { SecurityPageName } from '../../../../common/constants';
+import { createStore, State } from '../../store';
import {
- mockSourceSelections,
- mockSourceGroup,
- mockSourceGroups,
- mockPatterns,
- mockSource,
-} from './mocks';
-import { SecurityPageName } from './constants';
-const mockSourceDefaults = mockSource(SecurityPageName.default);
+ apolloClientObservable,
+ createSecuritySolutionStorageMock,
+ kibanaObservable,
+ mockGlobalState,
+ SUB_PLUGINS_REDUCER,
+} from '../../mock';
+const mockSourceDefaults = mockSource;
+
+const mockRouteSpy: RouteSpyState = {
+ pageName: SecurityPageName.overview,
+ detailName: undefined,
+ tabName: undefined,
+ search: '',
+ pathName: '/',
+};
+const mockDispatch = jest.fn();
+jest.mock('react-redux', () => {
+ const original = jest.requireActual('react-redux');
+
+ return {
+ ...original,
+ useDispatch: () => mockDispatch,
+ };
+});
+jest.mock('../../utils/route/use_route_spy', () => ({
+ useRouteSpy: () => [mockRouteSpy],
+}));
jest.mock('../../lib/kibana', () => ({
useKibana: jest.fn().mockReturnValue({
services: {
+ application: {
+ capabilities: {
+ siem: {
+ crud: true,
+ },
+ },
+ },
data: {
indexPatterns: {
getTitles: jest.fn().mockImplementation(() => Promise.resolve(mockPatterns)),
},
+ search: {
+ search: jest.fn().mockImplementation(() => ({
+ subscribe: jest.fn().mockImplementation(() => ({
+ error: jest.fn(),
+ next: jest.fn(),
+ })),
+ })),
+ },
},
+ notifications: {},
},
}),
+ useUiSetting$: jest.fn().mockImplementation(() => [mockPatterns]),
}));
jest.mock('../../utils/apollo_context', () => ({
useApolloClient: jest.fn().mockReturnValue({
@@ -34,148 +79,193 @@ jest.mock('../../utils/apollo_context', () => ({
}));
describe('Sourcerer Hooks', () => {
- const testId = SecurityPageName.default;
- const uninitializedId = SecurityPageName.host;
+ // const testId = SourcererScopeName.default;
+ // const uninitializedId = SourcererScopeName.detections;
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
+ const state: State = mockGlobalState;
+ const { storage } = createSecuritySolutionStorageMock();
+ let store = createStore(
+ state,
+ SUB_PLUGINS_REDUCER,
+ apolloClientObservable,
+ kibanaObservable,
+ storage
+ );
+
+ beforeEach(() => {
+ store = createStore(
+ state,
+ SUB_PLUGINS_REDUCER,
+ apolloClientObservable,
+ kibanaObservable,
+ storage
+ );
+ });
describe('Initialization', () => {
- it('initializes loading default index patterns', async () => {
+ it('initializes loading default and timeline index patterns', async () => {
await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSourceManager()
- );
- await waitForNextUpdate();
- expect(result.current).toEqual({
- activeSourceGroupId: 'default',
- availableIndexPatterns: [],
- availableSourceGroupIds: [],
- isIndexPatternsLoading: true,
- sourceGroups: {},
- getManageSourceGroupById: result.current.getManageSourceGroupById,
- initializeSourceGroup: result.current.initializeSourceGroup,
- setActiveSourceGroupId: result.current.setActiveSourceGroupId,
- updateSourceGroupIndicies: result.current.updateSourceGroupIndicies,
+ const { waitForNextUpdate } = renderHook(() => useInitSourcerer(), {
+ wrapper: ({ children }) => {children},
});
- });
- });
- it('initializes loading default source group', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSourceManager()
- );
await waitForNextUpdate();
await waitForNextUpdate();
- expect(result.current).toEqual({
- activeSourceGroupId: 'default',
- availableIndexPatterns: mockPatterns,
- availableSourceGroupIds: [],
- isIndexPatternsLoading: false,
- sourceGroups: {},
- getManageSourceGroupById: result.current.getManageSourceGroupById,
- initializeSourceGroup: result.current.initializeSourceGroup,
- setActiveSourceGroupId: result.current.setActiveSourceGroupId,
- updateSourceGroupIndicies: result.current.updateSourceGroupIndicies,
+ expect(mockDispatch).toBeCalledTimes(2);
+ expect(mockDispatch.mock.calls[0][0]).toEqual({
+ type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
+ payload: { id: 'default', loading: true },
});
- });
- });
- it('initialize completes with formatted source group data', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSourceManager()
- );
- await waitForNextUpdate();
- await waitForNextUpdate();
- await waitForNextUpdate();
- expect(result.current).toEqual({
- activeSourceGroupId: testId,
- availableIndexPatterns: mockPatterns,
- availableSourceGroupIds: [testId],
- isIndexPatternsLoading: false,
- sourceGroups: {
- default: mockSourceGroup(testId),
- },
- getManageSourceGroupById: result.current.getManageSourceGroupById,
- initializeSourceGroup: result.current.initializeSourceGroup,
- setActiveSourceGroupId: result.current.setActiveSourceGroupId,
- updateSourceGroupIndicies: result.current.updateSourceGroupIndicies,
+ expect(mockDispatch.mock.calls[1][0]).toEqual({
+ type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
+ payload: { id: 'timeline', loading: true },
});
+ // expect(mockDispatch.mock.calls[1][0]).toEqual({
+ // type: 'x-pack/security_solution/local/sourcerer/SET_INDEX_PATTERNS_LIST',
+ // payload: { allIndexPatterns: mockPatterns, kibanaIndexPatterns: [] },
+ // });
});
});
+ // TO DO sourcerer @S
+ // it('initializes loading default source group', async () => {
+ // await act(async () => {
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => useInitSourcerer(),
+ // {
+ // wrapper: ({ children }) => {children},
+ // }
+ // );
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // expect(result.current).toEqual({
+ // activeSourcererScopeId: 'default',
+ // kibanaIndexPatterns: mockPatterns,
+ // isIndexPatternsLoading: false,
+ // getSourcererScopeById: result.current.getSourcererScopeById,
+ // setActiveSourcererScopeId: result.current.setActiveSourcererScopeId,
+ // updateSourcererScopeIndices: result.current.updateSourcererScopeIndices,
+ // });
+ // });
+ // });
+ // it('initialize completes with formatted source group data', async () => {
+ // await act(async () => {
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => useInitSourcerer(),
+ // {
+ // wrapper: ({ children }) => {children},
+ // }
+ // );
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // expect(result.current).toEqual({
+ // activeSourcererScopeId: testId,
+ // kibanaIndexPatterns: mockPatterns,
+ // isIndexPatternsLoading: false,
+ // getSourcererScopeById: result.current.getSourcererScopeById,
+ // setActiveSourcererScopeId: result.current.setActiveSourcererScopeId,
+ // updateSourcererScopeIndices: result.current.updateSourcererScopeIndices,
+ // });
+ // });
+ // });
});
- describe('Methods', () => {
- it('getManageSourceGroupById: initialized source group returns defaults', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSourceManager()
- );
- await waitForNextUpdate();
- await waitForNextUpdate();
- await waitForNextUpdate();
- const initializedSourceGroup = result.current.getManageSourceGroupById(testId);
- expect(initializedSourceGroup).toEqual(mockSourceGroup(testId));
- });
- });
- it('getManageSourceGroupById: uninitialized source group returns defaults', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSourceManager()
- );
- await waitForNextUpdate();
- await waitForNextUpdate();
- await waitForNextUpdate();
- const uninitializedSourceGroup = result.current.getManageSourceGroupById(uninitializedId);
- expect(uninitializedSourceGroup).toEqual(getSourceDefaults(uninitializedId, mockPatterns));
- });
- });
- it('initializeSourceGroup: initializes source group', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSourceManager()
- );
- await waitForNextUpdate();
- await waitForNextUpdate();
- await waitForNextUpdate();
- result.current.initializeSourceGroup(
- uninitializedId,
- mockSourceGroups[uninitializedId],
- true
- );
- await waitForNextUpdate();
- const initializedSourceGroup = result.current.getManageSourceGroupById(uninitializedId);
- expect(initializedSourceGroup.indexPatterns).toEqual(mockSourceSelections[uninitializedId]);
- });
- });
- it('setActiveSourceGroupId: active source group id gets set only if it gets initialized first', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSourceManager()
- );
- await waitForNextUpdate();
- expect(result.current.activeSourceGroupId).toEqual(testId);
- result.current.setActiveSourceGroupId(uninitializedId);
- expect(result.current.activeSourceGroupId).toEqual(testId);
- result.current.initializeSourceGroup(uninitializedId);
- result.current.setActiveSourceGroupId(uninitializedId);
- expect(result.current.activeSourceGroupId).toEqual(uninitializedId);
- });
- });
- it('updateSourceGroupIndicies: updates source group indicies', async () => {
- await act(async () => {
- const { result, waitForNextUpdate } = renderHook(() =>
- useSourceManager()
- );
- await waitForNextUpdate();
- await waitForNextUpdate();
- await waitForNextUpdate();
- let sourceGroup = result.current.getManageSourceGroupById(testId);
- expect(sourceGroup.indexPatterns).toEqual(mockSourceSelections[testId]);
- result.current.updateSourceGroupIndicies(testId, ['endgame-*', 'filebeat-*']);
- await waitForNextUpdate();
- sourceGroup = result.current.getManageSourceGroupById(testId);
- expect(sourceGroup.indexPatterns).toEqual(['endgame-*', 'filebeat-*']);
- });
- });
- });
+ // describe('Methods', () => {
+ // it('getSourcererScopeById: initialized source group returns defaults', async () => {
+ // await act(async () => {
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => useInitSourcerer(),
+ // {
+ // wrapper: ({ children }) => {children},
+ // }
+ // );
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // const initializedSourcererScope = result.current.getSourcererScopeById(testId);
+ // expect(initializedSourcererScope).toEqual(mockSourcererScope(testId));
+ // });
+ // });
+ // it('getSourcererScopeById: uninitialized source group returns defaults', async () => {
+ // await act(async () => {
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => useInitSourcerer(),
+ // {
+ // wrapper: ({ children }) => {children},
+ // }
+ // );
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // const uninitializedSourcererScope = result.current.getSourcererScopeById(uninitializedId);
+ // expect(uninitializedSourcererScope).toEqual(
+ // getSourceDefaults(uninitializedId, mockPatterns)
+ // );
+ // });
+ // });
+ // // it('initializeSourcererScope: initializes source group', async () => {
+ // // await act(async () => {
+ // // const { result, waitForNextUpdate } = renderHook(
+ // // () => useSourcerer(),
+ // // {
+ // // wrapper: ({ children }) => {children},
+ // // }
+ // // );
+ // // await waitForNextUpdate();
+ // // await waitForNextUpdate();
+ // // await waitForNextUpdate();
+ // // result.current.initializeSourcererScope(
+ // // uninitializedId,
+ // // mockSourcererScopes[uninitializedId],
+ // // true
+ // // );
+ // // await waitForNextUpdate();
+ // // const initializedSourcererScope = result.current.getSourcererScopeById(uninitializedId);
+ // // expect(initializedSourcererScope.selectedPatterns).toEqual(
+ // // mockSourcererScopes[uninitializedId]
+ // // );
+ // // });
+ // // });
+ // it('setActiveSourcererScopeId: active source group id gets set only if it gets initialized first', async () => {
+ // await act(async () => {
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => useInitSourcerer(),
+ // {
+ // wrapper: ({ children }) => {children},
+ // }
+ // );
+ // await waitForNextUpdate();
+ // expect(result.current.activeSourcererScopeId).toEqual(testId);
+ // result.current.setActiveSourcererScopeId(uninitializedId);
+ // expect(result.current.activeSourcererScopeId).toEqual(testId);
+ // // result.current.initializeSourcererScope(uninitializedId);
+ // result.current.setActiveSourcererScopeId(uninitializedId);
+ // expect(result.current.activeSourcererScopeId).toEqual(uninitializedId);
+ // });
+ // });
+ // it('updateSourcererScopeIndices: updates source group indices', async () => {
+ // await act(async () => {
+ // const { result, waitForNextUpdate } = renderHook(
+ // () => useInitSourcerer(),
+ // {
+ // wrapper: ({ children }) => {children},
+ // }
+ // );
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // await waitForNextUpdate();
+ // let sourceGroup = result.current.getSourcererScopeById(testId);
+ // expect(sourceGroup.selectedPatterns).toEqual(mockSourcererScopes[testId]);
+ // expect(sourceGroup.scopePatterns).toEqual(mockSourcererScopes[testId]);
+ // result.current.updateSourcererScopeIndices({
+ // id: testId,
+ // selectedPatterns: ['endgame-*', 'filebeat-*'],
+ // });
+ // await waitForNextUpdate();
+ // sourceGroup = result.current.getSourcererScopeById(testId);
+ // expect(sourceGroup.scopePatterns).toEqual(mockSourcererScopes[testId]);
+ // expect(sourceGroup.selectedPatterns).toEqual(['endgame-*', 'filebeat-*']);
+ // });
+ // });
+ // });
});
diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx
index 91907b45aa449..afacd68d71592 100644
--- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx
@@ -4,412 +4,72 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { get, noop, isEmpty } from 'lodash/fp';
-import React, { createContext, useCallback, useContext, useEffect, useReducer } from 'react';
-import { IIndexPattern } from 'src/plugins/data/public';
-
-import { NO_ALERT_INDEX } from '../../../../common/constants';
-import { useKibana } from '../../lib/kibana';
-
-import { SourceQuery } from '../../../graphql/types';
-
-import { sourceQuery } from '../source/index.gql_query';
-import { useApolloClient } from '../../utils/apollo_context';
-import {
- sourceGroups,
- SecurityPageName,
- SourceGroupsType,
- SOURCERER_FEATURE_FLAG_ON,
-} from './constants';
-import {
- BrowserFields,
- DocValueFields,
- EMPTY_BROWSER_FIELDS,
- EMPTY_DOCVALUE_FIELD,
- getBrowserFields,
- getDocValueFields,
- getIndexFields,
- indicesExistOrDataTemporarilyUnavailable,
-} from './format';
-
-// TYPES
-interface ManageSource {
- browserFields: BrowserFields;
- defaultPatterns: string[];
- docValueFields: DocValueFields[];
- errorMessage: string | null;
- id: SourceGroupsType;
- indexPattern: IIndexPattern;
- indexPatterns: string[];
- indicesExist: boolean | undefined | null;
- loading: boolean;
-}
-
-interface ManageSourceInit extends Partial {
- id: SourceGroupsType;
-}
-
-type ManageSourceGroupById = {
- [id in SourceGroupsType]?: ManageSource;
-};
-
-type ActionManageSource =
- | {
- type: 'SET_SOURCE';
- id: SourceGroupsType;
- defaultIndex: string[];
- payload: ManageSourceInit;
- }
- | {
- type: 'SET_IS_SOURCE_LOADING';
- id: SourceGroupsType;
- payload: boolean;
- }
- | {
- type: 'SET_ACTIVE_SOURCE_GROUP_ID';
- payload: SourceGroupsType;
- }
- | {
- type: 'SET_AVAILABLE_INDEX_PATTERNS';
- payload: string[];
- }
- | {
- type: 'SET_IS_INDEX_PATTERNS_LOADING';
- payload: boolean;
- };
-
-interface ManageSourcerer {
- activeSourceGroupId: SourceGroupsType;
- availableIndexPatterns: string[];
- availableSourceGroupIds: SourceGroupsType[];
- isIndexPatternsLoading: boolean;
- sourceGroups: ManageSourceGroupById;
-}
-
-export interface UseSourceManager extends ManageSourcerer {
- getManageSourceGroupById: (id: SourceGroupsType) => ManageSource;
- initializeSourceGroup: (
- id: SourceGroupsType,
- indexToAdd?: string[] | null,
- onlyCheckIndexToAdd?: boolean
- ) => void;
- setActiveSourceGroupId: (id: SourceGroupsType) => void;
- updateSourceGroupIndicies: (id: SourceGroupsType, updatedIndicies: string[]) => void;
-}
-
-// DEFAULTS/INIT
-export const getSourceDefaults = (id: SourceGroupsType, defaultIndex: string[]) => ({
- browserFields: EMPTY_BROWSER_FIELDS,
- defaultPatterns: defaultIndex,
- docValueFields: EMPTY_DOCVALUE_FIELD,
- errorMessage: null,
- id,
- indexPattern: getIndexFields(defaultIndex.join(), []),
- indexPatterns: defaultIndex,
- indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined),
- loading: true,
-});
-
-const initManageSource: ManageSourcerer = {
- activeSourceGroupId: SecurityPageName.default,
- availableIndexPatterns: [],
- availableSourceGroupIds: [],
- isIndexPatternsLoading: true,
- sourceGroups: {},
-};
-const init: UseSourceManager = {
- ...initManageSource,
- getManageSourceGroupById: (id: SourceGroupsType) => getSourceDefaults(id, []),
- initializeSourceGroup: () => noop,
- setActiveSourceGroupId: () => noop,
- updateSourceGroupIndicies: () => noop,
-};
-
-const reducerManageSource = (state: ManageSourcerer, action: ActionManageSource) => {
- switch (action.type) {
- case 'SET_SOURCE':
- return {
- ...state,
- sourceGroups: {
- ...state.sourceGroups,
- [action.id]: {
- ...getSourceDefaults(action.id, action.defaultIndex),
- ...state.sourceGroups[action.id],
- ...action.payload,
- },
- },
- availableSourceGroupIds: state.availableSourceGroupIds.includes(action.id)
- ? state.availableSourceGroupIds
- : [...state.availableSourceGroupIds, action.id],
- };
- case 'SET_IS_SOURCE_LOADING':
- return {
- ...state,
- sourceGroups: {
- ...state.sourceGroups,
- [action.id]: {
- ...state.sourceGroups[action.id],
- id: action.id,
- loading: action.payload,
- },
- },
- };
- case 'SET_ACTIVE_SOURCE_GROUP_ID':
- return {
- ...state,
- activeSourceGroupId: action.payload,
- };
- case 'SET_AVAILABLE_INDEX_PATTERNS':
- return {
- ...state,
- availableIndexPatterns: action.payload,
- };
- case 'SET_IS_INDEX_PATTERNS_LOADING':
- return {
- ...state,
- isIndexPatternsLoading: action.payload,
- };
- default:
- return state;
- }
-};
-
-// HOOKS
-export const useSourceManager = (): UseSourceManager => {
- const {
- services: {
- data: { indexPatterns },
- },
- } = useKibana();
- const apolloClient = useApolloClient();
- const [state, dispatch] = useReducer(reducerManageSource, initManageSource);
-
- // Kibana Index Patterns
- const setIsIndexPatternsLoading = useCallback((loading: boolean) => {
- dispatch({
- type: 'SET_IS_INDEX_PATTERNS_LOADING',
- payload: loading,
- });
- }, []);
- const getDefaultIndex = useCallback(
- (indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) => {
- const filterIndexAdd = (indexToAdd ?? []).filter((item) => item !== NO_ALERT_INDEX);
- if (!isEmpty(filterIndexAdd)) {
- return onlyCheckIndexToAdd
- ? filterIndexAdd.sort()
- : [
- ...state.availableIndexPatterns,
- ...filterIndexAdd.filter((index) => !state.availableIndexPatterns.includes(index)),
- ].sort();
- }
- return state.availableIndexPatterns.sort();
- },
- [state.availableIndexPatterns]
- );
- const setAvailableIndexPatterns = useCallback((availableIndexPatterns: string[]) => {
- dispatch({
- type: 'SET_AVAILABLE_INDEX_PATTERNS',
- payload: availableIndexPatterns,
- });
- }, []);
- const fetchKibanaIndexPatterns = useCallback(() => {
- setIsIndexPatternsLoading(true);
- const abortCtrl = new AbortController();
-
- async function fetchTitles() {
- try {
- const result = await indexPatterns.getTitles();
- setAvailableIndexPatterns(result);
- setIsIndexPatternsLoading(false);
- } catch (error) {
- setIsIndexPatternsLoading(false);
- }
- }
-
- fetchTitles();
-
- return () => {
- return abortCtrl.abort();
- };
- }, [indexPatterns, setAvailableIndexPatterns, setIsIndexPatternsLoading]);
-
- // Security Solution Source Groups
- const setActiveSourceGroupId = useCallback(
- (sourceGroupId: SourceGroupsType) => {
- if (state.availableSourceGroupIds.includes(sourceGroupId)) {
- dispatch({
- type: 'SET_ACTIVE_SOURCE_GROUP_ID',
- payload: sourceGroupId,
- });
- }
- },
- [state.availableSourceGroupIds]
- );
- const setIsSourceLoading = useCallback(
- ({ id, loading }: { id: SourceGroupsType; loading: boolean }) => {
- dispatch({
- type: 'SET_IS_SOURCE_LOADING',
- id,
- payload: loading,
- });
- },
+import deepEqual from 'fast-deep-equal';
+import isEqual from 'lodash/isEqual';
+import { useEffect, useMemo } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
+import { ManageScope, SourcererScopeName } from '../../store/sourcerer/model';
+import { useIndexFields } from '../source';
+import { State } from '../../store';
+import { useUserInfo } from '../../../detections/components/user_info';
+
+export const useInitSourcerer = (
+ scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default
+) => {
+ const dispatch = useDispatch();
+
+ const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo();
+ const getConfigIndexPatternsSelector = useMemo(
+ () => sourcererSelectors.configIndexPatternsSelector(),
[]
);
- const enrichSource = useCallback(
- (id: SourceGroupsType, indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) => {
- let isSubscribed = true;
- const abortCtrl = new AbortController();
- const defaultIndex = getDefaultIndex(indexToAdd, onlyCheckIndexToAdd);
- const selectedPatterns = defaultIndex.filter((pattern) =>
- state.availableIndexPatterns.includes(pattern)
- );
- if (state.sourceGroups[id] == null) {
- dispatch({
- type: 'SET_SOURCE',
- id,
- defaultIndex: selectedPatterns,
- payload: { defaultPatterns: defaultIndex, id },
- });
- }
-
- async function fetchSource() {
- if (!apolloClient) return;
- setIsSourceLoading({ id, loading: true });
- try {
- const result = await apolloClient.query({
- query: sourceQuery,
- fetchPolicy: 'network-only',
- variables: {
- sourceId: 'default', // always
- defaultIndex: selectedPatterns,
- },
- context: {
- fetchOptions: {
- signal: abortCtrl.signal,
- },
- },
- });
- if (isSubscribed) {
- dispatch({
- type: 'SET_SOURCE',
- id,
- defaultIndex: selectedPatterns,
- payload: {
- browserFields: getBrowserFields(
- selectedPatterns.join(),
- get('data.source.status.indexFields', result)
- ),
- docValueFields: getDocValueFields(
- selectedPatterns.join(),
- get('data.source.status.indexFields', result)
- ),
- errorMessage: null,
- id,
- indexPattern: getIndexFields(
- selectedPatterns.join(),
- get('data.source.status.indexFields', result)
- ),
- indexPatterns: selectedPatterns,
- indicesExist: indicesExistOrDataTemporarilyUnavailable(
- get('data.source.status.indicesExist', result)
- ),
- loading: false,
- },
- });
- }
- } catch (error) {
- if (isSubscribed) {
- dispatch({
- type: 'SET_SOURCE',
- id,
- defaultIndex: selectedPatterns,
- payload: {
- errorMessage: error.message,
- id,
- loading: false,
- },
- });
- }
- }
- }
-
- fetchSource();
-
- return () => {
- isSubscribed = false;
- return abortCtrl.abort();
- };
- },
- [
- apolloClient,
- getDefaultIndex,
- setIsSourceLoading,
- state.availableIndexPatterns,
- state.sourceGroups,
- ]
- );
+ const ConfigIndexPatterns = useSelector(getConfigIndexPatternsSelector, isEqual);
- const initializeSourceGroup = useCallback(
- (id: SourceGroupsType, indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) =>
- enrichSource(id, indexToAdd, onlyCheckIndexToAdd),
- [enrichSource]
- );
-
- const updateSourceGroupIndicies = useCallback(
- (id: SourceGroupsType, updatedIndicies: string[]) => enrichSource(id, updatedIndicies, true),
- [enrichSource]
- );
- const getManageSourceGroupById = useCallback(
- (id: SourceGroupsType) => {
- const sourceById = state.sourceGroups[id];
- if (sourceById != null) {
- return sourceById;
- }
- return getSourceDefaults(id, getDefaultIndex());
- },
- [getDefaultIndex, state.sourceGroups]
- );
+ useIndexFields(scopeId);
+ useIndexFields(SourcererScopeName.timeline);
- // load initial default index
useEffect(() => {
- fetchKibanaIndexPatterns();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ if (!loadingSignalIndex && signalIndexName != null) {
+ dispatch(sourcererActions.setSignalIndexName({ signalIndexName }));
+ }
+ }, [dispatch, loadingSignalIndex, signalIndexName]);
+ // Related to timeline
useEffect(() => {
- if (!state.isIndexPatternsLoading) {
- Object.entries(sourceGroups).forEach(([key, value]) =>
- initializeSourceGroup(key as SourceGroupsType, value, true)
+ if (!loadingSignalIndex && signalIndexName != null) {
+ dispatch(
+ sourcererActions.setSelectedIndexPatterns({
+ id: SourcererScopeName.timeline,
+ selectedPatterns: [...ConfigIndexPatterns, signalIndexName],
+ })
);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state.isIndexPatternsLoading]);
+ }, [ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]);
- return {
- ...state,
- getManageSourceGroupById,
- initializeSourceGroup,
- setActiveSourceGroupId,
- updateSourceGroupIndicies,
- };
+ // Related to the detection page
+ useEffect(() => {
+ if (
+ scopeId === SourcererScopeName.detections &&
+ isSignalIndexExists &&
+ signalIndexName != null
+ ) {
+ dispatch(
+ sourcererActions.setSelectedIndexPatterns({
+ id: scopeId,
+ selectedPatterns: [signalIndexName],
+ })
+ );
+ }
+ }, [dispatch, isSignalIndexExists, scopeId, signalIndexName]);
};
-const ManageSourceContext = createContext(init);
-
-export const useManageSource = () => useContext(ManageSourceContext);
-
-interface ManageSourceProps {
- children: React.ReactNode;
-}
-
-export const MaybeManageSource = ({ children }: ManageSourceProps) => {
- const indexPatternManager = useSourceManager();
- return (
-
- {children}
-
+export const useSourcererScope = (scope: SourcererScopeName = SourcererScopeName.default) => {
+ const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
+ const SourcererScope = useSelector(
+ (state) => sourcererScopeSelector(state, scope),
+ deepEqual
);
+ return SourcererScope;
};
-export const ManageSource = SOURCERER_FEATURE_FLAG_ON
- ? MaybeManageSource
- : ({ children }: ManageSourceProps) => <>{children}>;
diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts
index cde14e54694f0..c34a6917f300e 100644
--- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts
+++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts
@@ -4,8 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SecurityPageName } from './constants';
-import { getSourceDefaults } from './index';
+import { initSourcererScope } from '../../store/sourcerer/model';
export const mockPatterns = [
'auditbeat-*',
@@ -14,32 +13,10 @@ export const mockPatterns = [
'logs-*',
'packetbeat-*',
'winlogbeat-*',
+ 'journalbeat-*',
];
-export const mockSourceGroups = {
- [SecurityPageName.default]: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'blobbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'winlogbeat-*',
- ],
- [SecurityPageName.host]: [
- 'apm-*-transaction*',
- 'endgame-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
-};
-
-export const mockSourceSelections = {
- [SecurityPageName.default]: ['auditbeat-*', 'endgame-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'],
- [SecurityPageName.host]: ['endgame-*', 'logs-*', 'packetbeat-*', 'winlogbeat-*'],
-};
-export const mockSource = (testId: SecurityPageName.default | SecurityPageName.host) => ({
+export const mockSource = {
data: {
source: {
id: 'default',
@@ -50,7 +27,7 @@ export const mockSource = (testId: SecurityPageName.default | SecurityPageName.h
category: '_id',
description: 'Each document has an _id that uniquely identifies it',
example: 'Y-6TfmcB0WOhS6qyMv3s',
- indexes: mockSourceSelections[testId],
+ indexes: mockPatterns,
name: '_id',
searchable: true,
type: 'string',
@@ -67,48 +44,45 @@ export const mockSource = (testId: SecurityPageName.default | SecurityPageName.h
loading: false,
networkStatus: 7,
stale: false,
-});
+};
-export const mockSourceGroup = (testId: SecurityPageName.default | SecurityPageName.host) => {
- const indexes = mockSourceSelections[testId];
- return {
- ...getSourceDefaults(testId, mockPatterns),
- defaultPatterns: mockSourceGroups[testId],
- browserFields: {
- _id: {
- fields: {
- _id: {
- __typename: 'IndexField',
- aggregatable: false,
- category: '_id',
- description: 'Each document has an _id that uniquely identifies it',
- esTypes: null,
- example: 'Y-6TfmcB0WOhS6qyMv3s',
- format: null,
- indexes,
- name: '_id',
- searchable: true,
- subType: null,
- type: 'string',
- },
- },
- },
- },
- indexPattern: {
- fields: [
- {
+export const mockSourcererScope = {
+ ...initSourcererScope,
+ scopePatterns: mockPatterns,
+ browserFields: {
+ _id: {
+ fields: {
+ _id: {
+ __typename: 'IndexField',
aggregatable: false,
+ category: '_id',
+ description: 'Each document has an _id that uniquely identifies it',
esTypes: null,
+ example: 'Y-6TfmcB0WOhS6qyMv3s',
+ format: null,
+ indexes: mockPatterns,
name: '_id',
searchable: true,
subType: null,
type: 'string',
},
- ],
- title: indexes.join(),
+ },
},
- indexPatterns: indexes,
- indicesExist: true,
- loading: false,
- };
+ },
+ indexPattern: {
+ fields: [
+ {
+ aggregatable: false,
+ esTypes: null,
+ name: '_id',
+ searchable: true,
+ subType: null,
+ type: 'string',
+ },
+ ],
+ title: mockPatterns.join(),
+ },
+ selectedPatterns: mockPatterns,
+ indicesExist: true,
+ loading: false,
};
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
index 573ef92f7e069..3051459d5de0c 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts
@@ -12,9 +12,25 @@ import {
createStartServicesMock,
createWithKibanaMock,
} from '../kibana_react.mock';
-
+const mockStartServicesMock = createStartServicesMock();
export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') };
-export const useKibana = jest.fn().mockReturnValue({ services: createStartServicesMock() });
+export const useKibana = jest.fn().mockReturnValue({
+ services: {
+ ...mockStartServicesMock,
+ data: {
+ ...mockStartServicesMock.data,
+ search: {
+ ...mockStartServicesMock.data.search,
+ search: jest.fn().mockImplementation(() => ({
+ subscribe: jest.fn().mockImplementation(() => ({
+ error: jest.fn(),
+ next: jest.fn(),
+ })),
+ })),
+ },
+ },
+ },
+});
export const useUiSetting = jest.fn(createUseUiSettingMock());
export const useUiSetting$ = jest.fn(createUseUiSetting$Mock());
export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http);
diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
index a74c9a6d2009d..0944b6aa27f67 100644
--- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts
@@ -22,11 +22,15 @@ import {
DEFAULT_TO,
DEFAULT_INTERVAL_TYPE,
DEFAULT_INTERVAL_VALUE,
+ DEFAULT_INDEX_PATTERN,
} from '../../../common/constants';
import { networkModel } from '../../network/store';
import { TimelineType, TimelineStatus } from '../../../common/types/timeline';
import { mockManagementState } from '../../management/store/reducer';
import { ManagementState } from '../../management/types';
+import { initialSourcererState, SourcererScopeName } from '../store/sourcerer/model';
+import { mockBrowserFields, mockDocValueFields } from '../containers/source/mock';
+import { mockIndexPattern } from './index_pattern';
export const mockGlobalState: State = {
app: {
@@ -203,6 +207,7 @@ export const mockGlobalState: State = {
id: 'test',
savedObjectId: null,
columns: defaultHeaders,
+ indexNames: DEFAULT_INDEX_PATTERN,
itemsPerPage: 5,
dataProviders: [],
description: '',
@@ -241,6 +246,28 @@ export const mockGlobalState: State = {
},
insertTimeline: null,
},
+ sourcerer: {
+ ...initialSourcererState,
+ sourcererScopes: {
+ ...initialSourcererState.sourcererScopes,
+ [SourcererScopeName.default]: {
+ ...initialSourcererState.sourcererScopes[SourcererScopeName.default],
+ selectedPatterns: DEFAULT_INDEX_PATTERN,
+ browserFields: mockBrowserFields,
+ indexPattern: mockIndexPattern,
+ docValueFields: mockDocValueFields,
+ loading: false,
+ },
+ [SourcererScopeName.timeline]: {
+ ...initialSourcererState.sourcererScopes[SourcererScopeName.timeline],
+ selectedPatterns: DEFAULT_INDEX_PATTERN,
+ browserFields: mockBrowserFields,
+ indexPattern: mockIndexPattern,
+ docValueFields: mockDocValueFields,
+ loading: false,
+ },
+ },
+ },
/**
* These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture,
* they are cast to mutable versions here.
diff --git a/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts b/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts
index 826057560f942..e4abc17e9034c 100644
--- a/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts
@@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const mockIndexPattern = {
+import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns';
+
+export const mockIndexPattern: IIndexPattern = {
fields: [
{
name: '@timestamp',
@@ -93,3 +95,5 @@ export const mockIndexPattern = {
],
title: 'filebeat-*,auditbeat-*,packetbeat-*',
};
+
+export const mockIndexNames = ['filebeat-*', 'auditbeat-*', 'packetbeat-*'];
diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
index 26013915315af..6403a50ad4a1d 100644
--- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
+++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts
@@ -2124,6 +2124,7 @@ export const mockTimelineModel: TimelineModel = {
highlightedDropAndProviderId: '',
historyIds: [],
id: 'ef579e40-jibber-jabber',
+ indexNames: [],
isFavorite: false,
isLive: false,
isLoading: false,
@@ -2228,6 +2229,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
highlightedDropAndProviderId: '',
historyIds: [],
id: TimelineId.active,
+ indexNames: [],
isFavorite: false,
isLive: false,
isLoading: false,
diff --git a/x-pack/plugins/security_solution/public/common/store/actions.ts b/x-pack/plugins/security_solution/public/common/store/actions.ts
index 6b446ab6692d9..f4134b5c47c2c 100644
--- a/x-pack/plugins/security_solution/public/common/store/actions.ts
+++ b/x-pack/plugins/security_solution/public/common/store/actions.ts
@@ -12,6 +12,7 @@ import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store
export { appActions } from './app';
export { dragAndDropActions } from './drag_and_drop';
export { inputsActions } from './inputs';
+export { sourcererActions } from './sourcerer';
import { RoutingAction } from './routing';
export type AppAction =
diff --git a/x-pack/plugins/security_solution/public/common/store/model.ts b/x-pack/plugins/security_solution/public/common/store/model.ts
index 0032a95cce321..04603d0607583 100644
--- a/x-pack/plugins/security_solution/public/common/store/model.ts
+++ b/x-pack/plugins/security_solution/public/common/store/model.ts
@@ -7,4 +7,5 @@
export { appModel } from './app';
export { dragAndDropModel } from './drag_and_drop';
export { inputsModel } from './inputs';
+export { sourcererModel } from './sourcerer';
export * from './types';
diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts
index a0977cea71da7..60cb6a4e960bd 100644
--- a/x-pack/plugins/security_solution/public/common/store/reducer.ts
+++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts
@@ -9,6 +9,7 @@ import { combineReducers, PreloadedState, AnyAction, Reducer } from 'redux';
import { appReducer, initialAppState } from './app';
import { dragAndDropReducer, initialDragAndDropState } from './drag_and_drop';
import { createInitialInputsState, inputsReducer } from './inputs';
+import { sourcererReducer, sourcererModel } from './sourcerer';
import { HostsPluginReducer } from '../../hosts/store';
import { NetworkPluginReducer } from '../../network/store';
@@ -18,6 +19,7 @@ import { SecuritySubPlugins } from '../../app/types';
import { ManagementPluginReducer } from '../../management';
import { State } from './types';
import { AppAction } from './actions';
+import { KibanaIndexPatterns } from './sourcerer/model';
export type SubPluginsInitReducer = HostsPluginReducer &
NetworkPluginReducer &
@@ -28,13 +30,22 @@ export type SubPluginsInitReducer = HostsPluginReducer &
* Factory for the 'initialState' that is used to preload state into the Security App's redux store.
*/
export const createInitialState = (
- pluginsInitState: SecuritySubPlugins['store']['initialState']
+ pluginsInitState: SecuritySubPlugins['store']['initialState'],
+ {
+ kibanaIndexPatterns,
+ configIndexPatterns,
+ }: { kibanaIndexPatterns: KibanaIndexPatterns; configIndexPatterns: string[] }
): PreloadedState => {
const preloadedState: PreloadedState = {
app: initialAppState,
dragAndDrop: initialDragAndDropState,
...pluginsInitState,
inputs: createInitialInputsState(),
+ sourcerer: {
+ ...sourcererModel.initialSourcererState,
+ kibanaIndexPatterns,
+ configIndexPatterns,
+ },
};
return preloadedState;
};
@@ -49,5 +60,6 @@ export const createReducer: (
app: appReducer,
dragAndDrop: dragAndDropReducer,
inputs: inputsReducer,
+ sourcerer: sourcererReducer,
...pluginsReducer,
});
diff --git a/x-pack/plugins/security_solution/public/common/store/selectors.ts b/x-pack/plugins/security_solution/public/common/store/selectors.ts
index b938bae39b634..3cefd92bf9e60 100644
--- a/x-pack/plugins/security_solution/public/common/store/selectors.ts
+++ b/x-pack/plugins/security_solution/public/common/store/selectors.ts
@@ -7,3 +7,4 @@
export { appSelectors } from './app';
export { dragAndDropSelectors } from './drag_and_drop';
export { inputsSelectors } from './inputs';
+export { sourcererSelectors } from './sourcerer';
diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts
new file mode 100644
index 0000000000000..0b40586798f09
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts
@@ -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 actionCreatorFactory from 'typescript-fsa';
+import { TimelineEventsType } from '../../../../common/types/timeline';
+
+import { KibanaIndexPatterns, ManageScopeInit, SourcererScopeName } from './model';
+
+const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer');
+
+export const setSource = actionCreator<{
+ id: SourcererScopeName;
+ payload: ManageScopeInit;
+}>('SET_SOURCE');
+
+export const setIndexPatternsList = actionCreator<{
+ kibanaIndexPatterns: KibanaIndexPatterns;
+ configIndexPatterns: string[];
+}>('SET_INDEX_PATTERNS_LIST');
+
+export const setSignalIndexName = actionCreator<{ signalIndexName: string }>(
+ 'SET_SIGNAL_INDEX_NAME'
+);
+
+export const setSourcererScopeLoading = actionCreator<{ id: SourcererScopeName; loading: boolean }>(
+ 'SET_SOURCERER_SCOPE_LOADING'
+);
+
+export const setSelectedIndexPatterns = actionCreator<{
+ id: SourcererScopeName;
+ selectedPatterns: string[];
+ eventType?: TimelineEventsType;
+}>('SET_SELECTED_INDEX_PATTERNS');
diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/index.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/index.ts
new file mode 100644
index 0000000000000..551c7d8e3efbc
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/index.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 * as sourcererActions from './actions';
+import * as sourcererModel from './model';
+import * as sourcererSelectors from './selectors';
+
+export { sourcererActions, sourcererModel, sourcererSelectors };
+export * from './reducer';
diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts
new file mode 100644
index 0000000000000..93f7ff95dfb00
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.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 { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns';
+import { DocValueFields } from '../../../../common/search_strategy/common';
+import {
+ BrowserFields,
+ EMPTY_BROWSER_FIELDS,
+ EMPTY_DOCVALUE_FIELD,
+ EMPTY_INDEX_PATTERN,
+} from '../../../../common/search_strategy/index_fields';
+
+export type ErrorModel = Error[];
+
+export enum SourcererScopeName {
+ default = 'default',
+ detections = 'detections',
+ timeline = 'timeline',
+}
+
+export interface ManageScope {
+ browserFields: BrowserFields;
+ docValueFields: DocValueFields[];
+ errorMessage: string | null;
+ id: SourcererScopeName;
+ indexPattern: IIndexPattern;
+ indicesExist: boolean | undefined | null;
+ loading: boolean;
+ selectedPatterns: string[];
+}
+
+export interface ManageScopeInit extends Partial {
+ id: SourcererScopeName;
+}
+
+export type SourcererScopeById = {
+ [id in SourcererScopeName]: ManageScope;
+};
+
+export type KibanaIndexPatterns = Array<{ id: string; title: string }>;
+
+// ManageSourcerer
+export interface SourcererModel {
+ kibanaIndexPatterns: KibanaIndexPatterns;
+ configIndexPatterns: string[];
+ signalIndexName: string | null;
+ sourcererScopes: SourcererScopeById;
+}
+
+export const initSourcererScope = {
+ browserFields: EMPTY_BROWSER_FIELDS,
+ docValueFields: EMPTY_DOCVALUE_FIELD,
+ errorMessage: null,
+ indexPattern: EMPTY_INDEX_PATTERN,
+ indicesExist: true,
+ loading: true,
+ selectedPatterns: [],
+};
+
+export const initialSourcererState: SourcererModel = {
+ kibanaIndexPatterns: [],
+ configIndexPatterns: [],
+ signalIndexName: null,
+ sourcererScopes: {
+ [SourcererScopeName.default]: {
+ ...initSourcererScope,
+ id: SourcererScopeName.default,
+ },
+ [SourcererScopeName.detections]: {
+ ...initSourcererScope,
+ id: SourcererScopeName.detections,
+ },
+ [SourcererScopeName.timeline]: {
+ ...initSourcererScope,
+ id: SourcererScopeName.timeline,
+ },
+ },
+};
+
+export type FSourcererScopePatterns = {
+ [id in SourcererScopeName]: string[];
+};
+export type SourcererScopePatterns = Partial;
diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts
new file mode 100644
index 0000000000000..b65d4d6338e50
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts
@@ -0,0 +1,93 @@
+/*
+ * 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/isEmpty';
+import { reducerWithInitialState } from 'typescript-fsa-reducers';
+
+import {
+ setIndexPatternsList,
+ setSourcererScopeLoading,
+ setSelectedIndexPatterns,
+ setSignalIndexName,
+ setSource,
+} from './actions';
+import { initialSourcererState, SourcererModel, SourcererScopeName } from './model';
+
+export type SourcererState = SourcererModel;
+
+export const sourcererReducer = reducerWithInitialState(initialSourcererState)
+ .case(setIndexPatternsList, (state, { kibanaIndexPatterns, configIndexPatterns }) => ({
+ ...state,
+ kibanaIndexPatterns,
+ configIndexPatterns,
+ }))
+ .case(setSignalIndexName, (state, { signalIndexName }) => ({
+ ...state,
+ signalIndexName,
+ }))
+ .case(setSourcererScopeLoading, (state, { id, loading }) => ({
+ ...state,
+ sourcererScopes: {
+ ...state.sourcererScopes,
+ [id]: {
+ ...state.sourcererScopes[id],
+ loading,
+ },
+ },
+ }))
+ .case(setSelectedIndexPatterns, (state, { id, selectedPatterns, eventType }) => {
+ const kibanaIndexPatterns = state.kibanaIndexPatterns.map((kip) => kip.title);
+ const newSelectedPatterns = selectedPatterns.filter(
+ (sp) =>
+ state.configIndexPatterns.includes(sp) ||
+ kibanaIndexPatterns.includes(sp) ||
+ (!isEmpty(state.signalIndexName) && state.signalIndexName === sp)
+ );
+ let defaultIndexPatterns = state.configIndexPatterns;
+ if (id === SourcererScopeName.timeline && isEmpty(newSelectedPatterns)) {
+ if (eventType === 'all' && !isEmpty(state.signalIndexName)) {
+ defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? ''];
+ } else if (eventType === 'raw') {
+ defaultIndexPatterns = state.configIndexPatterns;
+ } else if (
+ !isEmpty(state.signalIndexName) &&
+ (eventType === 'signal' || eventType === 'alert')
+ ) {
+ defaultIndexPatterns = [state.signalIndexName ?? ''];
+ }
+ } else if (id === SourcererScopeName.detections && isEmpty(newSelectedPatterns)) {
+ defaultIndexPatterns = [state.signalIndexName ?? ''];
+ }
+ return {
+ ...state,
+ sourcererScopes: {
+ ...state.sourcererScopes,
+ [id]: {
+ ...state.sourcererScopes[id],
+ selectedPatterns: isEmpty(newSelectedPatterns)
+ ? defaultIndexPatterns
+ : newSelectedPatterns,
+ },
+ },
+ };
+ })
+ .case(setSource, (state, { id, payload }) => {
+ const { ...sourcererScopes } = payload;
+ return {
+ ...state,
+ sourcererScopes: {
+ ...state.sourcererScopes,
+ [id]: {
+ ...state.sourcererScopes[id],
+ ...sourcererScopes,
+ ...(state.sourcererScopes[id].selectedPatterns.length === 0
+ ? { selectedPatterns: state.configIndexPatterns }
+ : {}),
+ },
+ },
+ };
+ })
+ .build();
diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts
new file mode 100644
index 0000000000000..ca9ea26ba5bac
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 { createSelector } from 'reselect';
+import { State } from '../types';
+import { SourcererScopeById, KibanaIndexPatterns, SourcererScopeName, ManageScope } from './model';
+
+export const sourcererKibanaIndexPatternsSelector = ({ sourcerer }: State): KibanaIndexPatterns =>
+ sourcerer.kibanaIndexPatterns;
+
+export const sourcererSignalIndexNameSelector = ({ sourcerer }: State): string | null =>
+ sourcerer.signalIndexName;
+
+export const sourcererConfigIndexPatternsSelector = ({ sourcerer }: State): string[] =>
+ sourcerer.configIndexPatterns;
+
+export const sourcererScopesSelector = ({ sourcerer }: State): SourcererScopeById =>
+ sourcerer.sourcererScopes;
+
+export const scopesSelector = () => createSelector(sourcererScopesSelector, (scopes) => scopes);
+
+export const kibanaIndexPatternsSelector = () =>
+ createSelector(
+ sourcererKibanaIndexPatternsSelector,
+ (kibanaIndexPatterns) => kibanaIndexPatterns
+ );
+
+export const signalIndexNameSelector = () =>
+ createSelector(sourcererSignalIndexNameSelector, (signalIndexName) => signalIndexName);
+
+export const configIndexPatternsSelector = () =>
+ createSelector(
+ sourcererConfigIndexPatternsSelector,
+ (configIndexPatterns) => configIndexPatterns
+ );
+
+export const getIndexNamesSelectedSelector = () => {
+ const getScopesSelector = scopesSelector();
+ const getConfigIndexPatternsSelector = configIndexPatternsSelector();
+
+ const mapStateToProps = (state: State, scopeId: SourcererScopeName): string[] => {
+ const scope = getScopesSelector(state)[scopeId];
+ const configIndexPatterns = getConfigIndexPatternsSelector(state);
+
+ return scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns;
+ };
+
+ return mapStateToProps;
+};
+
+export const getAllExistingIndexNamesSelector = () => {
+ const getSignalIndexNameSelector = signalIndexNameSelector();
+ const getConfigIndexPatternsSelector = configIndexPatternsSelector();
+
+ const mapStateToProps = (state: State): string[] => {
+ const signalIndexName = getSignalIndexNameSelector(state);
+ const configIndexPatterns = getConfigIndexPatternsSelector(state);
+
+ return signalIndexName != null
+ ? [...configIndexPatterns, signalIndexName]
+ : configIndexPatterns;
+ };
+
+ return mapStateToProps;
+};
+
+export const defaultIndexNamesSelector = () => {
+ const getScopesSelector = scopesSelector();
+ const getConfigIndexPatternsSelector = configIndexPatternsSelector();
+
+ const mapStateToProps = (state: State, scopeId: SourcererScopeName): string[] => {
+ const scope = getScopesSelector(state)[scopeId];
+ const configIndexPatterns = getConfigIndexPatternsSelector(state);
+
+ return scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns;
+ };
+
+ return mapStateToProps;
+};
+
+export const getSourcererScopeSelector = () => {
+ const getScopesSelector = scopesSelector();
+
+ const mapStateToProps = (state: State, scopeId: SourcererScopeName): ManageScope =>
+ getScopesSelector(state)[scopeId];
+
+ return mapStateToProps;
+};
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 91d92e4758c4a..6903567c752bc 100644
--- a/x-pack/plugins/security_solution/public/common/store/types.ts
+++ b/x-pack/plugins/security_solution/public/common/store/types.ts
@@ -12,6 +12,7 @@ import { AppAction } from './actions';
import { Immutable } from '../../../common/endpoint/types';
import { AppState } from './app/reducer';
import { InputsState } from './inputs/reducer';
+import { SourcererState } from './sourcerer/reducer';
import { HostsPluginState } from '../../hosts/store';
import { DragAndDropState } from './drag_and_drop/reducer';
import { TimelinePluginState } from '../../timelines/store/timeline';
@@ -25,6 +26,7 @@ export type StoreState = HostsPluginState &
app: AppState;
dragAndDrop: DragAndDropState;
inputs: InputsState;
+ sourcerer: SourcererState;
};
/**
* The redux `State` type for the Security App.
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
index 678aaf06e50e4..e3440f4158513 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx
@@ -197,6 +197,7 @@ describe('alert actions', () => {
highlightedDropAndProviderId: '',
historyIds: [],
id: '',
+ indexNames: [],
isFavorite: false,
isLive: false,
isLoading: false,
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
index 640726bb2e7c8..7f98d3b2f71de 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx
@@ -279,6 +279,7 @@ export const sendAlertToTimelineAction = async ({
...getThresholdAggregationDataProvider(ecsData, nonEcsData),
],
id: TimelineId.active,
+ indexNames: [],
dateRange: {
start: from,
end: to,
@@ -329,6 +330,7 @@ export const sendAlertToTimelineAction = async ({
},
],
id: TimelineId.active,
+ indexNames: [],
dateRange: {
start: from,
end: to,
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx
index be24957602037..6724d3a83d617 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx
@@ -22,7 +22,6 @@ describe('AlertsTableComponent', () => {
hasIndexWrite
from={'2020-07-07T08:20:18.966Z'}
loading
- signalsIndex="index"
to={'2020-07-08T08:20:18.966Z'}
globalQuery={{
query: 'query',
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
index 0416b3d2a459f..d66d37a020040 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx
@@ -13,7 +13,6 @@ import { Status } from '../../../../common/detection_engine/schemas/common/schem
import { Filter, esQuery } from '../../../../../../../src/plugins/data/public';
import { TimelineIdLiteral } from '../../../../common/types/timeline';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
-import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
import { HeaderSection } from '../../../common/components/header_section';
import { combineQueries } from '../../../timelines/components/timeline/helpers';
@@ -45,6 +44,8 @@ import {
displaySuccessToast,
displayErrorToast,
} from '../../../common/components/toasters';
+import { SourcererScopeName } from '../../../common/store/sourcerer/model';
+import { useSourcererScope } from '../../../common/containers/sourcerer';
interface OwnProps {
timelineId: TimelineIdLiteral;
@@ -55,7 +56,6 @@ interface OwnProps {
loading: boolean;
showBuildingBlockAlerts: boolean;
onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void;
- signalsIndex: string;
to: string;
}
@@ -80,19 +80,20 @@ export const AlertsTableComponent: React.FC = ({
setEventsLoading,
showBuildingBlockAlerts,
onShowBuildingBlockAlertsChanged,
- signalsIndex,
to,
}) => {
const [showClearSelectionAction, setShowClearSelectionAction] = useState(false);
const [filterGroup, setFilterGroup] = useState(FILTER_OPEN);
- const [{ browserFields, indexPatterns, isLoading: indexPatternsLoading }] = useFetchIndexPatterns(
- signalsIndex !== '' ? [signalsIndex] : [],
- 'alerts_table'
- );
+ const {
+ browserFields,
+ indexPattern: indexPatterns,
+ loading: indexPatternsLoading,
+ selectedPatterns,
+ } = useSourcererScope(SourcererScopeName.detections);
const kibana = useKibana();
const [, dispatchToaster] = useStateToaster();
const { addWarning } = useAppToasts();
- const { initializeTimeline, setSelectAll, setIndexToAdd } = useManageTimeline();
+ const { initializeTimeline, setSelectAll } = useManageTimeline();
const getGlobalQuery = useCallback(
(customFilters: Filter[]) => {
@@ -284,7 +285,6 @@ export const AlertsTableComponent: React.FC = ({
]
);
- const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]);
const defaultFiltersMemo = useMemo(() => {
if (isEmpty(defaultFilters)) {
return buildAlertStatusFilter(filterGroup);
@@ -301,7 +301,6 @@ export const AlertsTableComponent: React.FC = ({
filterManager,
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
id: timelineId,
- indexToAdd: defaultIndices,
loadingText: i18n.LOADING_ALERTS,
selectAll: false,
queryFields: requiredFieldsForActions,
@@ -310,16 +309,12 @@ export const AlertsTableComponent: React.FC = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- useEffect(() => {
- setIndexToAdd({ id: timelineId, indexToAdd: defaultIndices });
- }, [timelineId, defaultIndices, setIndexToAdd]);
-
const headerFilterGroup = useMemo(
() => ,
[onFilterGroupChangedCallback]
);
- if (loading || indexPatternsLoading || isEmpty(signalsIndex)) {
+ if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) {
return (
@@ -330,12 +325,12 @@ export const AlertsTableComponent: React.FC = ({
return (
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
index 4559e44b8c3c5..82fed152ea66d 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx
@@ -109,7 +109,7 @@ const AlertContextMenuComponent: React.FC = ({
const closeAddExceptionModal = useCallback(() => {
setShouldShowAddExceptionModal(false);
setAddExceptionModalState(addExceptionModalInitialState);
- }, [setShouldShowAddExceptionModal, setAddExceptionModalState]);
+ }, []);
const onAddExceptionCancel = useCallback(() => {
closeAddExceptionModal();
@@ -305,33 +305,6 @@ const AlertContextMenuComponent: React.FC = ({
[setShouldShowAddExceptionModal, setAddExceptionModalState]
);
- const AddExceptionModal = useCallback(
- () =>
- shouldShowAddExceptionModal === true && addExceptionModalState.alertData !== null ? (
-
- ) : null,
- [
- shouldShowAddExceptionModal,
- addExceptionModalState.alertData,
- addExceptionModalState.ruleName,
- addExceptionModalState.ruleId,
- addExceptionModalState.ruleIndices,
- addExceptionModalState.exceptionListType,
- onAddExceptionCancel,
- onAddExceptionConfirm,
- alertStatus,
- ]
- );
-
const button = (
= ({
-
+ {shouldShowAddExceptionModal === true && addExceptionModalState.alertData !== null && (
+
+ )}
>
);
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx
index 4ab5fa5e6012f..f4649b016f67c 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx
@@ -15,7 +15,6 @@ import { useApolloClient } from '../../../../common/utils/apollo_context';
import { sendAlertToTimelineAction } from '../actions';
import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers';
import { ActionIconItem } from '../../../../timelines/components/timeline/body/actions/action_icon_item';
-
import { CreateTimelineProps } from '../types';
import {
ACTION_INVESTIGATE_IN_TIMELINE,
@@ -49,6 +48,8 @@ const InvestigateInTimelineActionComponent: React.FC = (props) => (
-
+
);
DetectionEngineHeaderPageComponent.defaultProps = {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
index cb25785eaa5b2..4312be0b46990 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx
@@ -8,8 +8,9 @@ import { mount, shallow } from 'enzyme';
import { ThemeProvider } from 'styled-components';
import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json';
+import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub';
import { StepAboutRule } from '.';
-
+import { useFetchIndex } from '../../../../common/containers/source';
import { mockAboutStepRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock';
import { StepRuleDescription } from '../description_step';
import { stepAboutDefaultValue } from './default_value';
@@ -20,6 +21,7 @@ import {
} from '../../../pages/detection_engine/rules/types';
import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers';
+jest.mock('../../../../common/containers/source');
const theme = () => ({ eui: euiDarkVars, darkMode: true });
/* eslint-disable no-console */
@@ -44,6 +46,12 @@ describe('StepAboutRuleComponent', () => {
beforeEach(() => {
formHook = null;
+ (useFetchIndex as jest.Mock).mockImplementation(() => [
+ false,
+ {
+ indexPatterns: stubIndexPattern,
+ },
+ ]);
});
test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => {
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
index 66f95f5ce15d2..90b70e53a459e 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
@@ -39,8 +39,8 @@ import { NextStep } from '../next_step';
import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/eui_form';
import { SeverityField } from '../severity_mapping';
import { RiskScoreField } from '../risk_score_mapping';
-import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules';
import { AutocompleteField } from '../autocomplete_field';
+import { useFetchIndex } from '../../../../common/containers/source';
const CommonUseField = getUseField({ component: Field });
@@ -74,10 +74,8 @@ const StepAboutRuleComponent: FC = ({
}) => {
const initialState = defaultValues ?? stepAboutDefaultValue;
const [severityValue, setSeverityValue] = useState(initialState.severity.value);
- const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns(
- defineRuleData?.index ?? [],
- RuleStep.aboutRule
- );
+ const [indexPatternLoading, { indexPatterns }] = useFetchIndex(defineRuleData?.index ?? []);
+
const canUseExceptions =
defineRuleData?.ruleType &&
!isMlRule(defineRuleData.ruleType) &&
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
index 7846f0c406668..99999ddbf1976 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx
@@ -14,7 +14,6 @@ import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timelin
import { isMlRule } from '../../../../../common/machine_learning/helpers';
import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions';
import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license';
-import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
import { useUiSetting$ } from '../../../../common/lib/kibana';
import {
@@ -48,6 +47,7 @@ import { schema } from './schema';
import * as i18n from './translations';
import { isEqlRule, isThresholdRule } from '../../../../../common/detection_engine/utils';
import { EqlQueryBar } from '../eql_query_bar';
+import { useFetchIndex } from '../../../../common/containers/source';
const CommonUseField = getUseField({ component: Field });
@@ -125,10 +125,7 @@ const StepDefineRuleComponent: FC = ({
}) as unknown) as [Partial];
const index = formIndex || initialState.index;
const ruleType = formRuleType || initialState.ruleType;
- const [{ browserFields, indexPatterns, isLoading: indexPatternsLoading }] = useFetchIndexPatterns(
- index,
- RuleStep.defineRule
- );
+ const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(index);
// reset form when rule type changes
useEffect(() => {
diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx
index e1a29c3575d95..00e108ffb89b6 100644
--- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx
@@ -169,22 +169,19 @@ export const useUserInfo = (): State => {
if (loading !== privilegeLoading || indexNameLoading) {
dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loading, privilegeLoading, indexNameLoading]);
+ }, [dispatch, loading, privilegeLoading, indexNameLoading]);
useEffect(() => {
if (!loading && hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) {
dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loading, hasIndexManage, hasApiIndexManage]);
+ }, [dispatch, loading, hasIndexManage, hasApiIndexManage]);
useEffect(() => {
if (!loading && hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) {
dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loading, hasIndexWrite, hasApiIndexWrite]);
+ }, [dispatch, loading, hasIndexWrite, hasApiIndexWrite]);
useEffect(() => {
if (
@@ -194,36 +191,31 @@ export const useUserInfo = (): State => {
) {
dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loading, isSignalIndexExists, isApiSignalIndexExists]);
+ }, [dispatch, loading, isSignalIndexExists, isApiSignalIndexExists]);
useEffect(() => {
if (!loading && isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) {
dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loading, isAuthenticated, isApiAuthenticated]);
+ }, [dispatch, loading, isAuthenticated, isApiAuthenticated]);
useEffect(() => {
if (!loading && hasEncryptionKey !== isApiEncryptionKey && isApiEncryptionKey != null) {
dispatch({ type: 'updateHasEncryptionKey', hasEncryptionKey: isApiEncryptionKey });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loading, hasEncryptionKey, isApiEncryptionKey]);
+ }, [dispatch, loading, hasEncryptionKey, isApiEncryptionKey]);
useEffect(() => {
if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) {
dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loading, canUserCRUD, capabilitiesCanUserCRUD]);
+ }, [dispatch, loading, canUserCRUD, capabilitiesCanUserCRUD]);
useEffect(() => {
if (!loading && signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) {
dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName });
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [loading, signalIndexName, apiSignalIndexName]);
+ }, [dispatch, loading, signalIndexName, apiSignalIndexName]);
useEffect(() => {
if (
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx
deleted file mode 100644
index d36c19a6a35c6..0000000000000
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx
+++ /dev/null
@@ -1,475 +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 { renderHook, act } from '@testing-library/react-hooks';
-
-import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants';
-import { useApolloClient } from '../../../../common/utils/apollo_context';
-import { mocksSource } from '../../../../common/containers/source/mock';
-
-import { useFetchIndexPatterns, Return } from './fetch_index_patterns';
-
-const mockUseApolloClient = useApolloClient as jest.Mock;
-jest.mock('../../../../common/utils/apollo_context');
-
-describe('useFetchIndexPatterns', () => {
- beforeEach(() => {
- mockUseApolloClient.mockClear();
- });
- test('happy path', async () => {
- await act(async () => {
- mockUseApolloClient.mockImplementation(() => ({
- query: () => Promise.resolve(mocksSource[0].result),
- }));
- const { result, waitForNextUpdate } = renderHook(() =>
- useFetchIndexPatterns(DEFAULT_INDEX_PATTERN)
- );
- await waitForNextUpdate();
- await waitForNextUpdate();
-
- expect(result.current).toEqual([
- {
- browserFields: {
- base: {
- fields: {
- '@timestamp': {
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: '@timestamp',
- searchable: true,
- type: 'date',
- aggregatable: true,
- },
- },
- },
- agent: {
- fields: {
- 'agent.ephemeral_id': {
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'agent.ephemeral_id',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'agent.hostname': {
- category: 'agent',
- description: null,
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'agent.hostname',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'agent.id': {
- category: 'agent',
- description:
- 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.',
- example: '8a4f500d',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'agent.id',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'agent.name': {
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'agent.name',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- },
- },
- auditd: {
- fields: {
- 'auditd.data.a0': {
- category: 'auditd',
- description: null,
- example: null,
- format: '',
- indexes: ['auditbeat'],
- name: 'auditd.data.a0',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'auditd.data.a1': {
- category: 'auditd',
- description: null,
- example: null,
- format: '',
- indexes: ['auditbeat'],
- name: 'auditd.data.a1',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'auditd.data.a2': {
- category: 'auditd',
- description: null,
- example: null,
- format: '',
- indexes: ['auditbeat'],
- name: 'auditd.data.a2',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- },
- },
- client: {
- fields: {
- 'client.address': {
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'client.address',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'client.bytes': {
- category: 'client',
- description: 'Bytes sent from the client to the server.',
- example: '184',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'client.bytes',
- searchable: true,
- type: 'number',
- aggregatable: true,
- },
- 'client.domain': {
- category: 'client',
- description: 'Client domain.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'client.domain',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'client.geo.country_iso_code': {
- category: 'client',
- description: 'Country ISO code.',
- example: 'CA',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'client.geo.country_iso_code',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- },
- },
- cloud: {
- fields: {
- 'cloud.account.id': {
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'cloud.account.id',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'cloud.availability_zone': {
- category: 'cloud',
- description: 'Availability zone in which this host is running.',
- example: 'us-east-1c',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'cloud.availability_zone',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- },
- },
- container: {
- fields: {
- 'container.id': {
- category: 'container',
- description: 'Unique container id.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'container.id',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'container.image.name': {
- category: 'container',
- description: 'Name of the image the container was built on.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'container.image.name',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'container.image.tag': {
- category: 'container',
- description: 'Container image tag.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'container.image.tag',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- },
- },
- destination: {
- fields: {
- 'destination.address': {
- 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: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.address',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'destination.bytes': {
- category: 'destination',
- description: 'Bytes sent from the destination to the source.',
- example: '184',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.bytes',
- searchable: true,
- type: 'number',
- aggregatable: true,
- },
- 'destination.domain': {
- category: 'destination',
- description: 'Destination domain.',
- example: null,
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.domain',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- 'destination.ip': {
- aggregatable: true,
- category: 'destination',
- description:
- 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.',
- example: '',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.ip',
- searchable: true,
- type: 'ip',
- },
- 'destination.port': {
- aggregatable: true,
- category: 'destination',
- description: 'Port of the destination.',
- example: '',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'destination.port',
- searchable: true,
- type: 'long',
- },
- },
- },
- source: {
- fields: {
- 'source.ip': {
- aggregatable: true,
- category: 'source',
- description:
- 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.',
- example: '',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'source.ip',
- searchable: true,
- type: 'ip',
- },
- 'source.port': {
- aggregatable: true,
- category: 'source',
- description: 'Port of the source.',
- example: '',
- format: '',
- indexes: ['auditbeat', 'filebeat', 'packetbeat'],
- name: 'source.port',
- searchable: true,
- type: 'long',
- },
- },
- },
- event: {
- fields: {
- 'event.end': {
- 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: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- name: 'event.end',
- searchable: true,
- type: 'date',
- },
- },
- },
- },
- isLoading: false,
- indices: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- indicesExists: true,
- docValueFields: [
- {
- field: '@timestamp',
- format: 'date_time',
- },
- {
- field: 'event.end',
- format: 'date_time',
- },
- ],
- indexPatterns: {
- fields: [
- { name: '@timestamp', searchable: true, type: 'date', aggregatable: true },
- { name: 'agent.ephemeral_id', searchable: true, type: 'string', aggregatable: true },
- { name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true },
- { name: 'agent.id', searchable: true, type: 'string', aggregatable: true },
- { name: 'agent.name', searchable: true, type: 'string', aggregatable: true },
- { name: 'auditd.data.a0', searchable: true, type: 'string', aggregatable: true },
- { name: 'auditd.data.a1', searchable: true, type: 'string', aggregatable: true },
- { name: 'auditd.data.a2', searchable: true, type: 'string', aggregatable: true },
- { name: 'client.address', searchable: true, type: 'string', aggregatable: true },
- { name: 'client.bytes', searchable: true, type: 'number', aggregatable: true },
- { name: 'client.domain', searchable: true, type: 'string', aggregatable: true },
- {
- name: 'client.geo.country_iso_code',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- { name: 'cloud.account.id', searchable: true, type: 'string', aggregatable: true },
- {
- name: 'cloud.availability_zone',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- { name: 'container.id', searchable: true, type: 'string', aggregatable: true },
- {
- name: 'container.image.name',
- searchable: true,
- type: 'string',
- aggregatable: true,
- },
- { name: 'container.image.tag', searchable: true, type: 'string', aggregatable: true },
- { name: 'destination.address', searchable: true, type: 'string', aggregatable: true },
- { name: 'destination.bytes', searchable: true, type: 'number', aggregatable: true },
- { name: 'destination.domain', searchable: true, type: 'string', aggregatable: true },
- { name: 'destination.ip', searchable: true, type: 'ip', aggregatable: true },
- { name: 'destination.port', searchable: true, type: 'long', aggregatable: true },
- { name: 'source.ip', searchable: true, type: 'ip', aggregatable: true },
- { name: 'source.port', searchable: true, type: 'long', aggregatable: true },
- { name: 'event.end', searchable: true, type: 'date', aggregatable: true },
- ],
- title:
- 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*',
- },
- },
- result.current[1],
- ]);
- });
- });
-
- test('unhappy path', async () => {
- await act(async () => {
- mockUseApolloClient.mockImplementation(() => ({
- query: () => Promise.reject(new Error('Something went wrong')),
- }));
- const { result, waitForNextUpdate } = renderHook(() =>
- useFetchIndexPatterns(DEFAULT_INDEX_PATTERN)
- );
-
- await waitForNextUpdate();
- await waitForNextUpdate();
-
- expect(result.current).toEqual([
- {
- browserFields: {},
- docValueFields: [],
- indexPatterns: {
- fields: [],
- title: '',
- },
- indices: [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'logs-*',
- 'packetbeat-*',
- 'winlogbeat-*',
- ],
- indicesExists: false,
- isLoading: false,
- },
- result.current[1],
- ]);
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx
deleted file mode 100644
index 82c9292af7451..0000000000000
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx
+++ /dev/null
@@ -1,132 +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 { isEmpty, get } from 'lodash/fp';
-import { useEffect, useState, Dispatch, SetStateAction } from 'react';
-import deepEqual from 'fast-deep-equal';
-
-import { IIndexPattern } from '../../../../../../../../src/plugins/data/public';
-import {
- BrowserFields,
- getBrowserFields,
- getDocValueFields,
- getIndexFields,
- sourceQuery,
- DocValueFields,
-} from '../../../../common/containers/source';
-import { errorToToaster, useStateToaster } from '../../../../common/components/toasters';
-import { SourceQuery } from '../../../../graphql/types';
-import { useApolloClient } from '../../../../common/utils/apollo_context';
-
-import * as i18n from './translations';
-
-interface FetchIndexPatternReturn {
- browserFields: BrowserFields;
- docValueFields: DocValueFields[];
- isLoading: boolean;
- indices: string[];
- indicesExists: boolean;
- indexPatterns: IIndexPattern;
-}
-
-export type Return = [FetchIndexPatternReturn, Dispatch>];
-
-const DEFAULT_BROWSER_FIELDS = {};
-const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' };
-const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = [];
-
-// Fun fact: When using this hook multiple times within a component (e.g. add_exception_modal & edit_exception_modal),
-// the apolloClient will perform queryDeduplication and prevent the first query from executing. A deep compare is not
-// performed on `indices`, so another field must be passed to circumvent this.
-// For details, see https://github.com/apollographql/react-apollo/issues/2202
-export const useFetchIndexPatterns = (
- defaultIndices: string[] = [],
- queryDeduplication?: string
-): Return => {
- const apolloClient = useApolloClient();
- const [indices, setIndices] = useState(defaultIndices);
-
- const [state, setState] = useState({
- browserFields: DEFAULT_BROWSER_FIELDS,
- docValueFields: DEFAULT_DOC_VALUE_FIELDS,
- indices: defaultIndices,
- indicesExists: false,
- indexPatterns: DEFAULT_INDEX_PATTERNS,
- isLoading: false,
- });
-
- const [, dispatchToaster] = useStateToaster();
-
- useEffect(() => {
- if (!deepEqual(defaultIndices, indices)) {
- setIndices(defaultIndices);
- setState((prevState) => ({ ...prevState, indices: defaultIndices }));
- }
- }, [defaultIndices, indices]);
-
- useEffect(() => {
- let isSubscribed = true;
- const abortCtrl = new AbortController();
-
- async function fetchIndexPatterns() {
- if (apolloClient && !isEmpty(indices)) {
- setState((prevState) => ({ ...prevState, isLoading: true }));
- apolloClient
- .query({
- query: sourceQuery,
- fetchPolicy: 'cache-first',
- variables: {
- sourceId: 'default',
- defaultIndex: indices,
- ...(queryDeduplication != null ? { queryDeduplication } : {}),
- },
- context: {
- fetchOptions: {
- signal: abortCtrl.signal,
- },
- },
- })
- .then(
- (result) => {
- if (isSubscribed) {
- setState({
- browserFields: getBrowserFields(
- indices.join(),
- get('data.source.status.indexFields', result)
- ),
- docValueFields: getDocValueFields(
- indices.join(),
- get('data.source.status.indexFields', result)
- ),
- indices,
- isLoading: false,
- indicesExists: get('data.source.status.indicesExist', result),
- indexPatterns: getIndexFields(
- indices.join(),
- get('data.source.status.indexFields', result)
- ),
- });
- }
- },
- (error) => {
- if (isSubscribed) {
- setState((prevState) => ({ ...prevState, isLoading: false }));
- errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster });
- }
- }
- );
- }
- }
- fetchIndexPatterns();
- return () => {
- isSubscribed = false;
- abortCtrl.abort();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [indices]);
-
- return [state, setIndices];
-};
diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts
index a40ab2e487851..930391261ac87 100644
--- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts
+++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts
@@ -5,7 +5,6 @@
*/
export * from './api';
-export * from './fetch_index_patterns';
export * from './use_update_rule';
export * from './use_create_rule';
export * from './types';
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx
index 8c21f6a1e8cb7..a5d21d2847586 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx
@@ -20,7 +20,7 @@ import {
import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions';
import { DetectionEnginePageComponent } from './detection_engine';
import { useUserData } from '../../components/user_info';
-import { useWithSource } from '../../../common/containers/source';
+import { useSourcererScope } from '../../../common/containers/sourcerer';
import { createStore, State } from '../../../common/store';
import { mockHistory, Router } from '../../../cases/components/__mock__/router';
@@ -34,7 +34,7 @@ jest.mock('../../../common/components/query_bar', () => ({
}));
jest.mock('../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../components/user_info');
-jest.mock('../../../common/containers/source');
+jest.mock('../../../common/containers/sourcerer');
jest.mock('../../../common/components/link_to');
jest.mock('../../../common/containers/use_global_time', () => ({
useGlobalTime: jest.fn().mockReturnValue({
@@ -74,7 +74,7 @@ describe('DetectionEnginePageComponent', () => {
beforeAll(() => {
(useParams as jest.Mock).mockReturnValue({});
(useUserData as jest.Mock).mockReturnValue([{}]);
- (useWithSource as jest.Mock).mockReturnValue({
+ (useSourcererScope as jest.Mock).mockReturnValue({
indicesExist: true,
indexPattern: {},
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
index 3a3854f145db3..b39cd37521602 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
@@ -13,7 +13,6 @@ import { useHistory } from 'react-router-dom';
import { SecurityPageName } from '../../../app/types';
import { TimelineId } from '../../../../common/types/timeline';
import { useGlobalTime } from '../../../common/containers/use_global_time';
-import { useWithSource } from '../../../common/containers/source';
import { UpdateDateRange } from '../../../common/components/charts/common';
import { FiltersGlobal } from '../../../common/components/filters_global';
import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine';
@@ -46,6 +45,8 @@ import { timelineSelectors } from '../../../timelines/store/timeline';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { TimelineModel } from '../../../timelines/store/timeline/model';
import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config';
+import { useSourcererScope } from '../../../common/containers/sourcerer';
+import { SourcererScopeName } from '../../../common/store/sourcerer/model';
export const DetectionEnginePageComponent: React.FC = ({
filters,
@@ -117,10 +118,7 @@ export const DetectionEnginePageComponent: React.FC = ({
[setShowBuildingBlockAlerts]
);
- const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [
- signalIndexName,
- ]);
- const { indicesExist, indexPattern } = useWithSource('default', indexToAdd);
+ const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections);
if (isUserAuthenticated != null && !isUserAuthenticated && !loading) {
return (
@@ -202,7 +200,6 @@ export const DetectionEnginePageComponent: React.FC = ({
defaultFilters={alertsTableDefaultFilters}
showBuildingBlockAlerts={showBuildingBlockAlerts}
onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback}
- signalsIndex={signalIndexName ?? ''}
to={to}
/>
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx
index f8f9da78b2a06..22c3c43fb2356 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx
@@ -20,7 +20,7 @@ import { RuleDetailsPageComponent } from './index';
import { createStore, State } from '../../../../../common/store';
import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions';
import { useUserData } from '../../../../components/user_info';
-import { useWithSource } from '../../../../../common/containers/source';
+import { useSourcererScope } from '../../../../../common/containers/sourcerer';
import { useParams } from 'react-router-dom';
import { mockHistory, Router } from '../../../../../cases/components/__mock__/router';
@@ -35,7 +35,7 @@ jest.mock('../../../../../common/components/query_bar', () => ({
jest.mock('../../../../containers/detection_engine/lists/use_lists_config');
jest.mock('../../../../../common/components/link_to');
jest.mock('../../../../components/user_info');
-jest.mock('../../../../../common/containers/source');
+jest.mock('../../../../../common/containers/sourcerer');
jest.mock('../../../../../common/containers/use_global_time', () => ({
useGlobalTime: jest.fn().mockReturnValue({
from: '2020-07-07T08:20:18.966Z',
@@ -71,7 +71,7 @@ describe('RuleDetailsPageComponent', () => {
beforeAll(() => {
(useUserData as jest.Mock).mockReturnValue([{}]);
(useParams as jest.Mock).mockReturnValue({});
- (useWithSource as jest.Mock).mockReturnValue({
+ (useSourcererScope as jest.Mock).mockReturnValue({
indicesExist: true,
indexPattern: {},
});
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
index 68799f46eee57..4816358e06226 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx
@@ -36,10 +36,7 @@ import { SiemSearchBar } from '../../../../../common/components/search_bar';
import { WrapperPage } from '../../../../../common/components/wrapper_page';
import { Rule } from '../../../../containers/detection_engine/rules';
import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config';
-
-import { useWithSource } from '../../../../../common/containers/source';
import { SpyRoute } from '../../../../../common/utils/route/spy_routes';
-
import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details';
import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page';
import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel';
@@ -89,6 +86,8 @@ import { showGlobalFilters } from '../../../../../timelines/components/timeline/
import { timelineSelectors } from '../../../../../timelines/store/timeline';
import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults';
import { TimelineModel } from '../../../../../timelines/store/timeline/model';
+import { useSourcererScope } from '../../../../../common/containers/sourcerer';
+import { SourcererScopeName } from '../../../../../common/store/sourcerer/model';
enum RuleDetailTabs {
alerts = 'alerts',
@@ -265,10 +264,6 @@ export const RuleDetailsPageComponent: FC = ({
[rule, ruleDetailTab]
);
- const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [
- signalIndexName,
- ]);
-
const updateDateRangeCallback = useCallback(
({ x }) => {
if (!x) {
@@ -308,7 +303,7 @@ export const RuleDetailsPageComponent: FC = ({
[setShowBuildingBlockAlerts]
);
- const { indicesExist, indexPattern } = useWithSource('default', indexToAdd);
+ const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections);
const exceptionLists = useMemo((): {
lists: ExceptionIdentifiers[];
@@ -500,7 +495,6 @@ export const RuleDetailsPageComponent: FC = ({
loading={loading}
showBuildingBlockAlerts={showBuildingBlockAlerts}
onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback}
- signalsIndex={signalIndexName ?? ''}
to={to}
/>
)}
diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json
index 9e6a4f21ec64f..2f312c461ff8c 100644
--- a/x-pack/plugins/security_solution/public/graphql/introspection.json
+++ b/x-pack/plugins/security_solution/public/graphql/introspection.json
@@ -2088,104 +2088,6 @@
"isDeprecated": false,
"deprecationReason": null
},
- {
- "name": "OverviewNetwork",
- "description": "",
- "args": [
- {
- "name": "id",
- "description": "",
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "defaultValue": null
- },
- {
- "name": "timerange",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "filterQuery",
- "description": "",
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "defaultValue": null
- },
- {
- "name": "defaultIndex",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- }
- }
- },
- "defaultValue": null
- }
- ],
- "type": { "kind": "OBJECT", "name": "OverviewNetworkData", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "OverviewHost",
- "description": "",
- "args": [
- {
- "name": "id",
- "description": "",
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "defaultValue": null
- },
- {
- "name": "timerange",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "filterQuery",
- "description": "",
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "defaultValue": null
- },
- {
- "name": "defaultIndex",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- }
- }
- },
- "defaultValue": null
- }
- ],
- "type": { "kind": "OBJECT", "name": "OverviewHostData", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
{
"name": "whoAmI",
"description": "Just a simple example to get the app name",
@@ -2382,7 +2284,7 @@
"ofType": {
"kind": "NON_NULL",
"name": null,
- "ofType": { "kind": "OBJECT", "name": "IndexField", "ofType": null }
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
}
}
},
@@ -2405,153 +2307,6 @@
"enumValues": null,
"possibleTypes": null
},
- {
- "kind": "OBJECT",
- "name": "IndexField",
- "description": "A descriptor of a field in an index",
- "fields": [
- {
- "name": "category",
- "description": "Where the field belong",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "example",
- "description": "Example of field's value",
- "args": [],
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "indexes",
- "description": "whether the field's belong to an alias index",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "name",
- "description": "The name of the field",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "type",
- "description": "The type of the field's values as recognized by Kibana",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "searchable",
- "description": "Whether the field's values can be efficiently searched for",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "aggregatable",
- "description": "Whether the field's values can be aggregated",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "description",
- "description": "Description of the field",
- "args": [],
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "format",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "esTypes",
- "description": "the elastic type as mapped in the index",
- "args": [],
- "type": { "kind": "SCALAR", "name": "ToStringArrayNoNullable", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "subType",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "ToIFieldSubTypeNonNullable", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "SCALAR",
- "name": "ToStringArrayNoNullable",
- "description": "",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "SCALAR",
- "name": "ToIFieldSubTypeNonNullable",
- "description": "",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": null
- },
{
"kind": "INPUT_OBJECT",
"name": "TimerangeInput",
@@ -9050,256 +8805,18 @@
},
{
"kind": "OBJECT",
- "name": "OverviewNetworkData",
+ "name": "SayMyName",
"description": "",
"fields": [
{
- "name": "auditbeatSocket",
- "description": "",
+ "name": "appName",
+ "description": "The id of the source",
"args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "filebeatCisco",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "filebeatNetflow",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "filebeatPanw",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "filebeatSuricata",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "filebeatZeek",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "packetbeatDNS",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "packetbeatFlow",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "packetbeatTLS",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "inspect",
- "description": "",
- "args": [],
- "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "OverviewHostData",
- "description": "",
- "fields": [
- {
- "name": "auditbeatAuditd",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "auditbeatFIM",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "auditbeatLogin",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "auditbeatPackage",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "auditbeatProcess",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "auditbeatUser",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "endgameDns",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "endgameFile",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "endgameImageLoad",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "endgameNetwork",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "endgameProcess",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "endgameRegistry",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "endgameSecurity",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "filebeatSystemModule",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "winlogbeatSecurity",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "winlogbeatMWSysmonOperational",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "inspect",
- "description": "",
- "args": [],
- "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "SayMyName",
- "description": "",
- "fields": [
- {
- "name": "appName",
- "description": "The id of the source",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ },
"isDeprecated": false,
"deprecationReason": null
}
@@ -9466,6 +8983,22 @@
"isDeprecated": false,
"deprecationReason": null
},
+ {
+ "name": "indexNames",
+ "description": "",
+ "args": [],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
{
"name": "notes",
"description": "",
@@ -10933,6 +10466,20 @@
},
"defaultValue": null
},
+ {
+ "name": "indexNames",
+ "description": "",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ },
+ "defaultValue": null
+ },
{
"name": "title",
"description": "",
@@ -12214,6 +11761,16 @@
],
"possibleTypes": null
},
+ {
+ "kind": "SCALAR",
+ "name": "ToStringArrayNoNullable",
+ "description": "",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "EcsEdges",
@@ -12548,6 +12105,143 @@
],
"possibleTypes": null
},
+ {
+ "kind": "SCALAR",
+ "name": "ToIFieldSubTypeNonNullable",
+ "description": "",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "IndexField",
+ "description": "A descriptor of a field in an index",
+ "fields": [
+ {
+ "name": "category",
+ "description": "Where the field belong",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "example",
+ "description": "Example of field's value",
+ "args": [],
+ "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "indexes",
+ "description": "whether the field's belong to an alias index",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "The name of the field",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": "The type of the field's values as recognized by Kibana",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "searchable",
+ "description": "Whether the field's values can be efficiently searched for",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "aggregatable",
+ "description": "Whether the field's values can be aggregated",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the field",
+ "args": [],
+ "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "format",
+ "description": "",
+ "args": [],
+ "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "esTypes",
+ "description": "the elastic type as mapped in the index",
+ "args": [],
+ "type": { "kind": "SCALAR", "name": "ToStringArrayNoNullable", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "subType",
+ "description": "",
+ "args": [],
+ "type": { "kind": "SCALAR", "name": "ToIFieldSubTypeNonNullable", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "ENUM",
"name": "FlowDirection",
diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts
index 1699ac4dd33eb..bcb580a1a2988 100644
--- a/x-pack/plugins/security_solution/public/graphql/types.ts
+++ b/x-pack/plugins/security_solution/public/graphql/types.ts
@@ -132,6 +132,8 @@ export interface TimelineInput {
kqlQuery?: Maybe;
+ indexNames?: Maybe;
+
title?: Maybe;
templateTimelineId?: Maybe;
@@ -413,10 +415,6 @@ export enum FlowDirection {
biDirectional = 'biDirectional',
}
-export type ToStringArrayNoNullable = any;
-
-export type ToIFieldSubTypeNonNullable = any;
-
export type ToStringArray = string[];
export type Date = string;
@@ -431,6 +429,10 @@ export type ToAny = any;
export type EsValue = any;
+export type ToStringArrayNoNullable = any;
+
+export type ToIFieldSubTypeNonNullable = any;
+
// ====================================================
// Scalars
// ====================================================
@@ -554,10 +556,6 @@ export interface Source {
NetworkDnsHistogram: NetworkDsOverTimeData;
NetworkHttp: NetworkHttpData;
-
- OverviewNetwork?: Maybe;
-
- OverviewHost?: Maybe;
/** Just a simple example to get the app name */
whoAmI?: Maybe;
}
@@ -589,33 +587,7 @@ export interface SourceStatus {
/** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */
indicesExist: boolean;
/** The list of fields defined in the index mappings */
- indexFields: IndexField[];
-}
-
-/** A descriptor of a field in an index */
-export interface IndexField {
- /** Where the field belong */
- category: string;
- /** Example of field's value */
- example?: Maybe;
- /** whether the field's belong to an alias index */
- indexes: (Maybe)[];
- /** The name of the field */
- name: string;
- /** The type of the field's values as recognized by Kibana */
- type: string;
- /** Whether the field's values can be efficiently searched for */
- searchable: boolean;
- /** Whether the field's values can be aggregated */
- aggregatable: boolean;
- /** Description of the field */
- description?: Maybe;
-
- format?: Maybe;
- /** the elastic type as mapped in the index */
- esTypes?: Maybe;
-
- subType?: Maybe;
+ indexFields: string[];
}
export interface AuthenticationsData {
@@ -1856,64 +1828,6 @@ export interface NetworkHttpItem {
statuses: string[];
}
-export interface OverviewNetworkData {
- auditbeatSocket?: Maybe;
-
- filebeatCisco?: Maybe;
-
- filebeatNetflow?: Maybe;
-
- filebeatPanw?: Maybe;
-
- filebeatSuricata?: Maybe;
-
- filebeatZeek?: Maybe;
-
- packetbeatDNS?: Maybe;
-
- packetbeatFlow?: Maybe;
-
- packetbeatTLS?: Maybe;
-
- inspect?: Maybe;
-}
-
-export interface OverviewHostData {
- auditbeatAuditd?: Maybe;
-
- auditbeatFIM?: Maybe;
-
- auditbeatLogin?: Maybe;
-
- auditbeatPackage?: Maybe;
-
- auditbeatProcess?: Maybe;
-
- auditbeatUser?: Maybe;
-
- endgameDns?: Maybe;
-
- endgameFile?: Maybe;
-
- endgameImageLoad?: Maybe;
-
- endgameNetwork?: Maybe;
-
- endgameProcess?: Maybe;
-
- endgameRegistry?: Maybe;
-
- endgameSecurity?: Maybe;
-
- filebeatSystemModule?: Maybe;
-
- winlogbeatSecurity?: Maybe;
-
- winlogbeatMWSysmonOperational?: Maybe;
-
- inspect?: Maybe;
-}
-
export interface SayMyName {
/** The id of the source */
appName: string;
@@ -1946,6 +1860,8 @@ export interface TimelineResult {
kqlQuery?: Maybe;
+ indexNames?: Maybe;
+
notes?: Maybe;
noteIds?: Maybe;
@@ -2218,6 +2134,32 @@ export interface HostFields {
type?: Maybe;
}
+/** A descriptor of a field in an index */
+export interface IndexField {
+ /** Where the field belong */
+ category: string;
+ /** Example of field's value */
+ example?: Maybe;
+ /** whether the field's belong to an alias index */
+ indexes: (Maybe)[];
+ /** The name of the field */
+ name: string;
+ /** The type of the field's values as recognized by Kibana */
+ type: string;
+ /** Whether the field's values can be efficiently searched for */
+ searchable: boolean;
+ /** Whether the field's values can be aggregated */
+ aggregatable: boolean;
+ /** Description of the field */
+ description?: Maybe;
+
+ format?: Maybe;
+ /** the elastic type as mapped in the index */
+ esTypes?: Maybe;
+
+ subType?: Maybe;
+}
+
// ====================================================
// Arguments
// ====================================================
@@ -2483,24 +2425,6 @@ export interface NetworkHttpSourceArgs {
defaultIndex: string[];
}
-export interface OverviewNetworkSourceArgs {
- id?: Maybe;
-
- timerange: TimerangeInput;
-
- filterQuery?: Maybe;
-
- defaultIndex: string[];
-}
-export interface OverviewHostSourceArgs {
- id?: Maybe;
-
- timerange: TimerangeInput;
-
- filterQuery?: Maybe;
-
- defaultIndex: string[];
-}
export interface IndicesExistSourceStatusArgs {
defaultIndex: string[];
}
@@ -2637,61 +2561,6 @@ export namespace GetMatrixHistogramQuery {
};
}
-export namespace SourceQuery {
- export type Variables = {
- sourceId?: Maybe;
- defaultIndex: string[];
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'Source';
-
- id: string;
-
- status: Status;
- };
-
- export type Status = {
- __typename?: 'SourceStatus';
-
- indicesExist: boolean;
-
- indexFields: IndexFields[];
- };
-
- export type IndexFields = {
- __typename?: 'IndexField';
-
- category: string;
-
- description: Maybe;
-
- example: Maybe;
-
- indexes: (Maybe)[];
-
- name: string;
-
- searchable: boolean;
-
- type: string;
-
- aggregatable: boolean;
-
- format: Maybe;
-
- esTypes: Maybe;
-
- subType: Maybe;
- };
-}
-
export namespace GetAuthenticationsQuery {
export type Variables = {
sourceId: string;
@@ -4008,132 +3877,6 @@ export namespace GetUsersQuery {
};
}
-export namespace GetOverviewHostQuery {
- export type Variables = {
- sourceId: string;
- timerange: TimerangeInput;
- filterQuery?: Maybe;
- defaultIndex: string[];
- inspect: boolean;
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'Source';
-
- id: string;
-
- OverviewHost: Maybe;
- };
-
- export type OverviewHost = {
- __typename?: 'OverviewHostData';
-
- auditbeatAuditd: Maybe;
-
- auditbeatFIM: Maybe;
-
- auditbeatLogin: Maybe;
-
- auditbeatPackage: Maybe;
-
- auditbeatProcess: Maybe;
-
- auditbeatUser: Maybe;
-
- endgameDns: Maybe;
-
- endgameFile: Maybe;
-
- endgameImageLoad: Maybe;
-
- endgameNetwork: Maybe;
-
- endgameProcess: Maybe;
-
- endgameRegistry: Maybe;
-
- endgameSecurity: Maybe;
-
- filebeatSystemModule: Maybe;
-
- winlogbeatSecurity: Maybe;
-
- winlogbeatMWSysmonOperational: Maybe;
-
- inspect: Maybe;
- };
-
- export type Inspect = {
- __typename?: 'Inspect';
-
- dsl: string[];
-
- response: string[];
- };
-}
-
-export namespace GetOverviewNetworkQuery {
- export type Variables = {
- sourceId: string;
- timerange: TimerangeInput;
- filterQuery?: Maybe;
- defaultIndex: string[];
- inspect: boolean;
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'Source';
-
- id: string;
-
- OverviewNetwork: Maybe;
- };
-
- export type OverviewNetwork = {
- __typename?: 'OverviewNetworkData';
-
- auditbeatSocket: Maybe;
-
- filebeatCisco: Maybe;
-
- filebeatNetflow: Maybe;
-
- filebeatPanw: Maybe;
-
- filebeatSuricata: Maybe;
-
- filebeatZeek: Maybe;
-
- packetbeatDNS: Maybe;
-
- packetbeatFlow: Maybe;
-
- packetbeatTLS: Maybe;
-
- inspect: Maybe;
- };
-
- export type Inspect = {
- __typename?: 'Inspect';
-
- dsl: string[];
-
- response: string[];
- };
-}
-
export namespace GetAllTimeline {
export type Variables = {
pageInfo: PageInfoTimeline;
@@ -5269,6 +5012,8 @@ export namespace GetOneTimeline {
kqlQuery: Maybe;
+ indexNames: Maybe;
+
notes: Maybe;
noteIds: Maybe;
@@ -5601,6 +5346,8 @@ export namespace PersistTimelineMutation {
kqlQuery: Maybe;
+ indexNames: Maybe;
+
title: Maybe;
dateRange: Maybe;
diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx
index 606b43c6508fb..4f64cca45d162 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx
@@ -40,7 +40,12 @@ describe('FirstLastSeen Component', () => {
useFirstLastSeenHostMock.mockReturnValue([true, MOCKED_RESPONSE]);
const { container } = render(
-
+
);
expect(container.innerHTML).toBe(
@@ -52,7 +57,12 @@ describe('FirstLastSeen Component', () => {
useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]);
const { container } = render(
-
+
);
@@ -69,7 +79,12 @@ describe('FirstLastSeen Component', () => {
useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]);
const { container } = render(
-
+
);
await act(() =>
@@ -91,7 +106,12 @@ describe('FirstLastSeen Component', () => {
]);
const { container } = render(
-
+
);
@@ -114,7 +134,12 @@ describe('FirstLastSeen Component', () => {
]);
const { container } = render(
-
+
);
@@ -137,7 +162,12 @@ describe('FirstLastSeen Component', () => {
]);
const { container } = render(
-
+
);
await act(() =>
@@ -157,7 +187,12 @@ describe('FirstLastSeen Component', () => {
]);
const { container } = render(
-
+
);
await act(() =>
diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx
index a1b72fb39069c..ee415560cf9de 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx
@@ -10,6 +10,7 @@ import React, { useMemo } from 'react';
import { useFirstLastSeenHost } from '../../containers/hosts/first_last_seen';
import { getEmptyTagValue } from '../../../common/components/empty_value';
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
+import { DocValueFields } from '../../../../common/search_strategy';
export enum FirstLastSeenHostType {
FIRST_SEEN = 'first-seen',
@@ -17,47 +18,53 @@ export enum FirstLastSeenHostType {
}
interface FirstLastSeenHostProps {
+ docValueFields: DocValueFields[];
hostName: string;
+ indexNames: string[];
type: FirstLastSeenHostType;
}
-export const FirstLastSeenHost = React.memo(({ hostName, type }) => {
- const [loading, { firstSeen, lastSeen, errorMessage }] = useFirstLastSeenHost({
- hostName,
- });
- const valueSeen = useMemo(
- () => (type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen),
- [firstSeen, lastSeen, type]
- );
+export const FirstLastSeenHost = React.memo(
+ ({ docValueFields, hostName, type, indexNames }) => {
+ const [loading, { firstSeen, lastSeen, errorMessage }] = useFirstLastSeenHost({
+ docValueFields,
+ hostName,
+ indexNames,
+ });
+ const valueSeen = useMemo(
+ () => (type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen),
+ [firstSeen, lastSeen, type]
+ );
+
+ if (errorMessage != null) {
+ return (
+
+
+
+ );
+ }
- if (errorMessage != null) {
return (
-
-
-
+ <>
+ {loading && }
+ {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date'
+ ? valueSeen
+ : !loading &&
+ valueSeen != null && (
+
+
+
+ )}
+ {!loading && valueSeen == null && getEmptyTagValue()}
+ >
);
}
-
- return (
- <>
- {loading && }
- {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date'
- ? valueSeen
- : !loading &&
- valueSeen != null && (
-
-
-
- )}
- {!loading && valueSeen == null && getEmptyTagValue()}
- >
- );
-});
+);
FirstLastSeenHost.displayName = 'FirstLastSeenHost';
diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap
index 3143e680913b2..1d70f4f72ac8b 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap
@@ -68,97 +68,6 @@ exports[`Hosts Table rendering it renders the default Hosts table 1`] = `
}
fakeTotalCount={50}
id="hostsQuery"
- 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-*",
- }
- }
isInspect={false}
loadPage={[MockFunction]}
loading={false}
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 c4a391687843c..29e4dc48ae3c7 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
@@ -12,7 +12,6 @@ import { MockedProvider } from 'react-apollo/test-utils';
import '../../../common/mock/match_media';
import {
apolloClientObservable,
- mockIndexPattern,
mockGlobalState,
TestProviders,
SUB_PLUGINS_REDUCER,
@@ -69,7 +68,6 @@ describe('Hosts Table', () => {
data={mockData.Hosts.edges}
id="hostsQuery"
isInspect={false}
- indexPattern={mockIndexPattern}
fakeTotalCount={getOr(50, 'fakeTotalCount', mockData.Hosts.pageInfo)}
loading={false}
loadPage={loadPage}
@@ -92,7 +90,6 @@ describe('Hosts Table', () => {
void;
@@ -77,7 +75,6 @@ const HostsTableComponent = React.memo(
direction,
fakeTotalCount,
id,
- indexPattern,
isInspect,
limit,
loading,
diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx
index 0949616827470..84003e5dea5e9 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx
@@ -42,6 +42,7 @@ export const fieldsMapping: Readonly = [
const HostsKpiAuthenticationsComponent: React.FC = ({
filterQuery,
from,
+ indexNames,
to,
narrowDateRange,
setQuery,
@@ -50,6 +51,7 @@ const HostsKpiAuthenticationsComponent: React.FC = ({
const [loading, { refetch, id, inspect, ...data }] = useHostsKpiAuthentications({
filterQuery,
endDate: to,
+ indexNames,
startDate: from,
skip,
});
diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx
index b1c4d6331e450..908ff717e2711 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx
@@ -31,6 +31,7 @@ export const fieldsMapping: Readonly = [
const HostsKpiHostsComponent: React.FC = ({
filterQuery,
from,
+ indexNames,
to,
narrowDateRange,
setQuery,
@@ -39,6 +40,7 @@ const HostsKpiHostsComponent: React.FC = ({
const [loading, { refetch, id, inspect, ...data }] = useHostsKpiHosts({
filterQuery,
endDate: to,
+ indexNames,
startDate: from,
skip,
});
diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx
index fff4c64900a8b..6174e174db5a6 100644
--- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx
@@ -13,12 +13,13 @@ import { HostsKpiUniqueIps } from './unique_ips';
import { HostsKpiProps } from './types';
export const HostsKpiComponent = React.memo(
- ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => (
+ ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => (
(
(
(
HostsKpiComponent.displayName = 'HostsKpiComponent';
export const HostsDetailsKpiComponent = React.memo(
- ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => (
+ ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => (
(
= [
const HostsKpiUniqueIpsComponent: React.FC = ({
filterQuery,
from,
+ indexNames,
to,
narrowDateRange,
setQuery,
@@ -50,6 +51,7 @@ const HostsKpiUniqueIpsComponent: React.FC = ({
const [loading, { refetch, id, inspect, ...data }] = useHostsKpiUniqueIps({
filterQuery,
endDate: to,
+ indexNames,
startDate: from,
skip,
});
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx
index 7bf4f7a833fb8..b1563e85c93dd 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx
@@ -15,7 +15,6 @@ import {
isErrorResponse,
} from '../../../../../../../src/plugins/data/common';
-import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
import { HostsQueries } from '../../../../common/search_strategy/security_solution';
import {
HostAuthenticationsRequestOptions,
@@ -56,6 +55,7 @@ interface UseAuthentications {
docValueFields?: DocValueFields[];
filterQuery?: ESTermQuery | string;
endDate: string;
+ indexNames: string[];
startDate: string;
type: hostsModel.HostsType;
skip: boolean;
@@ -65,6 +65,7 @@ export const useAuthentications = ({
docValueFields,
filterQuery,
endDate,
+ indexNames,
startDate,
type,
skip,
@@ -74,15 +75,14 @@ export const useAuthentications = ({
(state: State) => getAuthenticationsSelector(state, type),
shallowEqual
);
- const { data, notifications, uiSettings } = useKibana().services;
+ const { data, notifications } = useKibana().services;
const refetch = useRef(noop);
const abortCtrl = useRef(new AbortController());
- const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY);
const [loading, setLoading] = useState(false);
const [authenticationsRequest, setAuthenticationsRequest] = useState<
HostAuthenticationsRequestOptions
>({
- defaultIndex,
+ defaultIndex: indexNames,
docValueFields: docValueFields ?? [],
factoryQueryType: HostsQueries.authentications,
filterQuery: createFilter(filterQuery),
@@ -186,7 +186,7 @@ export const useAuthentications = ({
setAuthenticationsRequest((prevRequest) => {
const myRequest = {
...prevRequest,
- defaultIndex,
+ defaultIndex: indexNames,
docValueFields: docValueFields ?? [],
filterQuery: createFilter(filterQuery),
pagination: generateTablePaginationOptions(activePage, limit),
@@ -201,7 +201,7 @@ export const useAuthentications = ({
}
return prevRequest;
});
- }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, skip, startDate]);
+ }, [activePage, docValueFields, endDate, filterQuery, indexNames, limit, skip, startDate]);
useEffect(() => {
authenticationsSearch(authenticationsRequest);
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx
index f68c340a47723..5b69e20398a35 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx
@@ -10,7 +10,6 @@ import deepEqual from 'fast-deep-equal';
import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
-import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
import { inputsModel } from '../../../../common/store';
import { useKibana } from '../../../../common/lib/kibana';
import {
@@ -41,9 +40,10 @@ export interface HostDetailsArgs {
}
interface UseHostDetails {
- id?: string;
- hostName: string;
endDate: string;
+ hostName: string;
+ id?: string;
+ indexNames: string[];
skip?: boolean;
startDate: string;
}
@@ -51,17 +51,17 @@ interface UseHostDetails {
export const useHostDetails = ({
endDate,
hostName,
+ indexNames,
+ id = ID,
skip = false,
startDate,
- id = ID,
}: UseHostDetails): [boolean, HostDetailsArgs] => {
- const { data, notifications, uiSettings } = useKibana().services;
+ const { data, notifications } = useKibana().services;
const refetch = useRef(noop);
const abortCtrl = useRef(new AbortController());
- const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY);
const [loading, setLoading] = useState(false);
const [hostDetailsRequest, setHostDetailsRequest] = useState({
- defaultIndex,
+ defaultIndex: indexNames,
hostName,
factoryQueryType: HostsQueries.details,
timerange: {
@@ -142,7 +142,7 @@ export const useHostDetails = ({
setHostDetailsRequest((prevRequest) => {
const myRequest = {
...prevRequest,
- defaultIndex,
+ defaultIndex: indexNames,
hostName,
timerange: {
interval: '12h',
@@ -155,7 +155,7 @@ export const useHostDetails = ({
}
return prevRequest;
});
- }, [defaultIndex, endDate, hostName, startDate, skip]);
+ }, [endDate, hostName, indexNames, startDate, skip]);
useEffect(() => {
hostDetailsSearch(hostDetailsRequest);
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx
index 12a82c7980b61..0236270d18618 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx
@@ -10,11 +10,9 @@ import { Query } from 'react-apollo';
import { connect } from 'react-redux';
import { compose } from 'redux';
-import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
import { inputsModel, inputsSelectors, State } from '../../../../common/store';
import { getDefaultFetchPolicy } from '../../../../common/containers/helpers';
import { QueryTemplate, QueryTemplateProps } from '../../../../common/containers/query_template';
-import { withKibana, WithKibanaProps } from '../../../../common/lib/kibana';
import { HostOverviewQuery } from './host_overview.gql_query';
import { GetHostOverviewQuery, HostItem } from '../../../../graphql/types';
@@ -42,7 +40,7 @@ export interface OwnProps extends QueryTemplateProps {
endDate: string;
}
-type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps;
+type HostsOverViewProps = OwnProps & HostOverviewReduxProps;
class HostOverviewByNameComponentQuery extends QueryTemplate<
HostsOverViewProps,
@@ -52,10 +50,10 @@ class HostOverviewByNameComponentQuery extends QueryTemplate<
public render() {
const {
id = ID,
+ indexNames,
isInspected,
children,
hostName,
- kibana,
skip,
sourceId,
startDate,
@@ -75,7 +73,7 @@ class HostOverviewByNameComponentQuery extends QueryTemplate<
from: startDate,
to: endDate,
},
- defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY),
+ defaultIndex: indexNames,
inspect: isInspected,
}}
>
@@ -108,6 +106,5 @@ const makeMapStateToProps = () => {
};
export const HostOverviewByNameQuery = compose>(
- connect(makeMapStateToProps),
- withKibana
+ connect(makeMapStateToProps)
)(HostOverviewByNameComponentQuery);
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx
index a6376642dfa29..cc944a59571f1 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx
@@ -7,17 +7,15 @@
import deepEqual from 'fast-deep-equal';
import { useCallback, useEffect, useRef, useState } from 'react';
-import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
-
import { useKibana } from '../../../../common/lib/kibana';
import {
HostsQueries,
HostFirstLastSeenRequestOptions,
HostFirstLastSeenStrategyResponse,
} from '../../../../../common/search_strategy/security_solution';
-import { useWithSource } from '../../../../common/containers/source';
import * as i18n from './translations';
+import { DocValueFields } from '../../../../../common/search_strategy';
import {
AbortError,
isCompleteResponse,
@@ -33,21 +31,23 @@ export interface FirstLastSeenHostArgs {
lastSeen?: string | null;
}
interface UseHostFirstLastSeen {
+ docValueFields: DocValueFields[];
hostName: string;
+ indexNames: string[];
}
export const useFirstLastSeenHost = ({
+ docValueFields,
hostName,
+ indexNames,
}: UseHostFirstLastSeen): [boolean, FirstLastSeenHostArgs] => {
- const { docValueFields } = useWithSource('default');
- const { data, notifications, uiSettings } = useKibana().services;
+ const { data, notifications } = useKibana().services;
const abortCtrl = useRef(new AbortController());
- const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY);
const [loading, setLoading] = useState(false);
const [firstLastSeenHostRequest, setFirstLastSeenHostRequest] = useState<
HostFirstLastSeenRequestOptions
>({
- defaultIndex,
+ defaultIndex: indexNames,
docValueFields: docValueFields ?? [],
factoryQueryType: HostsQueries.firstLastSeen,
hostName,
@@ -124,7 +124,7 @@ export const useFirstLastSeenHost = ({
setFirstLastSeenHostRequest((prevRequest) => {
const myRequest = {
...prevRequest,
- defaultIndex,
+ defaultIndex: indexNames,
docValueFields: docValueFields ?? [],
hostName,
};
@@ -133,7 +133,7 @@ export const useFirstLastSeenHost = ({
}
return prevRequest;
});
- }, [defaultIndex, docValueFields, hostName]);
+ }, [indexNames, docValueFields, hostName]);
useEffect(() => {
firstLastSeenHostSearch(firstLastSeenHostRequest);
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx
index 2eb926a9733c3..6ca0272e58d7d 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx
@@ -9,7 +9,6 @@ import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
-import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
import { inputsModel, State } from '../../../common/store';
import { createFilter } from '../../../common/containers/helpers';
import { useKibana } from '../../../common/lib/kibana';
@@ -54,6 +53,7 @@ interface UseAllHost {
docValueFields?: DocValueFields[];
filterQuery?: ESTermQuery | string;
endDate: string;
+ indexNames: string[];
skip?: boolean;
startDate: string;
type: hostsModel.HostsType;
@@ -63,6 +63,7 @@ export const useAllHost = ({
docValueFields,
filterQuery,
endDate,
+ indexNames,
skip = false,
startDate,
type,
@@ -71,13 +72,12 @@ export const useAllHost = ({
const { activePage, direction, limit, sortField } = useSelector((state: State) =>
getHostsSelector(state, type)
);
- const { data, notifications, uiSettings } = useKibana().services;
+ const { data, notifications } = useKibana().services;
const refetch = useRef(noop);
const abortCtrl = useRef(new AbortController());
- const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY);
const [loading, setLoading] = useState(false);
const [hostsRequest, setHostRequest] = useState({
- defaultIndex,
+ defaultIndex: indexNames,
docValueFields: docValueFields ?? [],
factoryQueryType: HostsQueries.hosts,
filterQuery: createFilter(filterQuery),
@@ -181,7 +181,7 @@ export const useAllHost = ({
setHostRequest((prevRequest) => {
const myRequest = {
...prevRequest,
- defaultIndex,
+ defaultIndex: indexNames,
docValueFields: docValueFields ?? [],
filterQuery: createFilter(filterQuery),
pagination: generateTablePaginationOptions(activePage, limit),
@@ -202,11 +202,11 @@ export const useAllHost = ({
});
}, [
activePage,
- defaultIndex,
direction,
docValueFields,
endDate,
filterQuery,
+ indexNames,
limit,
skip,
startDate,
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_host_details/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_host_details/index.tsx
index 1551e7d706714..26e4eaf9ea82e 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_host_details/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_host_details/index.tsx
@@ -9,10 +9,8 @@ import React from 'react';
import { Query } from 'react-apollo';
import { connect, ConnectedProps } from 'react-redux';
-import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../../graphql/types';
import { inputsModel, inputsSelectors, State } from '../../../common/store';
-import { useUiSetting } from '../../../common/lib/kibana';
import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers';
import { QueryTemplateProps } from '../../../common/containers/query_template';
@@ -33,7 +31,17 @@ export interface QueryKpiHostDetailsProps extends QueryTemplateProps {
}
const KpiHostDetailsComponentQuery = React.memo(
- ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => (
+ ({
+ id = ID,
+ children,
+ endDate,
+ filterQuery,
+ indexNames,
+ isInspected,
+ skip,
+ sourceId,
+ startDate,
+ }) => (
query={kpiHostDetailsQuery}
fetchPolicy={getDefaultFetchPolicy()}
@@ -47,7 +55,7 @@ const KpiHostDetailsComponentQuery = React.memo(DEFAULT_INDEX_KEY),
+ defaultIndex: indexNames ?? [],
inspect: isInspected,
}}
>
diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx
index 0d90b73e0a584..404231be1e6cd 100644
--- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx
+++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx
@@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal';
import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
-import { DEFAULT_INDEX_KEY } from '../../../../../common/constants';
import { inputsModel } from '../../../../common/store';
import { createFilter } from '../../../../common/containers/helpers';
import { useKibana } from '../../../../common/lib/kibana';
@@ -37,6 +36,7 @@ export interface HostsKpiAuthenticationsArgs
interface UseHostsKpiAuthentications {
filterQuery?: ESTermQuery | string;
endDate: string;
+ indexNames: string[];
skip?: boolean;
startDate: string;
}
@@ -44,18 +44,18 @@ interface UseHostsKpiAuthentications {
export const useHostsKpiAuthentications = ({
filterQuery,
endDate,
+ indexNames,
skip = false,
startDate,
}: UseHostsKpiAuthentications): [boolean, HostsKpiAuthenticationsArgs] => {
- const { data, notifications, uiSettings } = useKibana().services;
+ const { data, notifications } = useKibana().services;
const refetch = useRef(noop);
const abortCtrl = useRef(new AbortController());
- const defaultIndex = uiSettings.get