| Property | Type | Description |
| --- | --- | --- |
| [config](./kibana-plugin-server.plugininitializercontext.config.md) | {
create: <T = ConfigSchema>() => Observable<T>;
createIfExists: <T = ConfigSchema>() => Observable<T | undefined>;
}
| |
-| [env](./kibana-plugin-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
}
| |
+| [env](./kibana-plugin-server.plugininitializercontext.env.md) | {
mode: EnvironmentMode;
packageInfo: Readonly<PackageInfo>;
}
| |
| [logger](./kibana-plugin-server.plugininitializercontext.logger.md) | LoggerFactory
| |
| [opaqueId](./kibana-plugin-server.plugininitializercontext.opaqueid.md) | PluginOpaqueId
| |
diff --git a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md
index cffc4389becdf..e1ec743ae71cc 100644
--- a/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md
+++ b/docs/development/core/server/kibana-plugin-server.routeconfig.validate.md
@@ -14,7 +14,7 @@ validate: RouteSchemas | false;
## Remarks
-You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `false`.
+You \*must\* specify a validation schema to be able to read: - url path segments - request query - request body To opt out of validating the request, specify `validate: false`. In this case request params, query, and body will be \*\*empty\*\* objects and have no access to raw values. In some cases you may want to use another validation library. To do this, you need to instruct the `@kbn/config-schema` library to output \*\*non-validated values\*\* with setting schema as `schema.object({}, { allowUnknowns: true })`;
## Example
@@ -22,7 +22,7 @@ You \*must\* specify a validation schema to be able to read: - url path segments
```ts
import { schema } from '@kbn/config-schema';
router.get({
- path: 'path/{id}'
+ path: 'path/{id}',
validate: {
params: schema.object({
id: schema.string(),
@@ -30,7 +30,33 @@ You \*must\* specify a validation schema to be able to read: - url path segments
query: schema.object({...}),
body: schema.object({...}),
},
- })
+},
+(context, req, res,) {
+ req.params; // type Readonly<{id: string}>
+ console.log(req.params.id); // value
+});
+
+router.get({
+ path: 'path/{id}',
+ validate: false, // handler has no access to params, query, body values.
+},
+(context, req, res,) {
+ req.params; // type Readonly<{}>;
+ console.log(req.params.id); // undefined
+});
+
+router.get({
+ path: 'path/{id}',
+ validate: {
+ // handler has access to raw non-validated params in runtime
+ params: schema.object({}, { allowUnknowns: true })
+ },
+},
+(context, req, res,) {
+ req.params; // type Readonly<{}>;
+ console.log(req.params.id); // value
+ myValidationLibrary.validate({ params: req.params });
+});
```
diff --git a/docs/infrastructure/infra-ui.asciidoc b/docs/infrastructure/infra-ui.asciidoc
index e92f6b3f64205..5c8c50a978d63 100644
--- a/docs/infrastructure/infra-ui.asciidoc
+++ b/docs/infrastructure/infra-ui.asciidoc
@@ -62,29 +62,30 @@ Select *Auto-refresh* to keep up-to-date metrics information coming in, or *Stop
[[infra-configure-source]]
=== Configure the data to use for your metrics
+If your metrics have custom index patterns, or use non-default field settings, you can override the default configuration settings.
+
The default source configuration for metrics is specified in the {kibana-ref}/infrastructure-ui-settings-kb.html[Metrics app settings] in the {kibana-ref}/settings.html[Kibana configuration file].
The default configuration uses the `metricbeat-*` index pattern to query the data.
The default configuration also defines field settings for things like timestamps and container names.
-If your metrics have custom index patterns, or use non-default field settings, you can override the default settings.
-Click *Configuration* to change the settings.
-This opens the *Configure source* fly-out dialog.
+To change the configuration settings, click the *Settings* tab.
NOTE: These settings are shared with logs. Changes you make here may also affect the settings used by the *Logs* app.
-In the *Configure source* dialog, you can change the following values:
+In the *Settings* tab, you can change the values in these sections:
* *Name*: the name of the source configuration
-* *Indices*: the index pattern or patterns in the Elasticsearch indices to read metrics data and log data
- from
+* *Indices*: the index pattern or patterns in the Elasticsearch indices to read metrics data and log data from
* *Fields*: the names of specific fields in the indices that are used to query and interpret the data correctly
-TIP: If <> are enabled in your Kibana instance, any configuration changes you make here are specific to the current space.
-You can make different subsets of data available by creating multiple spaces with different data source configurations.
+When you have completed your changes, click *Apply*.
-TIP: If you don't see the *Configuration* option, you may not have sufficient privileges to change the source configuration.
+If the fields are greyed out and cannot be edited, you may not have sufficient privileges to change the source configuration.
For more information see <>.
+TIP: If <> are enabled in your Kibana instance, any configuration changes you make here are specific to the current space.
+You can make different subsets of data available by creating multiple spaces with different data source configurations.
+
[float]
[[infra-metrics-explorer]]
=== Visualize multiple metrics in Metrics Explorer
diff --git a/docs/logs/configuring.asciidoc b/docs/logs/configuring.asciidoc
index f2aa1e5e17b8f..6b54721f92e89 100644
--- a/docs/logs/configuring.asciidoc
+++ b/docs/logs/configuring.asciidoc
@@ -1,50 +1,46 @@
[role="xpack"]
[[xpack-logs-configuring]]
-:ecs-link: {ecs-ref}[Elastic Common Schema (ECS)]
+:ecs-base-link: {ecs-ref}/ecs-base.html[base]
== Configuring the Logs data
The default source configuration for logs is specified in the {kibana-ref}/logs-ui-settings-kb.html[Logs app settings] in the {kibana-ref}/settings.html[Kibana configuration file].
The default configuration uses the `filebeat-*` index pattern to query the data.
-The default configuration also defines field settings for things like timestamps and container names, and the default columns to show in the logs pane.
+The default configuration also defines field settings for things like timestamps and container names, and the default columns to show in the logs stream.
-If your logs have custom index patterns, or use non-default field settings, or contain parsed fields which you want to expose as individual columns, you can override the default settings.
-Click *Configuration* to change the settings.
-This opens the *Configure source* fly-out dialog.
+If your logs have custom index patterns, use non-default field settings, or contain parsed fields which you want to expose as individual columns, you can override the default configuration settings.
-NOTE: These settings are shared with metrics. Changes you make here may also affect the settings used by the *Metrics* app.
-
-TIP: If <> are enabled in your Kibana instance, any configuration changes you make here are specific to the current space.
-You can make different subsets of data available by creating multiple spaces with different data source configurations.
+To change the configuration settings, click the *Settings* tab.
-TIP: If you don't see the *Configuration* option, you may not have sufficient privileges to change the source configuration.
-For more information see <>.
-
-[float]
-=== Indices and fields tab
+NOTE: These settings are shared with metrics. Changes you make here may also affect the settings used by the *Metrics* app.
-In the *Indices and fields* tab, you can change the following values:
+In the *Settings* tab, you can change the values in these sections:
* *Name*: the name of the source configuration
* *Indices*: the index pattern or patterns in the Elasticsearch indices to read metrics data and log data from
* *Fields*: the names of specific fields in the indices that are used to query and interpret the data correctly
+* *Log columns*: the columns that are shown in the logs stream
-[float]
-==== Log columns configuration
-
-In the *Log columns* tab you can change the columns that are displayed in the Logs app.
-By default the following columns are shown:
+By default the logs stream shows following columns:
* *Timestamp*: The timestamp of the log entry from the `timestamp` field.
* *Message*: The message extracted from the document.
The content of this field depends on the type of log message.
-If no special log message type is detected, the {ecs-link} field `message` is used.
-// ++ add a better link. The actual page location is ecs-base
+If no special log message type is detected, the Elastic Common Schema (ECS) {ecs-base-link} field, `message`, is used.
-To add a new column, click *Add column*.
+To add a new column to the logs stream, in the *Settings* tab, click *Add column*.
In the list of available fields, select the field you want to add.
You can start typing a field name in the search box to filter the field list by that name.
To remove an existing column, click the *Remove this column* icon
-image:logs/images/logs-configure-source-dialog-remove-column-button.png[Remove column].
\ No newline at end of file
+image:logs/images/logs-configure-source-dialog-remove-column-button.png[Remove column].
+
+When you have completed your changes, click *Apply*.
+
+If the fields are greyed out and cannot be edited, you may not have sufficient privileges to change the source configuration.
+For more information see <>.
+
+TIP: If <> are enabled in your Kibana instance, any configuration changes you make here are specific to the current space.
+You can make different subsets of data available by creating multiple spaces with different data source configurations.
+
diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc
index bebc225feb0e6..65693f4399e53 100644
--- a/docs/logs/using.asciidoc
+++ b/docs/logs/using.asciidoc
@@ -43,6 +43,11 @@ To quickly jump to a nearby point in time, click the minimap timeline to the rig
Click *Customize* to customize the view.
Here, you can set the scale to use for the minimap timeline, choose whether to wrap long lines, and choose your preferred text size.
+[float]
+=== Configuring the data to use for your logs
+
+If your logs have custom index patterns, use non-default field settings, or contain parsed fields which you want to expose as individual columns, you can <>.
+
[float]
[[logs-stream]]
=== Stream or pause logs
@@ -70,10 +75,10 @@ To highlight a word or phrase in the logs stream, click *Highlights* and enter y
To inspect a log event, hover over it, then click the *View details* icon image:logs/images/logs-view-event.png[View event icon] beside the event.
This opens the *Log event document details* fly-out that shows the fields associated with the log event.
-To quickly filter the logs stream by one of the field values, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field.
+To quickly filter the logs stream by one of the field values, in the log event details, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field.
This automatically adds a search filter to the logs stream to filter the entries by this field and value.
-In the log event details, click *Actions* to see the other actions related to the event.
+To see other actions related to the event, in the log event details, click *Actions*.
Depending on the event and the features you have installed and configured, you may also be able to:
* Select *View status in Uptime* to <> in the *Uptime* app.
diff --git a/packages/kbn-es-query/src/filters/lib/meta_filter.ts b/packages/kbn-es-query/src/filters/lib/meta_filter.ts
index 7740dfa644353..8f6aef782cea2 100644
--- a/packages/kbn-es-query/src/filters/lib/meta_filter.ts
+++ b/packages/kbn-es-query/src/filters/lib/meta_filter.ts
@@ -26,6 +26,12 @@ export interface FilterState {
store: FilterStateStore;
}
+type FilterFormatterFunction = (value: any) => string;
+export interface FilterValueFormatter {
+ convert: FilterFormatterFunction;
+ getConverterFor: (type: string) => FilterFormatterFunction;
+}
+
export interface FilterMeta {
// index and type are optional only because when you create a new filter, there are no defaults
index?: string;
@@ -34,7 +40,7 @@ export interface FilterMeta {
negate: boolean;
alias: string | null;
key?: string;
- value?: string;
+ value?: string | ((formatter?: FilterValueFormatter) => string);
params?: any;
}
diff --git a/src/core/public/context/context_service.test.ts b/src/core/public/context/context_service.test.ts
index d575d57a6b275..934ea77df6fdc 100644
--- a/src/core/public/context/context_service.test.ts
+++ b/src/core/public/context/context_service.test.ts
@@ -20,17 +20,18 @@
import { PluginOpaqueId } from '../../server';
import { MockContextConstructor } from './context_service.test.mocks';
import { ContextService } from './context_service';
+import { coreMock } from '../mocks';
const pluginDependencies = new Map();
describe('ContextService', () => {
describe('#setup()', () => {
test('createContextContainer returns a new container configured with pluginDependencies', () => {
- const coreId = Symbol();
- const service = new ContextService({ coreId });
+ const context = coreMock.createCoreContext();
+ const service = new ContextService(context);
const setup = service.setup({ pluginDependencies });
expect(setup.createContextContainer()).toBeDefined();
- expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, coreId);
+ expect(MockContextConstructor).toHaveBeenCalledWith(pluginDependencies, context.coreId);
});
});
});
diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts
index 193e4dd23f23c..7104b9e10c741 100644
--- a/src/core/public/core_system.ts
+++ b/src/core/public/core_system.ts
@@ -20,6 +20,7 @@
import './core.css';
import { CoreId } from '../server';
+import { PackageInfo, EnvironmentMode } from '../server/types';
import { CoreSetup, CoreStart } from '.';
import { ChromeService } from './chrome';
import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors';
@@ -55,6 +56,10 @@ interface Params {
/** @internal */
export interface CoreContext {
coreId: CoreId;
+ env: {
+ mode: Readonly;
+ packageInfo: Readonly;
+ };
}
/** @internal */
@@ -130,7 +135,8 @@ export class CoreSystem {
this.rendering = new RenderingService();
this.application = new ApplicationService();
- this.coreContext = { coreId: Symbol('core') };
+ this.coreContext = { coreId: Symbol('core'), env: injectedMetadata.env };
+
this.context = new ContextService(this.coreContext);
this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins);
diff --git a/src/core/public/index.ts b/src/core/public/index.ts
index 5efc94af38246..3d451c7c1f37e 100644
--- a/src/core/public/index.ts
+++ b/src/core/public/index.ts
@@ -60,6 +60,7 @@ import { UiSettingsClient, UiSettingsState, UiSettingsClientContract } from './u
import { ApplicationSetup, Capabilities, ApplicationStart } from './application';
import { DocLinksStart } from './doc_links';
import { SavedObjectsStart } from './saved_objects';
+export { PackageInfo, EnvironmentMode } from '../server/types';
import {
IContextContainer,
IContextProvider,
diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts
index aa9d2a1458735..478285a70771e 100644
--- a/src/core/public/injected_metadata/injected_metadata_service.ts
+++ b/src/core/public/injected_metadata/injected_metadata_service.ts
@@ -19,6 +19,7 @@
import { get } from 'lodash';
import { DiscoveredPlugin, PluginName } from '../../server';
+import { EnvironmentMode, PackageInfo } from '../../server/types';
import { UiSettingsState } from '../ui_settings';
import { deepFreeze } from '../../utils/';
import { Capabilities } from '..';
@@ -46,6 +47,10 @@ export interface InjectedMetadataParams {
vars: {
[key: string]: unknown;
};
+ env: {
+ mode: Readonly;
+ packageInfo: Readonly;
+ };
uiPlugins: Array<{
id: PluginName;
plugin: DiscoveredPlugin;
diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts
index 8ce163fd59e14..8345980b6869d 100644
--- a/src/core/public/mocks.ts
+++ b/src/core/public/mocks.ts
@@ -18,7 +18,7 @@
*/
import { applicationServiceMock } from './application/application_service.mock';
import { chromeServiceMock } from './chrome/chrome_service.mock';
-import { CoreSetup, CoreStart, PluginInitializerContext } from '.';
+import { CoreContext, CoreSetup, CoreStart, PluginInitializerContext } from '.';
import { docLinksServiceMock } from './doc_links/doc_links_service.mock';
import { fatalErrorsServiceMock } from './fatal_errors/fatal_errors_service.mock';
import { httpServiceMock } from './http/http_service.mock';
@@ -75,9 +75,51 @@ function createCoreStartMock() {
return mock;
}
+function pluginInitializerContextMock() {
+ const mock: PluginInitializerContext = {
+ opaqueId: Symbol(),
+ env: {
+ mode: {
+ dev: true,
+ name: 'development',
+ prod: false,
+ },
+ packageInfo: {
+ version: 'version',
+ branch: 'branch',
+ buildNum: 100,
+ buildSha: 'buildSha',
+ dist: false,
+ },
+ },
+ };
+
+ return mock;
+}
+
+function createCoreContext(): CoreContext {
+ return {
+ coreId: Symbol('core context mock'),
+ env: {
+ mode: {
+ dev: true,
+ name: 'development',
+ prod: false,
+ },
+ packageInfo: {
+ version: 'version',
+ branch: 'branch',
+ buildNum: 100,
+ buildSha: 'buildSha',
+ dist: false,
+ },
+ },
+ };
+}
export const coreMock = {
+ createCoreContext,
createSetup: createCoreSetupMock,
createStart: createCoreStartMock,
- createPluginInitializerContext: jest.fn() as jest.Mock,
+ createPluginInitializerContext: pluginInitializerContextMock,
};
diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts
index 6cbe0c7e0ed82..85de5c6620cc1 100644
--- a/src/core/public/plugins/plugin.test.ts
+++ b/src/core/public/plugins/plugin.test.ts
@@ -19,6 +19,7 @@
import { mockInitializer, mockPlugin, mockPluginLoader } from './plugin.test.mocks';
import { DiscoveredPlugin } from '../../server';
+import { coreMock } from '../mocks';
import { PluginWrapper } from './plugin';
function createManifest(
@@ -36,7 +37,7 @@ function createManifest(
let plugin: PluginWrapper>;
const opaqueId = Symbol();
-const initializerContext = { opaqueId };
+const initializerContext = coreMock.createPluginInitializerContext();
const addBasePath = (path: string) => path;
beforeEach(() => {
diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts
index 51bd118d280e3..eae45654fce18 100644
--- a/src/core/public/plugins/plugin_context.ts
+++ b/src/core/public/plugins/plugin_context.ts
@@ -19,7 +19,8 @@
import { omit } from 'lodash';
-import { DiscoveredPlugin, PluginOpaqueId } from '../../server';
+import { DiscoveredPlugin } from '../../server';
+import { PluginOpaqueId, PackageInfo, EnvironmentMode } from '../../server/types';
import { CoreContext } from '../core_system';
import { PluginWrapper } from './plugin';
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
@@ -35,6 +36,10 @@ export interface PluginInitializerContext {
* A symbol used to identify this plugin in the system. Needed when registering handlers or context providers.
*/
readonly opaqueId: PluginOpaqueId;
+ readonly env: {
+ mode: Readonly;
+ packageInfo: Readonly;
+ };
}
/**
@@ -52,6 +57,7 @@ export function createPluginInitializerContext(
): PluginInitializerContext {
return {
opaqueId,
+ env: coreContext.env,
};
}
diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts
index 358bf71ac9188..cfac4c3648053 100644
--- a/src/core/public/plugins/plugins_service.test.ts
+++ b/src/core/public/plugins/plugins_service.test.ts
@@ -26,7 +26,7 @@ import {
} from './plugins_service.test.mocks';
import { PluginName, DiscoveredPlugin } from 'src/core/server';
-import { CoreContext } from '../core_system';
+import { coreMock } from '../mocks';
import {
PluginsService,
PluginsServiceStartDeps,
@@ -56,7 +56,7 @@ let plugins: Array<{ id: string; plugin: DiscoveredPlugin }>;
type DeeplyMocked = { [P in keyof T]: jest.Mocked };
-const mockCoreContext: CoreContext = { coreId: Symbol() };
+const mockCoreContext = coreMock.createCoreContext();
let mockSetupDeps: DeeplyMocked;
let mockSetupContext: DeeplyMocked;
let mockStartDeps: DeeplyMocked;
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 52a4f94945e19..515c02f802c40 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -226,6 +226,11 @@ export interface CoreContext {
//
// (undocumented)
coreId: CoreId;
+ // (undocumented)
+ env: {
+ mode: Readonly;
+ packageInfo: Readonly;
+ };
}
// @public
@@ -379,6 +384,16 @@ export interface DocLinksStart {
};
}
+// @public (undocumented)
+export interface EnvironmentMode {
+ // (undocumented)
+ dev: boolean;
+ // (undocumented)
+ name: 'development' | 'production';
+ // (undocumented)
+ prod: boolean;
+}
+
// @public
export interface ErrorToastOptions {
title: string;
@@ -642,6 +657,20 @@ export interface OverlayStart {
}) => OverlayRef;
}
+// @public (undocumented)
+export interface PackageInfo {
+ // (undocumented)
+ branch: string;
+ // (undocumented)
+ buildNum: number;
+ // (undocumented)
+ buildSha: string;
+ // (undocumented)
+ dist: boolean;
+ // (undocumented)
+ version: string;
+}
+
// @public
export interface Plugin {
// (undocumented)
@@ -657,6 +686,11 @@ export type PluginInitializer;
+ packageInfo: Readonly;
+ };
readonly opaqueId: PluginOpaqueId;
}
diff --git a/src/core/server/config/env.ts b/src/core/server/config/env.ts
index 89d0e68ed276b..bb6064c71742d 100644
--- a/src/core/server/config/env.ts
+++ b/src/core/server/config/env.ts
@@ -18,25 +18,12 @@
*/
import { resolve, dirname } from 'path';
+import { PackageInfo, EnvironmentMode } from './types';
// `require` is necessary for this to work inside x-pack code as well
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../../../../package.json');
-export interface PackageInfo {
- version: string;
- branch: string;
- buildNum: number;
- buildSha: string;
- dist: boolean;
-}
-
-export interface EnvironmentMode {
- name: 'development' | 'production';
- dev: boolean;
- prod: boolean;
-}
-
/** @internal */
export interface EnvOptions {
configs: string[];
diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts
index d27462a86a9c8..491a24b2ab3d6 100644
--- a/src/core/server/config/index.ts
+++ b/src/core/server/config/index.ts
@@ -21,6 +21,6 @@ export { ConfigService, IConfigService } from './config_service';
export { RawConfigService } from './raw_config_service';
export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config';
export { ObjectToConfigAdapter } from './object_to_config_adapter';
-export { CliArgs } from './env';
+export { CliArgs, Env } from './env';
-export { Env, EnvironmentMode, PackageInfo } from './env';
+export { EnvironmentMode, PackageInfo } from './types';
diff --git a/src/core/server/config/types.ts b/src/core/server/config/types.ts
new file mode 100644
index 0000000000000..117c8bf7c3b44
--- /dev/null
+++ b/src/core/server/config/types.ts
@@ -0,0 +1,38 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @public
+ */
+export interface PackageInfo {
+ version: string;
+ branch: string;
+ buildNum: number;
+ buildSha: string;
+ dist: boolean;
+}
+
+/**
+ * @public
+ */
+export interface EnvironmentMode {
+ name: 'development' | 'production';
+ dev: boolean;
+ prod: boolean;
+}
diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts
index 3beb2d17f7b60..bffa23551dd52 100644
--- a/src/core/server/http/router/route.ts
+++ b/src/core/server/http/router/route.ts
@@ -72,13 +72,18 @@ export interface RouteConfig
+ * console.log(req.params.id); // value
+ * });
+ *
+ * router.get({
+ * path: 'path/{id}',
+ * validate: false, // handler has no access to params, query, body values.
+ * },
+ * (context, req, res,) {
+ * req.params; // type Readonly<{}>;
+ * console.log(req.params.id); // undefined
+ * });
+ *
+ * router.get({
+ * path: 'path/{id}',
+ * validate: {
+ * // handler has access to raw non-validated params in runtime
+ * params: schema.object({}, { allowUnknowns: true })
+ * },
+ * },
+ * (context, req, res,) {
+ * req.params; // type Readonly<{}>;
+ * console.log(req.params.id); // value
+ * myValidationLibrary.validate({ params: req.params });
+ * });
* ```
*/
validate: RouteSchemas
| false;
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 369accad43ffc..0941153fee87d 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -50,7 +50,7 @@ import { ContextSetup } from './context';
import { SavedObjectsServiceStart } from './saved_objects';
export { bootstrap } from './bootstrap';
-export { ConfigPath, ConfigService } from './config';
+export { ConfigPath, ConfigService, EnvironmentMode, PackageInfo } from './config';
export {
IContextContainer,
IContextProvider,
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index 08b0ebc6501ec..52e999f5e03a7 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -116,7 +116,7 @@ export class LegacyService implements CoreService {
this.update$ = this.coreContext.configService.getConfig$().pipe(
tap(config => {
if (this.kbnServer !== undefined) {
- this.kbnServer.applyLoggingConfiguration(config.toRaw());
+ this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config));
}
}),
tap({ error: err => this.log.error(err) }),
@@ -257,6 +257,10 @@ export class LegacyService implements CoreService {
settings,
config,
{
+ env: {
+ mode: this.coreContext.env.mode,
+ packageInfo: this.coreContext.env.packageInfo,
+ },
handledConfigPaths: await this.coreContext.configService.getUsedPaths(),
setupDeps: {
core: coreSetup,
diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts
index f072ae5fd4dbd..c3d524c77402c 100644
--- a/src/core/server/mocks.ts
+++ b/src/core/server/mocks.ts
@@ -40,7 +40,7 @@ export function pluginInitializerContextConfigMock(config: T) {
return mock;
}
-function pluginInitializerContextMock(config: T) {
+function pluginInitializerContextMock(config: T = {} as T) {
const mock: PluginInitializerContext = {
opaqueId: Symbol(),
logger: loggingServiceMock.create(),
@@ -50,6 +50,13 @@ function pluginInitializerContextMock(config: T) {
name: 'development',
prod: false,
},
+ packageInfo: {
+ version: 'version',
+ branch: 'branch',
+ buildNum: 100,
+ buildSha: 'buildSha',
+ dist: false,
+ },
},
config: pluginInitializerContextConfigMock(config),
};
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index 46432cbb0da75..edcafbb9a3dc3 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -47,7 +47,10 @@ export function createPluginInitializerContext(
/**
* Environment information that is safe to expose to plugins and may be beneficial for them.
*/
- env: { mode: coreContext.env.mode },
+ env: {
+ mode: coreContext.env.mode,
+ packageInfo: coreContext.env.packageInfo,
+ },
/**
* Plugin-scoped logger
diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts
index 4b66c9fb65c18..9a3e922b3cb89 100644
--- a/src/core/server/plugins/types.ts
+++ b/src/core/server/plugins/types.ts
@@ -20,7 +20,7 @@
import { Observable } from 'rxjs';
import { Type } from '@kbn/config-schema';
-import { ConfigPath, EnvironmentMode } from '../config';
+import { ConfigPath, EnvironmentMode, PackageInfo } from '../config';
import { LoggerFactory } from '../logging';
import { CoreSetup, CoreStart } from '..';
@@ -159,7 +159,10 @@ export interface Plugin<
*/
export interface PluginInitializerContext {
opaqueId: PluginOpaqueId;
- env: { mode: EnvironmentMode };
+ env: {
+ mode: EnvironmentMode;
+ packageInfo: Readonly;
+ };
logger: LoggerFactory;
config: {
create: () => Observable;
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 24d555d758cb3..3988d8dc7a751 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -597,6 +597,16 @@ export interface ElasticsearchServiceSetup {
readonly dataClient$: Observable;
}
+// @public (undocumented)
+export interface EnvironmentMode {
+ // (undocumented)
+ dev: boolean;
+ // (undocumented)
+ name: 'development' | 'production';
+ // (undocumented)
+ prod: boolean;
+}
+
// @public
export interface ErrorHttpResponseOptions {
body?: ResponseError;
@@ -930,6 +940,20 @@ export interface OnPreAuthToolkit {
rewriteUrl: (url: string) => OnPreAuthResult;
}
+// @public (undocumented)
+export interface PackageInfo {
+ // (undocumented)
+ branch: string;
+ // (undocumented)
+ buildNum: number;
+ // (undocumented)
+ buildSha: string;
+ // (undocumented)
+ dist: boolean;
+ // (undocumented)
+ version: string;
+}
+
// @public
export interface Plugin {
// (undocumented)
@@ -953,6 +977,7 @@ export interface PluginInitializerContext {
// (undocumented)
env: {
mode: EnvironmentMode;
+ packageInfo: Readonly;
};
// (undocumented)
logger: LoggerFactory;
@@ -1525,6 +1550,5 @@ export interface SessionStorageFactory {
//
// src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/plugins_service.ts:39:5 - (ae-forgotten-export) The symbol "DiscoveredPluginInternal" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:162:10 - (ae-forgotten-export) The symbol "EnvironmentMode" needs to be exported by the entry point index.d.ts
```
diff --git a/src/core/server/types.ts b/src/core/server/types.ts
index d712c804d45d1..46c70a91721b5 100644
--- a/src/core/server/types.ts
+++ b/src/core/server/types.ts
@@ -20,3 +20,4 @@
/** This module is intended for consumption by public to avoid import issues with server-side code */
export { PluginOpaqueId } from './plugins/types';
export * from './saved_objects/types';
+export { EnvironmentMode, PackageInfo } from './config/types';
diff --git a/src/legacy/core_plugins/console/server/api_server/es_6_0/search.js b/src/legacy/core_plugins/console/server/api_server/es_6_0/search.js
index f77144b1524c6..71cc9626a273c 100644
--- a/src/legacy/core_plugins/console/server/api_server/es_6_0/search.js
+++ b/src/legacy/core_plugins/console/server/api_server/es_6_0/search.js
@@ -189,6 +189,7 @@ export default function (api) {
stats: [''],
timeout: '1s',
version: { __one_of: [true, false] },
+ track_total_hits: { __one_of: [true, false] },
},
});
diff --git a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx
index 4212daff0371e..6270dee72ab05 100644
--- a/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx
+++ b/src/legacy/core_plugins/data/public/filter/apply_filters/apply_filters_popover.tsx
@@ -33,10 +33,13 @@ import {
import { Filter } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { Component } from 'react';
-import { getFilterDisplayText } from '../filter_bar/filter_view';
+import { IndexPattern } from '../../index_patterns';
+import { getDisplayValueFromFilter } from '../filter_bar/filter_editor/lib/filter_editor_utils';
+import { getFilterDisplayText } from '../filter_bar/filter_editor/lib/get_filter_display_text';
interface Props {
filters: Filter[];
+ indexPatterns: IndexPattern[];
onCancel: () => void;
onSubmit: (filters: Filter[]) => void;
}
@@ -57,6 +60,11 @@ export class ApplyFiltersPopover extends Component {
};
}
+ private getLabel(filter: Filter) {
+ const filterDisplayValue = getDisplayValueFromFilter(filter, this.props.indexPatterns);
+ return getFilterDisplayText(filter, filterDisplayValue);
+ }
+
public render() {
if (this.props.filters.length === 0) {
return '';
@@ -67,7 +75,7 @@ export class ApplyFiltersPopover extends Component {
{this.props.filters.map((filter, i) => (
this.toggleFilterSelected(i)}
/>
diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts
index f0628f03c173e..c6bd435708782 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts
@@ -31,7 +31,7 @@ import {
PhrasesFilter,
RangeFilter,
} from '@kbn/es-query';
-import { omit } from 'lodash';
+import { omit, get } from 'lodash';
import { Ipv4Address } from '../../../../../../../../plugins/kibana_utils/public';
import { Field, IndexPattern, isFilterable } from '../../../../index_patterns';
import { FILTER_OPERATORS, Operator } from './filter_operators';
@@ -43,6 +43,27 @@ export function getIndexPatternFromFilter(
return indexPatterns.find(indexPattern => indexPattern.id === filter.meta.index);
}
+function getValueFormatter(indexPattern?: IndexPattern, key?: string) {
+ if (!indexPattern || !key) return;
+ let format = get(indexPattern, ['fields', 'byName', key, 'format']);
+ if (!format && indexPattern.fields.getByName) {
+ // TODO: Why is indexPatterns sometimes a map and sometimes an array?
+ format = (indexPattern.fields.getByName(key) as Field).format;
+ }
+ return format;
+}
+
+export function getDisplayValueFromFilter(filter: Filter, indexPatterns: IndexPattern[]): string {
+ const indexPattern = getIndexPatternFromFilter(filter, indexPatterns);
+
+ if (typeof filter.meta.value === 'function') {
+ const valueFormatter: any = getValueFormatter(indexPattern, filter.meta.key);
+ return filter.meta.value(valueFormatter);
+ } else {
+ return filter.meta.value || '';
+ }
+}
+
export function getFieldFromFilter(filter: FieldFilter, indexPattern: IndexPattern) {
return indexPattern.fields.find(field => field.name === filter.meta.key);
}
diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/get_filter_display_text.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/get_filter_display_text.ts
new file mode 100644
index 0000000000000..73ee1a69a2ce3
--- /dev/null
+++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/get_filter_display_text.ts
@@ -0,0 +1,53 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Filter } from '@kbn/es-query';
+import { i18n } from '@kbn/i18n';
+import { existsOperator, isOneOfOperator } from './filter_operators';
+
+export function getFilterDisplayText(filter: Filter, filterDisplayName: string) {
+ const prefix = filter.meta.negate
+ ? ` ${i18n.translate('data.filter.filterBar.negatedFilterPrefix', {
+ defaultMessage: 'NOT ',
+ })}`
+ : '';
+
+ if (filter.meta.alias !== null) {
+ return `${prefix}${filter.meta.alias}`;
+ }
+
+ switch (filter.meta.type) {
+ case 'exists':
+ return `${prefix}${filter.meta.key} ${existsOperator.message}`;
+ case 'geo_bounding_box':
+ return `${prefix}${filter.meta.key}: ${filterDisplayName}`;
+ case 'geo_polygon':
+ return `${prefix}${filter.meta.key}: ${filterDisplayName}`;
+ case 'phrase':
+ return `${prefix}${filter.meta.key}: ${filterDisplayName}`;
+ case 'phrases':
+ return `${prefix}${filter.meta.key} ${isOneOfOperator.message} ${filterDisplayName}`;
+ case 'query_string':
+ return `${prefix}${filterDisplayName}`;
+ case 'range':
+ return `${prefix}${filter.meta.key}: ${filterDisplayName}`;
+ default:
+ return `${prefix}${JSON.stringify(filter.query)}`;
+ }
+}
diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx
index 21259cec51d3a..0521b4e45aeb0 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx
+++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx
@@ -32,6 +32,7 @@ import { UiSettingsClientContract } from 'src/core/public';
import { IndexPattern } from '../../index_patterns';
import { FilterEditor } from './filter_editor';
import { FilterView } from './filter_view';
+import { getDisplayValueFromFilter } from './filter_editor/lib/filter_editor_utils';
interface Props {
id: string;
@@ -67,8 +68,9 @@ class FilterItemUI extends Component {
this.props.className
);
+ const displayName = getDisplayValueFromFilter(filter, this.props.indexPatterns);
const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : '';
- const dataTestSubjValue = filter.meta.value ? `filter-value-${filter.meta.value}` : '';
+ const dataTestSubjValue = filter.meta.value ? `filter-value-${displayName}` : '';
const dataTestSubjDisabled = `filter-${
this.props.filter.meta.disabled ? 'disabled' : 'enabled'
}`;
@@ -76,6 +78,7 @@ class FilterItemUI extends Component {
const badge = (
this.props.onRemove()}
onClick={this.togglePopover}
diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx
index dca2101d435cf..a7ea23efce49e 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx
+++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_view/index.tsx
@@ -21,20 +21,25 @@ import { EuiBadge } from '@elastic/eui';
import { Filter, isFilterPinned } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import React, { SFC } from 'react';
-import { existsOperator, isOneOfOperator } from '../filter_editor/lib/filter_operators';
+import { getFilterDisplayText } from '../filter_editor/lib/get_filter_display_text';
interface Props {
filter: Filter;
+ displayName: string;
[propName: string]: any;
}
-export const FilterView: SFC = ({ filter, iconOnClick, onClick, ...rest }: Props) => {
- let title = `Filter: ${getFilterDisplayText(filter)}. ${i18n.translate(
- 'data.filter.filterBar.moreFilterActionsMessage',
- {
- defaultMessage: 'Select for more filter actions.',
- }
- )}`;
+export const FilterView: SFC = ({
+ filter,
+ iconOnClick,
+ onClick,
+ displayName,
+ ...rest
+}: Props) => {
+ let title = i18n.translate('data.filter.filterBar.moreFilterActionsMessage', {
+ defaultMessage: 'Filter: {displayText}. Select for more filter actions.',
+ values: { displayText: getFilterDisplayText(filter, displayName) },
+ });
if (isFilterPinned(filter)) {
title = `${i18n.translate('data.filter.filterBar.pinnedFilterPrefix', {
@@ -67,38 +72,7 @@ export const FilterView: SFC = ({ filter, iconOnClick, onClick, ...rest }
})}
{...rest}
>
- {getFilterDisplayText(filter)}
+ {getFilterDisplayText(filter, displayName)}
);
};
-
-export function getFilterDisplayText(filter: Filter) {
- const prefix = filter.meta.negate
- ? ` ${i18n.translate('data.filter.filterBar.negatedFilterPrefix', {
- defaultMessage: 'NOT ',
- })}`
- : '';
-
- if (filter.meta.alias !== null) {
- return `${prefix}${filter.meta.alias}`;
- }
-
- switch (filter.meta.type) {
- case 'exists':
- return `${prefix}${filter.meta.key} ${existsOperator.message}`;
- case 'geo_bounding_box':
- return `${prefix}${filter.meta.key}: ${filter.meta.value}`;
- case 'geo_polygon':
- return `${prefix}${filter.meta.key}: ${filter.meta.value}`;
- case 'phrase':
- return `${prefix}${filter.meta.key}: ${filter.meta.value}`;
- case 'phrases':
- return `${prefix}${filter.meta.key} ${isOneOfOperator.message} ${filter.meta.value}`;
- case 'query_string':
- return `${prefix}${filter.meta.value}`;
- case 'range':
- return `${prefix}${filter.meta.key}: ${filter.meta.value}`;
- default:
- return `${prefix}${JSON.stringify(filter.query)}`;
- }
-}
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts
index 3980251ce0043..21c51c9f68f41 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.test.ts
@@ -26,9 +26,7 @@ import { Filter, FilterStateStore } from '@kbn/es-query';
import { FilterStateManager } from './filter_state_manager';
import { FilterManager } from './filter_manager';
-import { IndexPatterns } from '../../index_patterns';
import { getFilter } from './test_helpers/get_stub_filter';
-import { StubIndexPatterns } from './test_helpers/stub_index_pattern';
import { StubState } from './test_helpers/stub_state';
import { getFiltersArray } from './test_helpers/get_filters_array';
@@ -48,18 +46,13 @@ describe('filter_manager', () => {
let updateListener: sinon.SinonSpy;
let filterManager: FilterManager;
- let indexPatterns: StubIndexPatterns;
let readyFilters: Filter[];
beforeEach(() => {
updateListener = sinon.stub();
appStateStub = new StubState();
globalStateStub = new StubState();
- indexPatterns = new StubIndexPatterns();
- filterManager = new FilterManager(
- (indexPatterns as unknown) as IndexPatterns,
- setupMock.uiSettings
- );
+ filterManager = new FilterManager(setupMock.uiSettings);
readyFilters = getFiltersArray();
// FilterStateManager is tested indirectly.
@@ -81,7 +74,7 @@ describe('filter_manager', () => {
fetchSubscription.unsubscribe();
}
- await filterManager.removeAll();
+ filterManager.removeAll();
});
describe('observing', () => {
@@ -135,7 +128,7 @@ describe('filter_manager', () => {
test('app state should be set', async () => {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
- await filterManager.setFilters([f1]);
+ filterManager.setFilters([f1]);
expect(filterManager.getAppFilters()).toHaveLength(1);
expect(filterManager.getGlobalFilters()).toHaveLength(0);
expect(filterManager.getFilters()).toHaveLength(1);
@@ -149,7 +142,7 @@ describe('filter_manager', () => {
test('global state should be set', async () => {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
- await filterManager.setFilters([f1]);
+ filterManager.setFilters([f1]);
expect(filterManager.getAppFilters()).toHaveLength(0);
expect(filterManager.getGlobalFilters()).toHaveLength(1);
expect(filterManager.getFilters()).toHaveLength(1);
@@ -164,7 +157,7 @@ describe('filter_manager', () => {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
- await filterManager.setFilters([f1, f2]);
+ filterManager.setFilters([f1, f2]);
expect(filterManager.getAppFilters()).toHaveLength(1);
expect(filterManager.getGlobalFilters()).toHaveLength(1);
expect(filterManager.getFilters()).toHaveLength(2);
@@ -183,8 +176,8 @@ describe('filter_manager', () => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
- await filterManager.setFilters([f1]);
- await filterManager.setFilters([f2]);
+ filterManager.setFilters([f1]);
+ filterManager.setFilters([f2]);
expect(filterManager.getAppFilters()).toHaveLength(1);
expect(filterManager.getGlobalFilters()).toHaveLength(0);
@@ -204,7 +197,7 @@ describe('filter_manager', () => {
const fetchStub = jest.fn();
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, true, false, 'age', 34);
- await filterManager.setFilters([f1]);
+ filterManager.setFilters([f1]);
filterManager.getUpdates$().subscribe({
next: updateStub,
@@ -216,7 +209,7 @@ describe('filter_manager', () => {
const f2 = _.cloneDeep(f1);
f2.meta.negate = true;
- await filterManager.setFilters([f2]);
+ filterManager.setFilters([f2]);
// this time, events should be emitted
expect(fetchStub).toBeCalledTimes(0);
@@ -228,7 +221,7 @@ describe('filter_manager', () => {
test('app state should accept a single filter', async function() {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
- await filterManager.addFilters(f1);
+ filterManager.addFilters(f1);
expect(filterManager.getAppFilters()).toHaveLength(1);
expect(filterManager.getGlobalFilters()).toHaveLength(0);
expect(updateListener.callCount).toBe(1);
@@ -238,8 +231,8 @@ describe('filter_manager', () => {
test('app state should accept array', async () => {
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'female');
- await filterManager.addFilters([f1]);
- await filterManager.addFilters([f2]);
+ filterManager.addFilters([f1]);
+ filterManager.addFilters([f2]);
expect(filterManager.getAppFilters()).toHaveLength(2);
expect(filterManager.getGlobalFilters()).toHaveLength(0);
expect(appStateStub.filters.length).toBe(2);
@@ -248,7 +241,7 @@ describe('filter_manager', () => {
test('global state should accept a single filer', async () => {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
- await filterManager.addFilters(f1);
+ filterManager.addFilters(f1);
expect(filterManager.getAppFilters()).toHaveLength(0);
expect(filterManager.getGlobalFilters()).toHaveLength(1);
expect(updateListener.callCount).toBe(1);
@@ -258,7 +251,7 @@ describe('filter_manager', () => {
test('global state should be accept array', async () => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'gender', 'female');
- await filterManager.addFilters([f1, f2]);
+ filterManager.addFilters([f1, f2]);
expect(filterManager.getAppFilters()).toHaveLength(0);
expect(filterManager.getGlobalFilters()).toHaveLength(2);
expect(globalStateStub.filters.length).toBe(2);
@@ -268,7 +261,7 @@ describe('filter_manager', () => {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'gender', 'female');
- await filterManager.addFilters([f1, f2]);
+ filterManager.addFilters([f1, f2]);
expect(filterManager.getAppFilters()).toHaveLength(0);
expect(filterManager.getGlobalFilters()).toHaveLength(2);
expect(updateListener.callCount).toBe(1);
@@ -278,7 +271,7 @@ describe('filter_manager', () => {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
- await filterManager.addFilters([f1, f2]);
+ filterManager.addFilters([f1, f2]);
// FILTER SHOULD BE ADDED ONLY ONCE, TO GLOBAL
expect(filterManager.getAppFilters()).toHaveLength(0);
@@ -290,7 +283,7 @@ describe('filter_manager', () => {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
- await filterManager.addFilters([f1, f2]);
+ filterManager.addFilters([f1, f2]);
// FILTER SHOULD BE ADDED TWICE
expect(filterManager.getAppFilters()).toHaveLength(1);
@@ -302,7 +295,7 @@ describe('filter_manager', () => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
f1.$state = undefined;
- await filterManager.addFilters([f1], true);
+ filterManager.addFilters([f1], true);
// FILTER SHOULD BE GLOBAL
const f1Output = filterManager.getFilters()[0];
@@ -316,7 +309,7 @@ describe('filter_manager', () => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
f1.$state = undefined;
- await filterManager.addFilters([f1], false);
+ filterManager.addFilters([f1], false);
// FILTER SHOULD BE APP
const f1Output = filterManager.getFilters()[0];
@@ -328,8 +321,8 @@ describe('filter_manager', () => {
test('should return app and global filters', async function() {
const filters = getFiltersArray();
- await filterManager.addFilters(filters[0], false);
- await filterManager.addFilters(filters[1], true);
+ filterManager.addFilters(filters[0], false);
+ filterManager.addFilters(filters[1], true);
// global filters should be listed first
let res = filterManager.getFilters();
@@ -343,16 +336,16 @@ describe('filter_manager', () => {
expect(res[1].query).toEqual(filters[0].query);
// should return updated version of filters
- await filterManager.addFilters(filters[2], false);
+ filterManager.addFilters(filters[2], false);
res = filterManager.getFilters();
expect(res).toHaveLength(3);
});
test('should skip appStateStub filters that match globalStateStub filters', async function() {
- await filterManager.addFilters(readyFilters, true);
+ filterManager.addFilters(readyFilters, true);
const appFilter = _.cloneDeep(readyFilters[1]);
- await filterManager.addFilters(appFilter, false);
+ filterManager.addFilters(appFilter, false);
// global filters should be listed first
const res = filterManager.getFilters();
@@ -367,7 +360,7 @@ describe('filter_manager', () => {
const filter = _.cloneDeep(readyFilters[0]);
filter.meta.negate = false;
- await filterManager.addFilters(filter);
+ filterManager.addFilters(filter);
expect(filterManager.getFilters()).toHaveLength(1);
expect(filterManager.getFilters()[0]).toEqual(filter);
@@ -375,7 +368,7 @@ describe('filter_manager', () => {
const negatedFilter = _.cloneDeep(readyFilters[0]);
negatedFilter.meta.negate = true;
- await filterManager.addFilters(negatedFilter);
+ filterManager.addFilters(negatedFilter);
// The negated filter should overwrite the positive one
expect(globalStateStub.filters.length).toBe(1);
expect(filterManager.getFilters()).toHaveLength(1);
@@ -387,7 +380,7 @@ describe('filter_manager', () => {
const negatedFilter = _.cloneDeep(readyFilters[0]);
negatedFilter.meta.negate = true;
- await filterManager.addFilters(negatedFilter);
+ filterManager.addFilters(negatedFilter);
// The negated filter should overwrite the positive one
expect(globalStateStub.filters.length).toBe(1);
@@ -397,7 +390,7 @@ describe('filter_manager', () => {
const filter = _.cloneDeep(readyFilters[0]);
filter.meta.negate = false;
- await filterManager.addFilters(filter);
+ filterManager.addFilters(filter);
expect(globalStateStub.filters.length).toBe(1);
expect(globalStateStub.filters[0]).toEqual(filter);
});
@@ -414,7 +407,7 @@ describe('filter_manager', () => {
next: fetchStub,
});
- await filterManager.addFilters(readyFilters);
+ filterManager.addFilters(readyFilters);
// updates should trigger state saves
expect(appStateStub.save.callCount).toBe(1);
@@ -429,26 +422,26 @@ describe('filter_manager', () => {
describe('filter reconciliation', function() {
test('should de-dupe appStateStub filters being added', async function() {
const newFilter = _.cloneDeep(readyFilters[1]);
- await filterManager.addFilters(readyFilters, false);
+ filterManager.addFilters(readyFilters, false);
expect(appStateStub.filters.length).toBe(3);
- await filterManager.addFilters(newFilter, false);
+ filterManager.addFilters(newFilter, false);
expect(appStateStub.filters.length).toBe(3);
});
test('should de-dupe globalStateStub filters being added', async function() {
const newFilter = _.cloneDeep(readyFilters[1]);
- await filterManager.addFilters(readyFilters, true);
+ filterManager.addFilters(readyFilters, true);
expect(globalStateStub.filters.length).toBe(3);
- await filterManager.addFilters(newFilter, true);
+ filterManager.addFilters(newFilter, true);
expect(globalStateStub.filters.length).toBe(3);
});
test('should de-dupe globalStateStub filters being set', async () => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = _.cloneDeep(f1);
- await filterManager.setFilters([f1, f2]);
+ filterManager.setFilters([f1, f2]);
expect(filterManager.getAppFilters()).toHaveLength(0);
expect(filterManager.getGlobalFilters()).toHaveLength(1);
expect(filterManager.getFilters()).toHaveLength(1);
@@ -457,7 +450,7 @@ describe('filter_manager', () => {
test('should de-dupe appStateStub filters being set', async () => {
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
const f2 = _.cloneDeep(f1);
- await filterManager.setFilters([f1, f2]);
+ filterManager.setFilters([f1, f2]);
expect(filterManager.getAppFilters()).toHaveLength(1);
expect(filterManager.getGlobalFilters()).toHaveLength(0);
expect(filterManager.getFilters()).toHaveLength(1);
@@ -465,14 +458,14 @@ describe('filter_manager', () => {
test('should mutate global filters on appStateStub filter changes', async function() {
const idx = 1;
- await filterManager.addFilters(readyFilters, true);
+ filterManager.addFilters(readyFilters, true);
const appFilter = _.cloneDeep(readyFilters[idx]);
appFilter.meta.negate = true;
appFilter.$state = {
store: FilterStateStore.APP_STATE,
};
- await filterManager.addFilters(appFilter);
+ filterManager.addFilters(appFilter);
const res = filterManager.getFilters();
expect(res).toHaveLength(3);
_.each(res, function(filter, i) {
@@ -483,13 +476,13 @@ describe('filter_manager', () => {
});
test('should merge conflicting appStateStub filters', async function() {
- await filterManager.addFilters(readyFilters, true);
+ filterManager.addFilters(readyFilters, true);
const appFilter = _.cloneDeep(readyFilters[1]);
appFilter.meta.negate = true;
appFilter.$state = {
store: FilterStateStore.APP_STATE,
};
- await filterManager.addFilters(appFilter, false);
+ filterManager.addFilters(appFilter, false);
// global filters should be listed first
const res = filterManager.getFilters();
@@ -508,8 +501,8 @@ describe('filter_manager', () => {
f.meta.disabled = true;
return f;
});
- await filterManager.addFilters(disabledFilters, true);
- await filterManager.addFilters(readyFilters, true);
+ filterManager.addFilters(disabledFilters, true);
+ filterManager.addFilters(readyFilters, true);
const res = filterManager.getFilters();
expect(res).toHaveLength(3);
@@ -527,8 +520,8 @@ describe('filter_manager', () => {
f.meta.disabled = true;
return f;
});
- await filterManager.addFilters(disabledFilters, true);
- await filterManager.addFilters(readyFilters, false);
+ filterManager.addFilters(disabledFilters, true);
+ filterManager.addFilters(readyFilters, false);
const res = filterManager.getFilters();
expect(res).toHaveLength(3);
@@ -543,7 +536,7 @@ describe('filter_manager', () => {
describe('remove filters', () => {
test('remove on empty should do nothing and not fire events', async () => {
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
- await filterManager.removeAll();
+ filterManager.removeAll();
expect(updateListener.called).toBeFalsy();
expect(filterManager.getFilters()).toHaveLength(0);
});
@@ -551,10 +544,10 @@ describe('filter_manager', () => {
test('remove on full should clean and fire events', async () => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
- await filterManager.setFilters([f1, f2]);
+ filterManager.setFilters([f1, f2]);
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
- await filterManager.removeAll();
+ filterManager.removeAll();
expect(updateListener.called).toBeTruthy();
expect(filterManager.getFilters()).toHaveLength(0);
});
@@ -563,7 +556,7 @@ describe('filter_manager', () => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
const f3 = getFilter(FilterStateStore.APP_STATE, false, false, 'country', 'US');
- await filterManager.setFilters([f1, f2]);
+ filterManager.setFilters([f1, f2]);
expect(filterManager.getFilters()).toHaveLength(2);
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
@@ -576,7 +569,7 @@ describe('filter_manager', () => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
const f3 = getFilter(FilterStateStore.APP_STATE, false, false, 'country', 'US');
- await filterManager.setFilters([f1, f2, f3]);
+ filterManager.setFilters([f1, f2, f3]);
expect(filterManager.getFilters()).toHaveLength(3);
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
@@ -586,14 +579,14 @@ describe('filter_manager', () => {
});
test('should remove the filter from appStateStub', async function() {
- await filterManager.addFilters(readyFilters, false);
+ filterManager.addFilters(readyFilters, false);
expect(appStateStub.filters).toHaveLength(3);
filterManager.removeFilter(readyFilters[0]);
expect(appStateStub.filters).toHaveLength(2);
});
test('should remove the filter from globalStateStub', async function() {
- await filterManager.addFilters(readyFilters, true);
+ filterManager.addFilters(readyFilters, true);
expect(globalStateStub.filters).toHaveLength(3);
filterManager.removeFilter(readyFilters[0]);
expect(globalStateStub.filters).toHaveLength(2);
@@ -603,7 +596,7 @@ describe('filter_manager', () => {
const updateStub = jest.fn();
const fetchStub = jest.fn();
- await filterManager.addFilters(readyFilters, false);
+ filterManager.addFilters(readyFilters, false);
filterManager.getUpdates$().subscribe({
next: updateStub,
@@ -621,8 +614,8 @@ describe('filter_manager', () => {
});
test('should remove matching filters', async function() {
- await filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
- await filterManager.addFilters([readyFilters[2]], false);
+ filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
+ filterManager.addFilters([readyFilters[2]], false);
filterManager.removeFilter(readyFilters[0]);
@@ -631,8 +624,8 @@ describe('filter_manager', () => {
});
test('should remove matching filters by comparison', async function() {
- await filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
- await filterManager.addFilters([readyFilters[2]], false);
+ filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
+ filterManager.addFilters([readyFilters[2]], false);
filterManager.removeFilter(_.cloneDeep(readyFilters[0]));
@@ -645,8 +638,8 @@ describe('filter_manager', () => {
});
test('should do nothing with a non-matching filter', async function() {
- await filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
- await filterManager.addFilters([readyFilters[2]], false);
+ filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
+ filterManager.addFilters([readyFilters[2]], false);
const missedFilter = _.cloneDeep(readyFilters[0]);
missedFilter.meta.negate = !readyFilters[0].meta.negate;
@@ -657,12 +650,12 @@ describe('filter_manager', () => {
});
test('should remove all the filters from both states', async function() {
- await filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
- await filterManager.addFilters([readyFilters[2]], false);
+ filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
+ filterManager.addFilters([readyFilters[2]], false);
expect(globalStateStub.filters).toHaveLength(2);
expect(appStateStub.filters).toHaveLength(1);
- await filterManager.removeAll();
+ filterManager.removeAll();
expect(globalStateStub.filters).toHaveLength(0);
expect(appStateStub.filters).toHaveLength(0);
});
@@ -670,7 +663,7 @@ describe('filter_manager', () => {
describe('invert', () => {
test('should fire the update and fetch events', async function() {
- await filterManager.addFilters(readyFilters);
+ filterManager.addFilters(readyFilters);
expect(filterManager.getFilters()).toHaveLength(3);
const updateStub = jest.fn();
@@ -684,7 +677,7 @@ describe('filter_manager', () => {
});
readyFilters[1].meta.negate = !readyFilters[1].meta.negate;
- await filterManager.addFilters(readyFilters[1]);
+ filterManager.addFilters(readyFilters[1]);
expect(filterManager.getFilters()).toHaveLength(3);
expect(fetchStub).toBeCalledTimes(1);
expect(updateStub).toBeCalledTimes(1);
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts
index 6e5fa37113451..b3d6bd6873f50 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_manager.ts
@@ -29,17 +29,14 @@ import { mapAndFlattenFilters } from './lib/map_and_flatten_filters';
import { uniqFilters } from './lib/uniq_filters';
import { onlyDisabledFiltersChanged } from './lib/only_disabled';
import { PartitionedFilters } from './partitioned_filters';
-import { IndexPatterns } from '../../index_patterns';
export class FilterManager {
- private indexPatterns: IndexPatterns;
private filters: Filter[] = [];
private updated$: Subject = new Subject();
private fetch$: Subject = new Subject();
private uiSettings: UiSettingsClientContract;
- constructor(indexPatterns: IndexPatterns, uiSettings: UiSettingsClientContract) {
- this.indexPatterns = indexPatterns;
+ constructor(uiSettings: UiSettingsClientContract) {
this.uiSettings = uiSettings;
}
@@ -127,7 +124,7 @@ export class FilterManager {
/* Setters */
- public async addFilters(filters: Filter[] | Filter, pinFilterStatus?: boolean) {
+ public addFilters(filters: Filter[] | Filter, pinFilterStatus?: boolean) {
if (!Array.isArray(filters)) {
filters = [filters];
}
@@ -145,7 +142,7 @@ export class FilterManager {
const store = pinFilterStatus ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE;
FilterManager.setFiltersStore(filters, store);
- const mappedFilters = await mapAndFlattenFilters(this.indexPatterns, filters);
+ const mappedFilters = mapAndFlattenFilters(filters);
// This is where we add new filters to the correct place (app \ global)
const newPartitionedFilters = FilterManager.partitionFilters(mappedFilters);
@@ -157,8 +154,8 @@ export class FilterManager {
this.handleStateUpdate(newFilters);
}
- public async setFilters(newFilters: Filter[]) {
- const mappedFilters = await mapAndFlattenFilters(this.indexPatterns, newFilters);
+ public setFilters(newFilters: Filter[]) {
+ const mappedFilters = mapAndFlattenFilters(newFilters);
const newPartitionedFilters = FilterManager.partitionFilters(mappedFilters);
const mergedFilters = this.mergeIncomingFilters(newPartitionedFilters);
this.handleStateUpdate(mergedFilters);
@@ -176,8 +173,8 @@ export class FilterManager {
}
}
- public async removeAll() {
- await this.setFilters([]);
+ public removeAll() {
+ this.setFilters([]);
}
public static setFiltersStore(filters: Filter[], store: FilterStateStore) {
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts
index 84f336506b375..d1cad9a812399 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts
@@ -22,11 +22,9 @@ import sinon from 'sinon';
import { FilterStateStore } from '@kbn/es-query';
import { FilterStateManager } from './filter_state_manager';
-import { IndexPatterns } from '../../index_patterns';
import { StubState } from './test_helpers/stub_state';
import { getFilter } from './test_helpers/get_stub_filter';
import { FilterManager } from './filter_manager';
-import { StubIndexPatterns } from './test_helpers/stub_index_pattern';
import { coreMock } from '../../../../../../core/public/mocks';
const setupMock = coreMock.createSetup();
@@ -44,11 +42,7 @@ describe('filter_state_manager', () => {
beforeEach(() => {
appStateStub = new StubState();
globalStateStub = new StubState();
- const indexPatterns = new StubIndexPatterns();
- filterManager = new FilterManager(
- (indexPatterns as unknown) as IndexPatterns,
- setupMock.uiSettings
- );
+ filterManager = new FilterManager(setupMock.uiSettings);
});
describe('app_state_undefined', () => {
@@ -81,7 +75,7 @@ describe('filter_state_manager', () => {
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
- await filterManager.setFilters([f1, f2]);
+ filterManager.setFilters([f1, f2]);
sinon.assert.notCalled(appStateStub.save);
sinon.assert.calledOnce(globalStateStub.save);
@@ -89,10 +83,11 @@ describe('filter_state_manager', () => {
});
describe('app_state_defined', () => {
+ let filterStateManager: FilterStateManager;
beforeEach(() => {
// FilterStateManager is tested indirectly.
// Therefore, we don't need it's instance.
- new FilterStateManager(
+ filterStateManager = new FilterStateManager(
globalStateStub,
() => {
return appStateStub;
@@ -101,6 +96,10 @@ describe('filter_state_manager', () => {
);
});
+ afterEach(() => {
+ filterStateManager.destroy();
+ });
+
test('should update filter manager global filters', done => {
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
globalStateStub.filters.push(f1);
@@ -123,27 +122,27 @@ describe('filter_state_manager', () => {
}, 100);
});
- test('should update URL when filter manager filters are set', async () => {
+ test('should update URL when filter manager filters are set', () => {
appStateStub.save = sinon.stub();
globalStateStub.save = sinon.stub();
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
- await filterManager.setFilters([f1, f2]);
+ filterManager.setFilters([f1, f2]);
sinon.assert.calledOnce(appStateStub.save);
sinon.assert.calledOnce(globalStateStub.save);
});
- test('should update URL when filter manager filters are added', async () => {
+ test('should update URL when filter manager filters are added', () => {
appStateStub.save = sinon.stub();
globalStateStub.save = sinon.stub();
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
- await filterManager.addFilters([f1, f2]);
+ filterManager.addFilters([f1, f2]);
sinon.assert.calledOnce(appStateStub.save);
sinon.assert.calledOnce(globalStateStub.save);
@@ -156,13 +155,13 @@ describe('filter_state_manager', () => {
** would cause filter state manager detects those changes
** And triggers *another* filter manager update.
*/
- test('should NOT re-trigger filter manager', async done => {
+ test('should NOT re-trigger filter manager', done => {
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
filterManager.setFilters([f1]);
const setFiltersSpy = sinon.spy(filterManager, 'setFilters');
f1.meta.negate = true;
- await filterManager.setFilters([f1]);
+ filterManager.setFilters([f1]);
setTimeout(() => {
expect(setFiltersSpy.callCount).toEqual(1);
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.test.ts
index 7f9dbb1ea2609..c0c509634aba2 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.test.ts
@@ -31,27 +31,27 @@ describe('filter manager utilities', () => {
});
describe('generateMappingChain()', () => {
- test('should create a chaining function which calls the next function if the promise is rejected', async () => {
+ test('should create a chaining function which calls the next function if the error is thrown', async () => {
const filter: Filter = buildEmptyFilter(true);
- mapping.rejects(filter);
- next.resolves('good');
+ mapping.throws(filter);
+ next.returns('good');
const chain = generateMappingChain(mapping, next);
- const result = await chain(filter);
+ const result = chain(filter);
expect(result).toBe('good');
sinon.assert.calledOnce(next);
});
- test('should create a chaining function which DOES NOT call the next function if the result is resolved', async () => {
+ test('should create a chaining function which DOES NOT call the next function if the result is returned', async () => {
const filter: Filter = buildEmptyFilter(true);
- mapping.resolves('good');
- next.resolves('bad');
+ mapping.returns('good');
+ next.returns('bad');
const chain = generateMappingChain(mapping, next);
- const result = await chain(filter);
+ const result = chain(filter);
expect(result).toBe('good');
});
@@ -59,10 +59,10 @@ describe('filter manager utilities', () => {
test('should resolve result for the mapping function', async () => {
const filter: Filter = buildEmptyFilter(true);
- mapping.resolves({ key: 'test', value: 'example' });
+ mapping.returns({ key: 'test', value: 'example' });
const chain = generateMappingChain(mapping, next);
- const result = await chain(filter);
+ const result = chain(filter);
sinon.assert.notCalled(next);
expect(result).toEqual({ key: 'test', value: 'example' });
@@ -72,10 +72,10 @@ describe('filter manager utilities', () => {
// @ts-ignore
const filter: Filter = { test: 'example' };
- mapping.resolves({ key: 'test', value: 'example' });
+ mapping.returns({ key: 'test', value: 'example' });
const chain = generateMappingChain(mapping, next);
- const result = await chain(filter);
+ const result = chain(filter);
sinon.assert.calledOnce(mapping);
expect(mapping.args[0][0]).toEqual({ test: 'example' });
@@ -86,29 +86,31 @@ describe('filter manager utilities', () => {
test('should resolve result for the next function', async () => {
const filter: Filter = buildEmptyFilter(true);
- mapping.rejects(filter);
- next.resolves({ key: 'test', value: 'example' });
+ mapping.throws(filter);
+ next.returns({ key: 'test', value: 'example' });
const chain = generateMappingChain(mapping, next);
- const result = await chain(filter);
+ const result = chain(filter);
sinon.assert.calledOnce(mapping);
sinon.assert.calledOnce(next);
expect(result).toEqual({ key: 'test', value: 'example' });
});
- test('should reject with an error if no functions match', async done => {
+ test('should throw an error if no functions match', async done => {
const filter: Filter = buildEmptyFilter(true);
- mapping.rejects(filter);
+ mapping.throws(filter);
const chain = generateMappingChain(mapping);
- chain(filter).catch(err => {
+ try {
+ chain(filter);
+ } catch (err) {
expect(err).toBeInstanceOf(Error);
expect(err.message).toBe('No mappings have been found for filter.');
done();
- });
+ }
});
});
});
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.ts
index 38c83f3f4a821..760270edd7170 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/generate_mapping_chain.ts
@@ -23,12 +23,14 @@ const noop = () => {
};
export const generateMappingChain = (fn: Function, next: Function = noop) => {
- return async (filter: Filter) => {
- return await fn(filter).catch((result: any) => {
+ return (filter: Filter) => {
+ try {
+ return fn(filter);
+ } catch (result) {
if (result === filter) {
return next(filter);
}
throw result;
- });
+ }
};
};
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.test.ts
index c9fcc745d48e3..fce2aa0373ebe 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.test.ts
@@ -19,16 +19,16 @@
import { Filter } from '@kbn/es-query';
import { mapAndFlattenFilters } from './map_and_flatten_filters';
-import { StubIndexPatterns } from '../test_helpers/stub_index_pattern';
-import { IndexPatterns } from '../../../index_patterns';
describe('filter manager utilities', () => {
describe('mapAndFlattenFilters()', () => {
- let mockIndexPatterns: unknown;
let filters: unknown;
+ function getDisplayName(filter: Filter) {
+ return typeof filter.meta.value === 'function' ? filter.meta.value() : filter.meta.value;
+ }
+
beforeEach(() => {
- mockIndexPatterns = new StubIndexPatterns();
filters = [
null,
[
@@ -44,11 +44,8 @@ describe('filter manager utilities', () => {
];
});
- test('should map and flatten the filters', async () => {
- const results = await mapAndFlattenFilters(
- mockIndexPatterns as IndexPatterns,
- filters as Filter[]
- );
+ test('should map and flatten the filters', () => {
+ const results = mapAndFlattenFilters(filters as Filter[]);
expect(results).toHaveLength(5);
expect(results[0]).toHaveProperty('meta');
@@ -63,9 +60,11 @@ describe('filter manager utilities', () => {
expect(results[2].meta).toHaveProperty('key', 'query');
expect(results[2].meta).toHaveProperty('value', 'foo:bar');
expect(results[3].meta).toHaveProperty('key', 'bytes');
- expect(results[3].meta).toHaveProperty('value', '1024 to 2048');
+ expect(results[3].meta).toHaveProperty('value');
+ expect(getDisplayName(results[3])).toBe('1024 to 2048');
expect(results[4].meta).toHaveProperty('key', '_type');
- expect(results[4].meta).toHaveProperty('value', 'apache');
+ expect(results[4].meta).toHaveProperty('value');
+ expect(getDisplayName(results[4])).toBe('apache');
});
});
});
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.ts
index 1481295896187..b350c3957b142 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_and_flatten_filters.ts
@@ -20,10 +20,7 @@
import { compact, flatten } from 'lodash';
import { Filter } from '@kbn/es-query';
import { mapFilter } from './map_filter';
-import { IndexPatterns } from '../../../index_patterns';
-export const mapAndFlattenFilters = (indexPatterns: IndexPatterns, filters: Filter[]) => {
- const promises = compact(flatten(filters)).map((item: Filter) => mapFilter(indexPatterns, item));
-
- return Promise.all(promises);
+export const mapAndFlattenFilters = (filters: Filter[]) => {
+ return compact(flatten(filters)).map((item: Filter) => mapFilter(item));
};
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.test.ts
index 78f3b40bbbe2c..acb6e89711033 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.test.ts
@@ -23,7 +23,7 @@ describe('filter manager utilities', () => {
describe('mapDefault()', () => {
test('should return the key and value for matching filters', async () => {
const filter: CustomFilter = buildQueryFilter({ match_all: {} }, 'index');
- const result = await mapDefault(filter);
+ const result = mapDefault(filter);
expect(result).toHaveProperty('key', 'query');
expect(result).toHaveProperty('value', '{"match_all":{}}');
@@ -33,7 +33,7 @@ describe('filter manager utilities', () => {
const filter = buildEmptyFilter(true) as CustomFilter;
try {
- await mapDefault(filter);
+ mapDefault(filter);
} catch (e) {
expect(e).toBe(filter);
}
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.ts
index bdfce4e753f65..70c191879c22e 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_default.ts
@@ -20,7 +20,7 @@
import { Filter, FILTERS } from '@kbn/es-query';
import { find, keys, get } from 'lodash';
-export const mapDefault = async (filter: Filter) => {
+export const mapDefault = (filter: Filter) => {
const metaProperty = /(^\$|meta)/;
const key = find(keys(filter), item => !item.match(metaProperty));
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.test.ts
index 670056b68234f..c352d3e2b9a73 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.test.ts
@@ -24,7 +24,7 @@ describe('filter manager utilities', () => {
describe('mapExists()', () => {
test('should return the key and value for matching filters', async () => {
const filter: ExistsFilter = buildExistsFilter({ name: '_type' }, 'index');
- const result = await mapExists(filter);
+ const result = mapExists(filter);
expect(result).toHaveProperty('key', '_type');
expect(result).toHaveProperty('value', 'exists');
@@ -34,7 +34,7 @@ describe('filter manager utilities', () => {
const filter = buildEmptyFilter(true) as ExistsFilter;
try {
- await mapQueryString(filter);
+ mapQueryString(filter);
} catch (e) {
expect(e).toBe(filter);
done();
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.ts
index bf793378342c7..d539219a1ca24 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_exists.ts
@@ -20,7 +20,7 @@
import { Filter, isExistsFilter, FILTERS } from '@kbn/es-query';
import { get } from 'lodash';
-export const mapExists = async (filter: Filter) => {
+export const mapExists = (filter: Filter) => {
if (isExistsFilter(filter)) {
return {
type: FILTERS.EXISTS,
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.test.ts
index ca701454481ee..ad1c4457f673e 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.test.ts
@@ -19,17 +19,11 @@
import { Filter } from '@kbn/es-query';
import { mapFilter } from './map_filter';
-import { StubIndexPatterns } from '../test_helpers/stub_index_pattern';
-import { IndexPatterns } from '../../../index_patterns';
describe('filter manager utilities', () => {
- let indexPatterns: IndexPatterns;
-
- beforeEach(() => {
- const stubIndexPatterns: unknown = new StubIndexPatterns();
-
- indexPatterns = stubIndexPatterns as IndexPatterns;
- });
+ function getDisplayName(filter: Filter) {
+ return typeof filter.meta.value === 'function' ? filter.meta.value() : filter.meta.value;
+ }
describe('mapFilter()', () => {
test('should map query filters', async () => {
@@ -37,44 +31,48 @@ describe('filter manager utilities', () => {
meta: { index: 'logstash-*' },
query: { match: { _type: { query: 'apache' } } },
};
- const after = await mapFilter(indexPatterns, before as Filter);
+ const after = mapFilter(before as Filter);
expect(after).toHaveProperty('meta');
expect(after.meta).toHaveProperty('key', '_type');
- expect(after.meta).toHaveProperty('value', 'apache');
+ expect(after.meta).toHaveProperty('value');
+ expect(getDisplayName(after)).toBe('apache');
expect(after.meta).toHaveProperty('disabled', false);
expect(after.meta).toHaveProperty('negate', false);
});
test('should map exists filters', async () => {
const before: any = { meta: { index: 'logstash-*' }, exists: { field: '@timestamp' } };
- const after = await mapFilter(indexPatterns, before as Filter);
+ const after = mapFilter(before as Filter);
expect(after).toHaveProperty('meta');
expect(after.meta).toHaveProperty('key', '@timestamp');
- expect(after.meta).toHaveProperty('value', 'exists');
+ expect(after.meta).toHaveProperty('value');
+ expect(getDisplayName(after)).toBe('exists');
expect(after.meta).toHaveProperty('disabled', false);
expect(after.meta).toHaveProperty('negate', false);
});
test('should map missing filters', async () => {
const before: any = { meta: { index: 'logstash-*' }, missing: { field: '@timestamp' } };
- const after = await mapFilter(indexPatterns, before as Filter);
+ const after = mapFilter(before as Filter);
expect(after).toHaveProperty('meta');
expect(after.meta).toHaveProperty('key', '@timestamp');
- expect(after.meta).toHaveProperty('value', 'missing');
+ expect(after.meta).toHaveProperty('value');
+ expect(getDisplayName(after)).toBe('missing');
expect(after.meta).toHaveProperty('disabled', false);
expect(after.meta).toHaveProperty('negate', false);
});
test('should map json filter', async () => {
const before: any = { meta: { index: 'logstash-*' }, query: { match_all: {} } };
- const after = await mapFilter(indexPatterns, before as Filter);
+ const after = mapFilter(before as Filter);
expect(after).toHaveProperty('meta');
expect(after.meta).toHaveProperty('key', 'query');
- expect(after.meta).toHaveProperty('value', '{"match_all":{}}');
+ expect(after.meta).toHaveProperty('value');
+ expect(getDisplayName(after)).toBe('{"match_all":{}}');
expect(after.meta).toHaveProperty('disabled', false);
expect(after.meta).toHaveProperty('negate', false);
});
@@ -83,7 +81,7 @@ describe('filter manager utilities', () => {
const before: any = { meta: { index: 'logstash-*' } };
try {
- await mapFilter(indexPatterns, before as Filter);
+ mapFilter(before as Filter);
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e.message).toBe('No mappings have been found for filter.');
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.ts
index f607595915a52..c0d251e647fd1 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_filter.ts
@@ -19,7 +19,6 @@
import { Filter } from '@kbn/es-query';
import { reduceRight } from 'lodash';
-import { IndexPatterns } from '../../../index_patterns';
import { mapMatchAll } from './map_match_all';
import { mapPhrase } from './map_phrase';
@@ -33,7 +32,7 @@ import { mapGeoPolygon } from './map_geo_polygon';
import { mapDefault } from './map_default';
import { generateMappingChain } from './generate_mapping_chain';
-export async function mapFilter(indexPatterns: IndexPatterns, filter: Filter) {
+export function mapFilter(filter: Filter) {
/** Mappers **/
// Each mapper is a simple promise function that test if the mapper can
@@ -52,14 +51,14 @@ export async function mapFilter(indexPatterns: IndexPatterns, filter: Filter) {
// and add it here. ProTip: These are executed in order listed
const mappers = [
mapMatchAll,
- mapRange(indexPatterns),
- mapPhrase(indexPatterns),
+ mapRange,
+ mapPhrase,
mapPhrases,
mapExists,
mapMissing,
mapQueryString,
- mapGeoBoundingBox(indexPatterns),
- mapGeoPolygon(indexPatterns),
+ mapGeoBoundingBox,
+ mapGeoPolygon,
mapDefault,
];
@@ -74,13 +73,15 @@ export async function mapFilter(indexPatterns: IndexPatterns, filter: Filter) {
(memo, map) => generateMappingChain(map, memo),
noop
);
- const mapped = await mapFn(filter);
+
+ const mapped = mapFn(filter);
// Map the filter into an object with the key and value exposed so it's
// easier to work with in the template
filter.meta = filter.meta || {};
filter.meta.type = mapped.type;
filter.meta.key = mapped.key;
+ // Display value or formatter function.
filter.meta.value = mapped.value;
filter.meta.params = mapped.params;
filter.meta.disabled = Boolean(filter.meta.disabled);
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.test.ts
index 51b38c343d2a9..c3c99e6f6c4a3 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.test.ts
@@ -18,19 +18,10 @@
*/
import { mapGeoBoundingBox } from './map_geo_bounding_box';
-import { StubIndexPatterns } from '../test_helpers/stub_index_pattern';
-import { IndexPatterns } from '../../../index_patterns';
+import { Filter, GeoBoundingBoxFilter } from '@kbn/es-query';
describe('filter manager utilities', () => {
describe('mapGeoBoundingBox()', () => {
- let mapGeoBoundingBoxFn: Function;
-
- beforeEach(() => {
- const indexPatterns: unknown = new StubIndexPatterns();
-
- mapGeoBoundingBoxFn = mapGeoBoundingBox(indexPatterns as IndexPatterns);
- });
-
test('should return the key and value for matching filters with bounds', async () => {
const filter = {
meta: {
@@ -43,16 +34,20 @@ describe('filter manager utilities', () => {
bottom_right: { lat: 15, lon: 20 },
},
},
- };
+ } as GeoBoundingBoxFilter;
- const result = await mapGeoBoundingBoxFn(filter);
+ const result = mapGeoBoundingBox(filter);
expect(result).toHaveProperty('key', 'point');
expect(result).toHaveProperty('value');
- // remove html entities and non-alphanumerics to get the gist of the value
- expect(result.value.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe(
- 'lat5lon10tolat15lon20'
- );
+
+ if (result.value) {
+ const displayName = result.value();
+ // remove html entities and non-alphanumerics to get the gist of the value
+ expect(displayName.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe(
+ 'lat5lon10tolat15lon20'
+ );
+ }
});
test('should return the key and value even when using ignore_unmapped', async () => {
@@ -68,25 +63,29 @@ describe('filter manager utilities', () => {
bottom_right: { lat: 15, lon: 20 },
},
},
- };
- const result = await mapGeoBoundingBoxFn(filter);
+ } as GeoBoundingBoxFilter;
+ const result = mapGeoBoundingBox(filter);
expect(result).toHaveProperty('key', 'point');
expect(result).toHaveProperty('value');
- // remove html entities and non-alphanumerics to get the gist of the value
- expect(result.value.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe(
- 'lat5lon10tolat15lon20'
- );
+
+ if (result.value) {
+ const displayName = result.value();
+ // remove html entities and non-alphanumerics to get the gist of the value
+ expect(displayName.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe(
+ 'lat5lon10tolat15lon20'
+ );
+ }
});
test('should return undefined for none matching', async done => {
const filter = {
meta: { index: 'logstash-*' },
query: { query_string: { query: 'foo:bar' } },
- };
+ } as Filter;
try {
- await mapGeoBoundingBoxFn(filter);
+ mapGeoBoundingBox(filter);
} catch (e) {
expect(e).toBe(filter);
done();
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.ts
index de6b63c287cc4..1f9b8cd842509 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_bounding_box.ts
@@ -16,58 +16,46 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { get } from 'lodash';
-import { GeoBoundingBoxFilter, Filter, FILTERS, isGeoBoundingBoxFilter } from '@kbn/es-query';
-import { IndexPatterns, IndexPattern } from '../../../index_patterns';
-import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/public';
-
-const getFormattedValue = (params: any, key: string, indexPattern?: IndexPattern) => {
- const formatter: any =
- indexPattern && key && get(indexPattern, ['fields', 'byName', key, 'format']);
-
- return formatter
- ? {
- topLeft: formatter.convert(params.top_left),
- bottomRight: formatter.convert(params.bottom_right),
- }
- : {
- topLeft: JSON.stringify(params.top_left),
- bottomRight: JSON.stringify(params.bottom_right),
- };
+import {
+ GeoBoundingBoxFilter,
+ Filter,
+ FILTERS,
+ isGeoBoundingBoxFilter,
+ FilterValueFormatter,
+} from '@kbn/es-query';
+
+const getFormattedValueFn = (params: any) => {
+ return (formatter?: FilterValueFormatter) => {
+ const corners = formatter
+ ? {
+ topLeft: formatter.convert(params.top_left),
+ bottomRight: formatter.convert(params.bottom_right),
+ }
+ : {
+ topLeft: JSON.stringify(params.top_left),
+ bottomRight: JSON.stringify(params.bottom_right),
+ };
+
+ return corners.topLeft + ' to ' + corners.bottomRight;
+ };
};
-const getParams = (filter: GeoBoundingBoxFilter, indexPattern?: IndexPattern) => {
+const getParams = (filter: GeoBoundingBoxFilter) => {
const key = Object.keys(filter.geo_bounding_box).filter(k => k !== 'ignore_unmapped')[0];
const params = filter.geo_bounding_box[key];
- const { topLeft, bottomRight } = getFormattedValue(params, key, indexPattern);
return {
key,
params,
type: FILTERS.GEO_BOUNDING_BOX,
- value: topLeft + ' to ' + bottomRight,
+ value: getFormattedValueFn(params),
};
};
-export const mapGeoBoundingBox = (indexPatterns: IndexPatterns) => {
- return async (filter: Filter) => {
- if (!isGeoBoundingBoxFilter(filter)) {
- throw filter;
- }
+export const mapGeoBoundingBox = (filter: Filter) => {
+ if (!isGeoBoundingBoxFilter(filter)) {
+ throw filter;
+ }
- try {
- let indexPattern;
-
- if (filter.meta.index) {
- indexPattern = await indexPatterns.get(filter.meta.index);
- }
-
- return getParams(filter, indexPattern);
- } catch (error) {
- if (error instanceof SavedObjectNotFound) {
- return getParams(filter);
- }
- throw error;
- }
- };
+ return getParams(filter);
};
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.test.ts
index b9427d541e703..ee4f9b295d682 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.test.ts
@@ -17,19 +17,10 @@
* under the License.
*/
import { mapGeoPolygon } from './map_geo_polygon';
-import { StubIndexPatterns } from '../test_helpers/stub_index_pattern';
-import { IndexPatterns } from '../../../index_patterns';
+import { GeoPolygonFilter, Filter } from '@kbn/es-query';
describe('filter manager utilities', () => {
describe('mapGeoPolygon()', () => {
- let mapGeoPolygonFn: Function;
-
- beforeEach(() => {
- const indexPatterns: unknown = new StubIndexPatterns();
-
- mapGeoPolygonFn = mapGeoPolygon(indexPatterns as IndexPatterns);
- });
-
test('should return the key and value for matching filters with bounds', async () => {
const filter = {
meta: {
@@ -40,17 +31,20 @@ describe('filter manager utilities', () => {
points: [{ lat: 5, lon: 10 }, { lat: 15, lon: 20 }],
},
},
- };
+ } as GeoPolygonFilter;
- const result = await mapGeoPolygonFn(filter);
+ const result = mapGeoPolygon(filter);
expect(result).toHaveProperty('key', 'point');
expect(result).toHaveProperty('value');
- // remove html entities and non-alphanumerics to get the gist of the value
- expect(result.value.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe(
- 'lat5lon10lat15lon20'
- );
+ if (result.value) {
+ const displayName = result.value();
+ // remove html entities and non-alphanumerics to get the gist of the value
+ expect(displayName.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe(
+ 'lat5lon10lat15lon20'
+ );
+ }
});
test('should return the key and value even when using ignore_unmapped', async () => {
@@ -64,26 +58,29 @@ describe('filter manager utilities', () => {
points: [{ lat: 5, lon: 10 }, { lat: 15, lon: 20 }],
},
},
- };
- const result = await mapGeoPolygonFn(filter);
+ } as GeoPolygonFilter;
+ const result = mapGeoPolygon(filter);
expect(result).toHaveProperty('key', 'point');
expect(result).toHaveProperty('value');
- // remove html entities and non-alphanumerics to get the gist of the value
- expect(result.value.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe(
- 'lat5lon10lat15lon20'
- );
+ if (result.value) {
+ const displayName = result.value();
+ // remove html entities and non-alphanumerics to get the gist of the value
+ expect(displayName.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).toBe(
+ 'lat5lon10lat15lon20'
+ );
+ }
});
test('should return undefined for none matching', async done => {
const filter = {
meta: { index: 'logstash-*' },
query: { query_string: { query: 'foo:bar' } },
- };
+ } as Filter;
try {
- await mapGeoPolygonFn(filter);
+ mapGeoPolygon(filter);
} catch (e) {
expect(e).toBe(filter);
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.ts
index b49e0c7314228..03ce4130d0c97 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_geo_polygon.ts
@@ -16,21 +16,25 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { get } from 'lodash';
-import { GeoPolygonFilter, Filter, FILTERS, isGeoPolygonFilter } from '@kbn/es-query';
-import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/public';
-import { IndexPatterns, IndexPattern } from '../../../index_patterns';
+import {
+ GeoPolygonFilter,
+ Filter,
+ FILTERS,
+ isGeoPolygonFilter,
+ FilterValueFormatter,
+} from '@kbn/es-query';
const POINTS_SEPARATOR = ', ';
-const getFormattedValue = (value: any, key: string, indexPattern?: IndexPattern) => {
- const formatter: any =
- indexPattern && key && get(indexPattern, ['fields', 'byName', key, 'format']);
-
- return formatter ? formatter.convert(value) : JSON.stringify(value);
+const getFormattedValueFn = (points: string[]) => {
+ return (formatter?: FilterValueFormatter) => {
+ return points
+ .map((point: string) => (formatter ? formatter.convert(point) : JSON.stringify(point)))
+ .join(POINTS_SEPARATOR);
+ };
};
-function getParams(filter: GeoPolygonFilter, indexPattern?: IndexPattern) {
+function getParams(filter: GeoPolygonFilter) {
const key = Object.keys(filter.geo_polygon).filter(k => k !== 'ignore_unmapped')[0];
const params = filter.geo_polygon[key];
@@ -38,31 +42,13 @@ function getParams(filter: GeoPolygonFilter, indexPattern?: IndexPattern) {
key,
params,
type: FILTERS.GEO_POLYGON,
- value: (params.points || [])
- .map((point: string) => getFormattedValue(point, key, indexPattern))
- .join(POINTS_SEPARATOR),
+ value: getFormattedValueFn(params.points || []),
};
}
-export function mapGeoPolygon(indexPatterns: IndexPatterns) {
- return async function(filter: Filter) {
- if (!isGeoPolygonFilter(filter)) {
- throw filter;
- }
-
- try {
- let indexPattern;
-
- if (filter.meta.index) {
- indexPattern = await indexPatterns.get(filter.meta.index);
- }
-
- return getParams(filter, indexPattern);
- } catch (error) {
- if (error instanceof SavedObjectNotFound) {
- return getParams(filter);
- }
- throw error;
- }
- };
+export function mapGeoPolygon(filter: Filter) {
+ if (!isGeoPolygonFilter(filter)) {
+ throw filter;
+ }
+ return getParams(filter);
}
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.test.ts
index 1139f98f51335..2f0641598a2ce 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.test.ts
@@ -41,7 +41,7 @@ describe('filter_manager/lib', () => {
delete filter.match_all;
try {
- await mapMatchAll(filter);
+ mapMatchAll(filter);
} catch (e) {
expect(e).toBe(filter);
done();
@@ -51,13 +51,13 @@ describe('filter_manager/lib', () => {
describe('when given a match_all filter', () => {
test('key is set to meta field', async () => {
- const result = await mapMatchAll(filter);
+ const result = mapMatchAll(filter);
expect(result).toHaveProperty('key', filter.meta.field);
});
test('value is set to meta formattedValue', async () => {
- const result = await mapMatchAll(filter);
+ const result = mapMatchAll(filter);
expect(result).toHaveProperty('value', filter.meta.formattedValue);
});
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.ts
index a08b60127ac05..a1387e6dbe457 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_match_all.ts
@@ -18,7 +18,7 @@
*/
import { Filter, FILTERS, isMatchAllFilter } from '@kbn/es-query';
-export const mapMatchAll = async (filter: Filter) => {
+export const mapMatchAll = (filter: Filter) => {
if (isMatchAllFilter(filter)) {
return {
type: FILTERS.MATCH_ALL,
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.test.ts
index d8f7faa8cd9d9..ca23f25826906 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.test.ts
@@ -26,7 +26,7 @@ describe('filter manager utilities', () => {
missing: { field: '_type' },
...buildEmptyFilter(true),
};
- const result = await mapMissing(filter);
+ const result = mapMissing(filter);
expect(result).toHaveProperty('key', '_type');
expect(result).toHaveProperty('value', 'missing');
@@ -36,7 +36,7 @@ describe('filter manager utilities', () => {
const filter = buildEmptyFilter(true) as ExistsFilter;
try {
- await mapMissing(filter);
+ mapMissing(filter);
} catch (e) {
expect(e).toBe(filter);
done();
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.ts
index e398917b7f9be..861a84ed61646 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_missing.ts
@@ -18,7 +18,7 @@
*/
import { Filter, FILTERS, isMissingFilter } from '@kbn/es-query';
-export const mapMissing = async (filter: Filter) => {
+export const mapMissing = (filter: Filter) => {
if (isMissingFilter(filter)) {
return {
type: FILTERS.MISSING,
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.test.ts
index e3eae169e3607..c95a2529add14 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.test.ts
@@ -17,38 +17,33 @@
* under the License.
*/
import { mapPhrase } from './map_phrase';
-import { StubIndexPatterns } from '../test_helpers/stub_index_pattern';
-import { IndexPatterns } from '../../../index_patterns';
+import { PhraseFilter, Filter } from '@kbn/es-query';
describe('filter manager utilities', () => {
describe('mapPhrase()', () => {
- let mapPhraseFn: Function;
-
- beforeEach(() => {
- const indexPatterns: unknown = new StubIndexPatterns();
-
- mapPhraseFn = mapPhrase(indexPatterns as IndexPatterns);
- });
-
test('should return the key and value for matching filters', async () => {
const filter = {
meta: { index: 'logstash-*' },
query: { match: { _type: { query: 'apache', type: 'phrase' } } },
- };
- const result = await mapPhraseFn(filter);
+ } as PhraseFilter;
+ const result = mapPhrase(filter);
+ expect(result).toHaveProperty('value');
expect(result).toHaveProperty('key', '_type');
- expect(result).toHaveProperty('value', 'apache');
+ if (result.value) {
+ const displayName = result.value();
+ expect(displayName).toBe('apache');
+ }
});
test('should return undefined for none matching', async done => {
const filter = {
meta: { index: 'logstash-*' },
query: { query_string: { query: 'foo:bar' } },
- };
+ } as Filter;
try {
- await mapPhraseFn(filter);
+ mapPhrase(filter);
} catch (e) {
expect(e).toBe(filter);
done();
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.ts
index 3ebe363f46df5..4f6ff9e0d9199 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrase.ts
@@ -24,21 +24,19 @@ import {
FILTERS,
isPhraseFilter,
isScriptedPhraseFilter,
+ FilterValueFormatter,
} from '@kbn/es-query';
-import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/public';
-import { IndexPatterns, IndexPattern } from '../../../index_patterns';
const getScriptedPhraseValue = (filter: PhraseFilter) =>
get(filter, ['script', 'script', 'params', 'value']);
-const getFormattedValue = (value: any, key: string, indexPattern?: IndexPattern) => {
- const formatter: any =
- indexPattern && key && get(indexPattern, ['fields', 'byName', key, 'format']);
-
- return formatter ? formatter.convert(value) : value;
+const getFormattedValueFn = (value: any) => {
+ return (formatter?: FilterValueFormatter) => {
+ return formatter ? formatter.convert(value) : value;
+ };
};
-const getParams = (filter: PhraseFilter, indexPattern?: IndexPattern) => {
+const getParams = (filter: PhraseFilter) => {
const scriptedPhraseValue = getScriptedPhraseValue(filter);
const isScriptedFilter = Boolean(scriptedPhraseValue);
const key = isScriptedFilter ? filter.meta.field || '' : Object.keys(filter.query.match)[0];
@@ -49,32 +47,17 @@ const getParams = (filter: PhraseFilter, indexPattern?: IndexPattern) => {
key,
params,
type: FILTERS.PHRASE,
- value: getFormattedValue(query, key, indexPattern),
+ value: getFormattedValueFn(query),
};
};
export const isMapPhraseFilter = (filter: any): filter is PhraseFilter =>
isPhraseFilter(filter) || isScriptedPhraseFilter(filter);
-export const mapPhrase = (indexPatterns: IndexPatterns) => {
- return async (filter: Filter) => {
- if (!isMapPhraseFilter(filter)) {
- throw filter;
- }
-
- try {
- let indexPattern;
+export const mapPhrase = (filter: Filter) => {
+ if (!isMapPhraseFilter(filter)) {
+ throw filter;
+ }
- if (filter.meta.index) {
- indexPattern = await indexPatterns.get(filter.meta.index);
- }
-
- return getParams(filter, indexPattern);
- } catch (error) {
- if (error instanceof SavedObjectNotFound) {
- return getParams(filter);
- }
- throw error;
- }
- };
+ return getParams(filter);
};
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrases.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrases.ts
index 92226b68ef326..c17ff11d49fd4 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrases.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_phrases.ts
@@ -19,7 +19,7 @@
import { Filter, isPhrasesFilter } from '@kbn/es-query';
-export const mapPhrases = async (filter: Filter) => {
+export const mapPhrases = (filter: Filter) => {
if (!isPhrasesFilter(filter)) {
throw filter;
}
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.test.ts
index ebf9f05ba387b..4b1a5d39c405d 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.test.ts
@@ -27,7 +27,7 @@ describe('filter manager utilities', () => {
{ query_string: { query: 'foo:bar' } },
'index'
);
- const result = await mapQueryString(filter);
+ const result = mapQueryString(filter);
expect(result).toHaveProperty('key', 'query');
expect(result).toHaveProperty('value', 'foo:bar');
@@ -37,7 +37,7 @@ describe('filter manager utilities', () => {
const filter = buildEmptyFilter(true) as QueryStringFilter;
try {
- await mapQueryString(filter);
+ mapQueryString(filter);
} catch (e) {
expect(e).toBe(filter);
done();
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.ts
index 14ad52beaf0b3..94da8074edd04 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_query_string.ts
@@ -18,7 +18,7 @@
*/
import { Filter, FILTERS, isQueryStringFilter } from '@kbn/es-query';
-export const mapQueryString = async (filter: Filter) => {
+export const mapQueryString = (filter: Filter) => {
if (isQueryStringFilter(filter)) {
return {
type: FILTERS.QUERY_STRING,
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.test.ts
index 4f81572183005..12d2919e2d47b 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.test.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.test.ts
@@ -18,43 +18,48 @@
*/
import { mapRange } from './map_range';
-import { StubIndexPatterns } from '../test_helpers/stub_index_pattern';
-import { IndexPatterns } from '../../../index_patterns';
+import { RangeFilter, Filter, FilterMeta } from '@kbn/es-query';
describe('filter manager utilities', () => {
describe('mapRange()', () => {
- let mapRangeFn: Function;
-
- beforeEach(() => {
- const indexPatterns: unknown = new StubIndexPatterns();
-
- mapRangeFn = mapRange(indexPatterns as IndexPatterns);
- });
-
test('should return the key and value for matching filters with gt/lt', async () => {
- const filter = { meta: { index: 'logstash-*' }, range: { bytes: { lt: 2048, gt: 1024 } } };
- const result = await mapRangeFn(filter);
+ const filter = {
+ meta: { index: 'logstash-*' } as FilterMeta,
+ range: { bytes: { lt: 2048, gt: 1024 } },
+ } as RangeFilter;
+ const result = mapRange(filter);
expect(result).toHaveProperty('key', 'bytes');
- expect(result).toHaveProperty('value', '1024 to 2048');
+ expect(result).toHaveProperty('value');
+ if (result.value) {
+ const displayName = result.value();
+ expect(displayName).toBe('1024 to 2048');
+ }
});
test('should return the key and value for matching filters with gte/lte', async () => {
- const filter = { meta: { index: 'logstash-*' }, range: { bytes: { lte: 2048, gte: 1024 } } };
- const result = await mapRangeFn(filter);
+ const filter = {
+ meta: { index: 'logstash-*' } as FilterMeta,
+ range: { bytes: { lte: 2048, gte: 1024 } },
+ } as RangeFilter;
+ const result = mapRange(filter);
expect(result).toHaveProperty('key', 'bytes');
- expect(result).toHaveProperty('value', '1024 to 2048');
+ expect(result).toHaveProperty('value');
+ if (result.value) {
+ const displayName = result.value();
+ expect(displayName).toBe('1024 to 2048');
+ }
});
test('should return undefined for none matching', async done => {
const filter = {
meta: { index: 'logstash-*' },
query: { query_string: { query: 'foo:bar' } },
- };
+ } as Filter;
try {
- await mapRangeFn(filter);
+ mapRange(filter);
} catch (e) {
expect(e).toBe(filter);
diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.ts
index 7b85adacce1d0..76f9d3621e171 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_manager/lib/map_range.ts
@@ -17,15 +17,31 @@
* under the License.
*/
-import { Filter, RangeFilter, FILTERS, isRangeFilter, isScriptedRangeFilter } from '@kbn/es-query';
+import {
+ Filter,
+ RangeFilter,
+ FILTERS,
+ isRangeFilter,
+ isScriptedRangeFilter,
+ FilterValueFormatter,
+} from '@kbn/es-query';
import { get, has } from 'lodash';
-import { SavedObjectNotFound } from '../../../../../../../plugins/kibana_utils/public';
-import { IndexPatterns, IndexPattern, Field } from '../../../index_patterns';
+
+const getFormattedValueFn = (left: any, right: any) => {
+ return (formatter?: FilterValueFormatter) => {
+ let displayValue = `${left} to ${right}`;
+ if (formatter) {
+ const convert = formatter.getConverterFor('text');
+ displayValue = `${convert(left)} to ${convert(right)}`;
+ }
+ return displayValue;
+ };
+};
const getFirstRangeKey = (filter: RangeFilter) => filter.range && Object.keys(filter.range)[0];
const getRangeByKey = (filter: RangeFilter, key: string) => get(filter, ['range', key]);
-function getParams(filter: RangeFilter, indexPattern?: IndexPattern) {
+function getParams(filter: RangeFilter) {
const isScriptedRange = isScriptedRangeFilter(filter);
const key: string = (isScriptedRange ? filter.meta.field : getFirstRangeKey(filter)) || '';
const params: any = isScriptedRange
@@ -38,16 +54,7 @@ function getParams(filter: RangeFilter, indexPattern?: IndexPattern) {
let right = has(params, 'lte') ? params.lte : params.lt;
if (right == null) right = Infinity;
- let value = `${left} to ${right}`;
-
- // Sometimes a filter will end up with an invalid index param. This could happen for a lot of reasons,
- // for example a user might manually edit the url or the index pattern's ID might change due to
- // external factors e.g. a reindex. We only need the index in order to grab the field formatter, so we fallback
- // on displaying the raw value if the index is invalid.
- if (key && indexPattern && indexPattern.fields.getByName(key)) {
- const convert = (indexPattern.fields.getByName(key) as Field).format.getConverterFor('text');
- value = `${convert(left)} to ${convert(right)}`;
- }
+ const value = getFormattedValueFn(left, right);
return { type: FILTERS.RANGE, key, value, params };
}
@@ -55,25 +62,10 @@ function getParams(filter: RangeFilter, indexPattern?: IndexPattern) {
export const isMapRangeFilter = (filter: any): filter is RangeFilter =>
isRangeFilter(filter) || isScriptedRangeFilter(filter);
-export const mapRange = (indexPatterns: IndexPatterns) => {
- return async (filter: Filter) => {
- if (!isMapRangeFilter(filter)) {
- throw filter;
- }
-
- try {
- let indexPattern;
-
- if (filter.meta.index) {
- indexPattern = await indexPatterns.get(filter.meta.index);
- }
+export const mapRange = (filter: Filter) => {
+ if (!isMapRangeFilter(filter)) {
+ throw filter;
+ }
- return getParams(filter, indexPattern);
- } catch (error) {
- if (error instanceof SavedObjectNotFound) {
- return getParams(filter);
- }
- throw error;
- }
- };
+ return getParams(filter);
};
diff --git a/src/legacy/core_plugins/data/public/filter/filter_service.mock.ts b/src/legacy/core_plugins/data/public/filter/filter_service.mock.ts
index 5cacc890cd5cc..94268ef69c49a 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_service.mock.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_service.mock.ts
@@ -17,11 +17,17 @@
* under the License.
*/
-import { FilterService, FilterStart } from '.';
+import { FilterService, FilterStart, FilterSetup } from '.';
type FilterServiceClientContract = PublicMethodsOf;
-const createSetupContract = () => {};
+const createSetupContractMock = () => {
+ const setupContract: jest.Mocked = {
+ filterManager: jest.fn() as any,
+ };
+
+ return setupContract;
+};
const createStartContractMock = () => {
const startContract: jest.Mocked = {
@@ -38,13 +44,13 @@ const createMock = () => {
stop: jest.fn(),
};
- mocked.setup.mockReturnValue(createSetupContract());
+ mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createStartContractMock());
return mocked;
};
export const filterServiceMock = {
create: createMock,
- createSetupContract,
+ createSetupContract: createSetupContractMock,
createStartContract: createStartContractMock,
};
diff --git a/src/legacy/core_plugins/data/public/filter/filter_service.ts b/src/legacy/core_plugins/data/public/filter/filter_service.ts
index fbeab1233533e..0c46259ef0e00 100644
--- a/src/legacy/core_plugins/data/public/filter/filter_service.ts
+++ b/src/legacy/core_plugins/data/public/filter/filter_service.ts
@@ -18,7 +18,6 @@
*/
import { UiSettingsClientContract } from 'src/core/public';
-import { IndexPatterns } from '../index_patterns';
import { FilterManager } from './filter_manager';
/**
@@ -27,18 +26,23 @@ import { FilterManager } from './filter_manager';
*/
export interface FilterServiceDependencies {
- indexPatterns: IndexPatterns;
uiSettings: UiSettingsClientContract;
}
export class FilterService {
- public setup() {
- // Filter service requires index patterns, which are only available in `start`
+ filterManager!: FilterManager;
+
+ public setup({ uiSettings }: FilterServiceDependencies) {
+ this.filterManager = new FilterManager(uiSettings);
+
+ return {
+ filterManager: this.filterManager,
+ };
}
- public start({ indexPatterns, uiSettings }: FilterServiceDependencies) {
+ public start() {
return {
- filterManager: new FilterManager(indexPatterns, uiSettings),
+ filterManager: this.filterManager,
};
}
@@ -48,4 +52,5 @@ export class FilterService {
}
/** @public */
+export type FilterSetup = ReturnType;
export type FilterStart = ReturnType;
diff --git a/src/legacy/core_plugins/data/public/filter/index.tsx b/src/legacy/core_plugins/data/public/filter/index.tsx
index 3ada13ba2ceea..cda7350ecadef 100644
--- a/src/legacy/core_plugins/data/public/filter/index.tsx
+++ b/src/legacy/core_plugins/data/public/filter/index.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-export { FilterService, FilterStart } from './filter_service';
+export * from './filter_service';
export { FilterBar } from './filter_bar';
diff --git a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx b/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx
index c6f3a6e35bc43..77692d7bcaa0d 100644
--- a/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx
+++ b/src/legacy/core_plugins/data/public/index_patterns/components/index_pattern_select.tsx
@@ -22,6 +22,7 @@ import React, { Component } from 'react';
import { EuiComboBox } from '@elastic/eui';
import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../../core/public';
+import { getIndexPatternTitle } from '../utils';
interface IndexPatternSelectProps {
onChange: (opt: any) => void;
@@ -54,17 +55,6 @@ const getIndexPatterns = async (
return resp.savedObjects;
};
-const getIndexPatternTitle = async (
- client: SavedObjectsClientContract,
- indexPatternId: string
-): Promise> => {
- const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject;
- if (savedObject.error) {
- throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`);
- }
- return savedObject.attributes.title;
-};
-
// Takes in stateful runtime dependencies and pre-wires them to the component
export function createIndexPatternSelect(savedObjectsClient: SavedObjectsClientContract) {
return (props: Omit) => (
diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts
index a5e02e43adf56..35ec4bad68e14 100644
--- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts
+++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.ts
@@ -23,10 +23,10 @@ import { i18n } from '@kbn/i18n';
import { fieldFormats } from 'ui/registry/field_formats';
// @ts-ignore
import { expandShorthand } from 'ui/utils/mapping_setup';
-import { findObjectByTitle } from 'ui/saved_objects';
+
import { NotificationsSetup, SavedObjectsClientContract } from 'src/core/public';
import { SavedObjectNotFound, DuplicateField } from '../../../../../../plugins/kibana_utils/public';
-
+import { findIndexPatternByTitle } from '../utils';
import { IndexPatternMissingIndices } from '../errors';
import { Field, FieldList, FieldType, FieldListInterface } from '../fields';
import { createFieldsFetcher } from './_fields_fetcher';
@@ -387,9 +387,8 @@ export class IndexPattern implements StaticIndexPattern {
return response.id;
};
- const potentialDuplicateByTitle = await findObjectByTitle(
+ const potentialDuplicateByTitle = await findIndexPatternByTitle(
this.savedObjectsClient,
- type,
this.title
);
// If there is potentially duplicate title, just create it
diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts
index d2deaa92e2b40..f573f33ef7a52 100644
--- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts
+++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts
@@ -36,12 +36,6 @@ jest.mock('ui/registry/field_formats', () => ({
},
}));
-jest.mock('ui/notify', () => ({
- toastNotifications: {
- addDanger: jest.fn(),
- },
-}));
-
jest.mock('./index_pattern', () => {
class IndexPattern {
init = async () => {
diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts
index 17a0f45865d11..562dcb248edae 100644
--- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts
+++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts
@@ -108,3 +108,6 @@ export type IndexPatternsStart = ReturnType;
/** @public */
export { IndexPattern, IndexPatterns, StaticIndexPattern, Field, FieldType, FieldListInterface };
+
+/** @public */
+export { getIndexPatternTitle, findIndexPatternByTitle } from './utils';
diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.ts b/src/legacy/core_plugins/data/public/index_patterns/utils.ts
index 237b8386e7561..62f5ddbe9e2b0 100644
--- a/src/legacy/core_plugins/data/public/index_patterns/utils.ts
+++ b/src/legacy/core_plugins/data/public/index_patterns/utils.ts
@@ -17,12 +17,14 @@
* under the License.
*/
-import { get } from 'lodash';
+import { find, get } from 'lodash';
import { Field, FieldType } from './fields';
import { StaticIndexPattern } from './index_patterns';
import { getFilterableKbnTypeNames } from '../../../../../plugins/data/public';
+import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public';
+
export const ILLEGAL_CHARACTERS = 'ILLEGAL_CHARACTERS';
export const CONTAINS_SPACES = 'CONTAINS_SPACES';
export const INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE = ['\\', '/', '?', '"', '<', '>', '|'];
@@ -44,6 +46,48 @@ function findIllegalCharacters(indexPattern: string): string[] {
return illegalCharacters;
}
+/**
+ * Returns an object matching a given title
+ *
+ * @param client {SavedObjectsClientContract}
+ * @param title {string}
+ * @returns {Promise}
+ */
+export async function findIndexPatternByTitle(
+ client: SavedObjectsClientContract,
+ title: string
+): Promise | void> {
+ if (!title) {
+ return Promise.resolve();
+ }
+
+ const { savedObjects } = await client.find({
+ type: 'index-pattern',
+ perPage: 10,
+ search: `"${title}"`,
+ searchFields: ['title'],
+ fields: ['title'],
+ });
+
+ return find(
+ savedObjects,
+ (obj: SimpleSavedObject) => obj.get('title').toLowerCase() === title.toLowerCase()
+ );
+}
+
+export async function getIndexPatternTitle(
+ client: SavedObjectsClientContract,
+ indexPatternId: string
+): Promise> {
+ const savedObject = (await client.get('index-pattern', indexPatternId)) as SimpleSavedObject;
+
+ if (savedObject.error) {
+ throw new Error(`Unable to get index-pattern title: ${savedObject.error.message}`);
+ }
+
+ return savedObject.attributes.title;
+}
+
function indexPatternContainsSpaces(indexPattern: string): boolean {
return indexPattern.includes(' ');
}
diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts
index 6f9660d986e79..c25d6742db8da 100644
--- a/src/legacy/core_plugins/data/public/plugin.ts
+++ b/src/legacy/core_plugins/data/public/plugin.ts
@@ -20,7 +20,7 @@
import { CoreSetup, CoreStart, Plugin } from 'kibana/public';
import { SearchService, SearchStart, createSearchBar, StatetfulSearchBarProps } from './search';
import { QueryService, QuerySetup } from './query';
-import { FilterService, FilterStart } from './filter';
+import { FilterService, FilterSetup, FilterStart } from './filter';
import { TimefilterService, TimefilterSetup } from './timefilter';
import { IndexPatternsService, IndexPatternsSetup, IndexPatternsStart } from './index_patterns';
import {
@@ -52,6 +52,7 @@ export interface DataSetup {
query: QuerySetup;
timefilter: TimefilterSetup;
indexPatterns: IndexPatternsSetup;
+ filter: FilterSetup;
}
/**
@@ -100,10 +101,14 @@ export class DataPlugin
uiSettings,
store: __LEGACY.storage,
});
+ const filterService = this.filter.setup({
+ uiSettings,
+ });
this.setupApi = {
indexPatterns: this.indexPatterns.setup(),
query: this.query.setup(),
timefilter: timefilterService,
+ filter: filterService,
};
return this.setupApi;
@@ -119,23 +124,17 @@ export class DataPlugin
notifications,
});
- const filterService = this.filter.start({
- uiSettings,
- indexPatterns: indexPatternsService.indexPatterns,
- });
-
const SearchBar = createSearchBar({
core,
data,
store: __LEGACY.storage,
timefilter: this.setupApi.timefilter,
- filterManager: filterService.filterManager,
+ filterManager: this.setupApi.filter.filterManager,
});
return {
...this.setupApi!,
indexPatterns: indexPatternsService,
- filter: filterService,
search: this.search.start(savedObjects.client),
ui: {
SearchBar,
diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx
index 3683fb7c2c16e..8c7441088bab0 100644
--- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx
+++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
+import { compact } from 'lodash';
import { Filter } from '@kbn/es-query';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import classNames from 'classnames';
@@ -195,7 +196,12 @@ class SearchBarUI extends Component {
}
private shouldRenderFilterBar() {
- return this.props.showFilterBar && this.props.filters && this.props.indexPatterns;
+ return (
+ this.props.showFilterBar &&
+ this.props.filters &&
+ this.props.indexPatterns &&
+ compact(this.props.indexPatterns).length > 0
+ );
}
public setFilterBarHeight = () => {
diff --git a/src/legacy/core_plugins/data/public/shim/apply_filter_directive.html b/src/legacy/core_plugins/data/public/shim/apply_filter_directive.html
index ed7a5d70a2b80..0b02fcefcdbd5 100644
--- a/src/legacy/core_plugins/data/public/shim/apply_filter_directive.html
+++ b/src/legacy/core_plugins/data/public/shim/apply_filter_directive.html
@@ -4,4 +4,5 @@
filters="state.filters"
on-cancel="onCancel"
on-submit="onSubmit"
+ index-patterns="indexPatterns"
>
diff --git a/src/legacy/core_plugins/data/public/shim/legacy_module.ts b/src/legacy/core_plugins/data/public/shim/legacy_module.ts
index 32b88720a4579..9ce35e6d2fa9e 100644
--- a/src/legacy/core_plugins/data/public/shim/legacy_module.ts
+++ b/src/legacy/core_plugins/data/public/shim/legacy_module.ts
@@ -79,7 +79,7 @@ export const initLegacyModule = once((): void => {
.directive('applyFiltersPopoverComponent', (reactDirective: any) =>
reactDirective(wrapInI18nContext(ApplyFiltersPopover))
)
- .directive('applyFiltersPopover', (indexPatterns: IndexPatterns) => {
+ .directive('applyFiltersPopover', () => {
return {
template,
restrict: 'E',
@@ -87,6 +87,7 @@ export const initLegacyModule = once((): void => {
filters: '=',
onCancel: '=',
onSubmit: '=',
+ indexPatterns: '=',
},
link($scope: any) {
$scope.state = {};
@@ -94,8 +95,8 @@ export const initLegacyModule = once((): void => {
// Each time the new filters change we want to rebuild (not just re-render) the "apply filters"
// popover, because it has to reset its state whenever the new filters change. Setting a `key`
// property on the component accomplishes this due to how React handles the `key` property.
- $scope.$watch('filters', async (filters: any) => {
- const mappedFilters: Filter[] = await mapAndFlattenFilters(indexPatterns, filters);
+ $scope.$watch('filters', (filters: any) => {
+ const mappedFilters: Filter[] = mapAndFlattenFilters(filters);
$scope.state = {
filters: mappedFilters,
key: Date.now(),
@@ -107,7 +108,7 @@ export const initLegacyModule = once((): void => {
const module = uiModules.get('kibana/index_patterns');
let _service: any;
- module.service('indexPatterns', function(chrome: any) {
+ module.service('indexPatterns', function() {
if (!_service)
_service = new IndexPatterns(
npStart.core.uiSettings,
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html
index 39db357a69321..68c8131fa1a7b 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html
+++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html
@@ -46,6 +46,7 @@
filters="appState.$newFilters"
on-cancel="onCancelApplyFilters"
on-submit="onApplyFilters"
+ index-patterns="indexPatterns"
>
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx
index de32d060b6cdb..60004374eb33e 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx
+++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx
@@ -258,7 +258,7 @@ export class DashboardAppController {
updateIndexPatterns(dashboardContainer);
});
- inputSubscription = dashboardContainer.getInput$().subscribe(async () => {
+ inputSubscription = dashboardContainer.getInput$().subscribe(() => {
let dirty = false;
// This has to be first because handleDashboardContainerChanges causes
@@ -266,7 +266,7 @@ export class DashboardAppController {
// Add filters modifies the object passed to it, hence the clone deep.
if (!_.isEqual(container.getInput().filters, queryFilter.getFilters())) {
- await queryFilter.addFilters(_.cloneDeep(container.getInput().filters));
+ queryFilter.addFilters(_.cloneDeep(container.getInput().filters));
dashboardStateManager.applyFilters($scope.model.query, container.getInput().filters);
dirty = true;
@@ -453,7 +453,6 @@ export class DashboardAppController {
$scope.onClearSavedQuery = () => {
delete $scope.savedQuery;
dashboardStateManager.setSavedQueryId(undefined);
- queryFilter.removeAll();
dashboardStateManager.applyFilters(
{
query: '',
@@ -462,10 +461,12 @@ export class DashboardAppController {
},
[]
);
+ // Making this method sync broke the updates.
+ // Temporary fix, until we fix the complex state in this file.
+ setTimeout(queryFilter.removeAll, 0);
};
const updateStateFromSavedQuery = (savedQuery: SavedQuery) => {
- queryFilter.setFilters(savedQuery.attributes.filters || []);
dashboardStateManager.applyFilters(
savedQuery.attributes.query,
savedQuery.attributes.filters || []
@@ -479,6 +480,11 @@ export class DashboardAppController {
timefilter.setRefreshInterval(savedQuery.attributes.timefilter.refreshInterval);
}
}
+ // Making this method sync broke the updates.
+ // Temporary fix, until we fix the complex state in this file.
+ setTimeout(() => {
+ queryFilter.setFilters(savedQuery.attributes.filters || []);
+ }, 0);
};
$scope.$watch('savedQuery', (newSavedQuery: SavedQuery) => {
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts
index 3b6b99dcb6d25..1fd50081c58bd 100644
--- a/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts
+++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/filter_utils.ts
@@ -66,6 +66,14 @@ export class FilterUtils {
* @returns {Array.
}
*/
public static cleanFiltersForComparison(filters: Filter[]) {
- return _.map(filters, filter => _.omit(filter, ['$$hashKey', '$state']));
+ return _.map(filters, filter => {
+ const f: Partial = _.omit(filter, ['$$hashKey', '$state']);
+ if (f.meta) {
+ // f.meta.value is the value displayed in the filter bar.
+ // It may also be loaded differently and shouldn't be used in this comparison.
+ return _.omit(f.meta, ['value']);
+ }
+ return f;
+ });
}
}
diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html
index aa59b2d0a81c4..50e921e14973b 100644
--- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html
+++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html
@@ -77,6 +77,7 @@
filters="state.$newFilters"
on-cancel="onCancelApplyFilters"
on-submit="onApplyFilters"
+ index-patterns="indexPatterns"
>
class VegaVisualization {
constructor(el, vis) {
this.savedObjectsClient = chrome.getSavedObjectsClient();
@@ -40,7 +41,7 @@ export const createVegaVisualization = ({ serviceSettings }) => class VegaVisual
async findIndex(index) {
let idxObj;
if (index) {
- idxObj = await findObjectByTitle(this.savedObjectsClient, 'index-pattern', index);
+ idxObj = await findIndexPatternByTitle(this.savedObjectsClient, index);
if (!idxObj) {
throw new Error(i18n.translate('visTypeVega.visualization.indexNotFoundErrorMessage', {
defaultMessage: 'Index {index} not found',
diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts
index d12ac08e273c0..f5771c6b86d9a 100644
--- a/src/legacy/server/kbn_server.d.ts
+++ b/src/legacy/server/kbn_server.d.ts
@@ -24,9 +24,11 @@ import { SavedObjectsClientProviderOptions, CoreSetup } from 'src/core/server';
import {
ConfigService,
ElasticsearchServiceSetup,
+ EnvironmentMode,
LoggerFactory,
SavedObjectsClientContract,
SavedObjectsLegacyService,
+ PackageInfo,
} from '../../core/server';
import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from '../../core/server/';
@@ -102,6 +104,10 @@ type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promi
// eslint-disable-next-line import/no-default-export
export default class KbnServer {
public readonly newPlatform: {
+ env: {
+ mode: Readonly
;
+ packageInfo: Readonly;
+ };
coreContext: {
logger: LoggerFactory;
};
diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js
index 56539f409a34c..f7ed56b10c267 100644
--- a/src/legacy/server/kbn_server.js
+++ b/src/legacy/server/kbn_server.js
@@ -57,10 +57,14 @@ export default class KbnServer {
this.settings = settings || {};
this.config = config;
- const { setupDeps, startDeps, handledConfigPaths, logger, __internals } = core;
+ const { setupDeps, startDeps, handledConfigPaths, logger, __internals, env } = core;
this.server = __internals.hapiServer;
this.newPlatform = {
+ env: {
+ mode: env.mode,
+ packageInfo: env.packageInfo,
+ },
__internals,
coreContext: {
logger,
diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js
index 68cfd756acf6b..883358e0d3c9a 100644
--- a/src/legacy/ui/ui_render/ui_render_mixin.js
+++ b/src/legacy/ui/ui_render/ui_render_mixin.js
@@ -237,6 +237,7 @@ export function uiRenderMixin(kbnServer, server, config) {
buildNumber: config.get('pkg.buildNum'),
branch: config.get('pkg.branch'),
basePath,
+ env: kbnServer.newPlatform.env,
legacyMode: app.getId() !== 'core',
i18n: {
translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`,
diff --git a/src/optimize/base_optimizer.js b/src/optimize/base_optimizer.js
index f88e05ab91dde..0e7d03498c0aa 100644
--- a/src/optimize/base_optimizer.js
+++ b/src/optimize/base_optimizer.js
@@ -479,8 +479,10 @@ export default class BaseOptimizer {
optimization: {
minimizer: [
new TerserPlugin({
- parallel: true,
+ parallel: this.getThreadLoaderPoolConfig().workers,
sourceMap: false,
+ cache: false,
+ extractComments: false,
terserOptions: {
compress: false,
mangle: false
diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js
index 46aa472b5ae56..cb1f9be9b16c9 100644
--- a/src/optimize/dynamic_dll_plugin/dll_config_model.js
+++ b/src/optimize/dynamic_dll_plugin/dll_config_model.js
@@ -224,6 +224,8 @@ function optimized(config) {
// the parallel processes on terser
parallel: config.threadLoaderPoolConfig.workers,
sourceMap: false,
+ cache: false,
+ extractComments: false,
terserOptions: {
compress: {
// The following is required for dead-code the removal
diff --git a/src/plugins/data/public/search/es_search/es_search_service.test.ts b/src/plugins/data/public/search/es_search/es_search_service.test.ts
index d1069cd6815fe..d756b54a5cce6 100644
--- a/src/plugins/data/public/search/es_search/es_search_service.test.ts
+++ b/src/plugins/data/public/search/es_search/es_search_service.test.ts
@@ -25,10 +25,9 @@ import { searchSetupMock } from '../mocks';
describe('ES search strategy service', () => {
let service: EsSearchService;
let mockCoreSetup: MockedKeys;
- const opaqueId = Symbol();
beforeEach(() => {
- service = new EsSearchService({ opaqueId });
+ service = new EsSearchService(coreMock.createPluginInitializerContext());
mockCoreSetup = coreMock.createSetup();
});
diff --git a/src/plugins/data/public/search/es_search/index.test.ts b/src/plugins/data/public/search/es_search/index.test.ts
index 7f7a2ed397d43..ecceffc752e9a 100644
--- a/src/plugins/data/public/search/es_search/index.test.ts
+++ b/src/plugins/data/public/search/es_search/index.test.ts
@@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-
+import { coreMock } from '../../../../../../src/core/public/mocks';
import { esSearchService } from '.';
it('es search service is instantiated', () => {
- const esSearch = esSearchService({ opaqueId: Symbol() });
+ const esSearch = esSearchService(coreMock.createPluginInitializerContext());
expect(esSearch).toBeDefined();
});
diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts
index 8102b0e915fb7..69e05e8df48e2 100644
--- a/src/plugins/data/public/search/search_service.test.ts
+++ b/src/plugins/data/public/search/search_service.test.ts
@@ -25,9 +25,9 @@ import { CoreSetup } from '../../../../core/public';
describe('Search service', () => {
let searchService: SearchService;
let mockCoreSetup: MockedKeys;
- const opaqueId = Symbol();
+
beforeEach(() => {
- searchService = new SearchService({ opaqueId });
+ searchService = new SearchService(coreMock.createPluginInitializerContext());
mockCoreSetup = coreMock.createSetup();
});
diff --git a/src/plugins/data/server/search/es_search/es_search_service.test.ts b/src/plugins/data/server/search/es_search/es_search_service.test.ts
index faf9487159c15..0b274c62958a9 100644
--- a/src/plugins/data/server/search/es_search/es_search_service.test.ts
+++ b/src/plugins/data/server/search/es_search/es_search_service.test.ts
@@ -19,31 +19,13 @@
import { coreMock } from '../../../../../core/server/mocks';
import { EsSearchService } from './es_search_service';
-import { PluginInitializerContext } from '../../../../../core/server';
import { searchSetupMock } from '../mocks';
describe('ES search strategy service', () => {
let service: EsSearchService;
const mockCoreSetup = coreMock.createSetup();
- const opaqueId = Symbol();
- const context: PluginInitializerContext = {
- opaqueId,
- config: {
- createIfExists: jest.fn(),
- create: jest.fn(),
- },
- env: {
- mode: {
- dev: false,
- name: 'development',
- prod: false,
- },
- },
- logger: {
- get: jest.fn(),
- },
- };
+ const context = coreMock.createPluginInitializerContext();
beforeEach(() => {
service = new EsSearchService(context);
diff --git a/src/core/server/types.ts~master b/src/plugins/es_ui_shared/public/components/section_loading/index.ts
similarity index 83%
rename from src/core/server/types.ts~master
rename to src/plugins/es_ui_shared/public/components/section_loading/index.ts
index 9b55da17a40a8..6e10fe9a89cd1 100644
--- a/src/core/server/types.ts~master
+++ b/src/plugins/es_ui_shared/public/components/section_loading/index.ts
@@ -17,6 +17,4 @@
* under the License.
*/
-/** This module is intended for consumption by public to avoid import issues with server-side code */
-
-export { PluginOpaqueId } from './plugins/types';
+export { SectionLoading } from './section_loading';
diff --git a/src/plugins/es_ui_shared/public/components/section_loading/section_loading.tsx b/src/plugins/es_ui_shared/public/components/section_loading/section_loading.tsx
new file mode 100644
index 0000000000000..41495d605d768
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/components/section_loading/section_loading.tsx
@@ -0,0 +1,60 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+
+import {
+ EuiEmptyPrompt,
+ EuiLoadingSpinner,
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTextColor,
+} from '@elastic/eui';
+
+interface Props {
+ inline?: boolean;
+ children: React.ReactNode;
+ [key: string]: any;
+}
+
+export const SectionLoading: React.FunctionComponent = ({ inline, children, ...rest }) => {
+ if (inline) {
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+ }
+
+ return (
+ }
+ body={{children} }
+ data-test-subj="sectionLoading"
+ />
+ );
+};
diff --git a/test/plugin_functional/plugins/core_plugin_b/public/index.ts b/test/plugin_functional/plugins/core_plugin_b/public/index.ts
index db3b5c42a964f..31aa7f81c2ac1 100644
--- a/test/plugin_functional/plugins/core_plugin_b/public/index.ts
+++ b/test/plugin_functional/plugins/core_plugin_b/public/index.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { PluginInitializer } from 'kibana/public';
+import { PluginInitializer, PluginInitializerContext } from 'kibana/public';
import {
CorePluginBDeps,
CorePluginBPlugin,
@@ -29,4 +29,4 @@ export const plugin: PluginInitializer<
CorePluginBPluginSetup,
CorePluginBPluginStart,
CorePluginBDeps
-> = () => new CorePluginBPlugin();
+> = (context: PluginInitializerContext) => new CorePluginBPlugin(context);
diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx
index 627fd05404b24..56cc1cb4ab425 100644
--- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx
+++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx
@@ -17,13 +17,14 @@
* under the License.
*/
-import { Plugin, CoreSetup } from 'kibana/public';
+import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/public';
import { CorePluginAPluginSetup } from '../../core_plugin_a/public/plugin';
declare global {
interface Window {
corePluginB?: string;
hasAccessToInjectedMetadata?: boolean;
+ env?: PluginInitializerContext['env'];
}
}
@@ -33,6 +34,9 @@ export interface CorePluginBDeps {
export class CorePluginBPlugin
implements Plugin {
+ constructor(pluginContext: PluginInitializerContext) {
+ window.env = pluginContext.env;
+ }
public setup(core: CoreSetup, deps: CorePluginBDeps) {
window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`;
window.hasAccessToInjectedMetadata = 'getInjectedVar' in core.injectedMetadata;
diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
index ba9a874bcc5d3..5cfaa1c22f4e5 100644
--- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
+++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx
@@ -86,7 +86,7 @@ export class DashboardContainerExample extends React.Component {
}
public switchViewMode = () => {
- this.setState((prevState: State) => {
+ this.setState<'viewMode'>((prevState: State) => {
if (!this.container || isErrorEmbeddable(this.container)) {
return prevState;
}
diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.js b/test/plugin_functional/test_suites/core_plugins/ui_plugins.js
index df855f243d403..15a4dcabddbd1 100644
--- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.js
+++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.js
@@ -44,5 +44,16 @@ export default function ({ getService, getPageObjects }) {
expect(hasAccessToInjectedMetadata).to.equal(true);
});
});
+ describe('have env data provided', function describeIndexTests() {
+ before(async () => {
+ await PageObjects.common.navigateToApp('bar');
+ });
+
+ it('should attach pluginContext to window.corePluginB', async () => {
+ const envData = await browser.execute('return window.env');
+ expect(envData.mode.dev).to.be(true);
+ expect(envData.packageInfo.version).to.be.a('string');
+ });
+ });
});
}
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 1eb5bd752baee..872d4ed9c29d1 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -35,7 +35,7 @@
"xpack.security": "legacy/plugins/security",
"xpack.server": "legacy/server",
"xpack.snapshotRestore": "legacy/plugins/snapshot_restore",
- "xpack.spaces": "legacy/plugins/spaces",
+ "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"],
"xpack.transform": "legacy/plugins/transform",
"xpack.upgradeAssistant": "legacy/plugins/upgrade_assistant",
"xpack.uptime": "legacy/plugins/uptime",
diff --git a/x-pack/legacy/plugins/actions/server/actions_config.ts b/x-pack/legacy/plugins/actions/server/actions_config.ts
index 0d33914920b21..3053c88f1c9ef 100644
--- a/x-pack/legacy/plugins/actions/server/actions_config.ts
+++ b/x-pack/legacy/plugins/actions/server/actions_config.ts
@@ -61,7 +61,7 @@ function isWhitelistedHostnameInUri(config: ActionsConfigType, uri: string): boo
tryCatch(() => new URL(uri)),
map(url => url.hostname),
mapNullable(hostname => isWhitelisted(config, hostname)),
- getOrElse(() => false)
+ getOrElse(() => false)
);
}
diff --git a/x-pack/legacy/plugins/actions/server/lib/action_executor.ts b/x-pack/legacy/plugins/actions/server/lib/action_executor.ts
index 5df2fe082eb13..aef389262f884 100644
--- a/x-pack/legacy/plugins/actions/server/lib/action_executor.ts
+++ b/x-pack/legacy/plugins/actions/server/lib/action_executor.ts
@@ -6,7 +6,7 @@
import Hapi from 'hapi';
import { EncryptedSavedObjectsStartContract } from '../shim';
-import { SpacesPlugin as SpacesPluginStartContract } from '../../../spaces';
+import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../../spaces';
import { Logger } from '../../../../../../src/core/server';
import { validateParams, validateConfig, validateSecrets } from './validate_with_schema';
import {
diff --git a/x-pack/legacy/plugins/actions/server/shim.ts b/x-pack/legacy/plugins/actions/server/shim.ts
index 60873ffed0e8e..c457a40a78b67 100644
--- a/x-pack/legacy/plugins/actions/server/shim.ts
+++ b/x-pack/legacy/plugins/actions/server/shim.ts
@@ -11,7 +11,7 @@ import { ActionsConfigType } from './types';
import { TaskManager } from '../../task_manager';
import { XPackMainPlugin } from '../../xpack_main/xpack_main';
import KbnServer from '../../../../../src/legacy/server/kbn_server';
-import { SpacesPlugin as SpacesPluginStartContract } from '../../spaces';
+import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces';
import { EncryptedSavedObjectsPlugin } from '../../encrypted_saved_objects';
import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server';
import {
diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts
index 78fb0837d1031..c977fda451df1 100644
--- a/x-pack/legacy/plugins/alerting/server/shim.ts
+++ b/x-pack/legacy/plugins/alerting/server/shim.ts
@@ -6,7 +6,7 @@
import Hapi from 'hapi';
import { Legacy } from 'kibana';
-import { SpacesPlugin as SpacesPluginStartContract } from '../../spaces';
+import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces';
import { TaskManager } from '../../task_manager';
import { XPackMainPlugin } from '../../xpack_main/xpack_main';
import KbnServer from '../../../../../src/legacy/server/kbn_server';
diff --git a/x-pack/legacy/plugins/alerting/server/types.ts b/x-pack/legacy/plugins/alerting/server/types.ts
index e0b9f6e03beab..3c71412da2c89 100644
--- a/x-pack/legacy/plugins/alerting/server/types.ts
+++ b/x-pack/legacy/plugins/alerting/server/types.ts
@@ -5,7 +5,7 @@
*/
import { AlertInstance } from './lib';
-import { AlertTypeRegistry } from './alert_type_registry';
+import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry';
import { PluginSetupContract, PluginStartContract } from './plugin';
import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../../src/core/server';
@@ -95,4 +95,4 @@ export interface AlertingPlugin {
start: PluginStartContract;
}
-export type AlertTypeRegistry = PublicMethodsOf;
+export type AlertTypeRegistry = PublicMethodsOf;
diff --git a/x-pack/legacy/plugins/apm/common/i18n.ts b/x-pack/legacy/plugins/apm/common/i18n.ts
index 18d2d4df7a93b..1e27baa7c28de 100644
--- a/x-pack/legacy/plugins/apm/common/i18n.ts
+++ b/x-pack/legacy/plugins/apm/common/i18n.ts
@@ -12,3 +12,10 @@ export const NOT_AVAILABLE_LABEL = i18n.translate(
defaultMessage: 'N/A'
}
);
+
+export const UNIDENTIFIED_SERVICE_NODES_LABEL = i18n.translate(
+ 'xpack.apm.serviceNodeNameMissing',
+ {
+ defaultMessage: '(Empty)'
+ }
+);
diff --git a/x-pack/legacy/plugins/apm/common/projections/metrics.ts b/x-pack/legacy/plugins/apm/common/projections/metrics.ts
index 96d7992e2919b..5c9eeb54744d7 100644
--- a/x-pack/legacy/plugins/apm/common/projections/metrics.ts
+++ b/x-pack/legacy/plugins/apm/common/projections/metrics.ts
@@ -5,8 +5,25 @@
*/
import { Setup } from '../../server/lib/helpers/setup_request';
-import { SERVICE_NAME, PROCESSOR_EVENT } from '../elasticsearch_fieldnames';
+import {
+ SERVICE_NAME,
+ PROCESSOR_EVENT,
+ SERVICE_NODE_NAME
+} from '../elasticsearch_fieldnames';
import { rangeFilter } from '../../server/lib/helpers/range_filter';
+import { SERVICE_NODE_NAME_MISSING } from '../service_nodes';
+
+function getServiceNodeNameFilters(serviceNodeName?: string) {
+ if (!serviceNodeName) {
+ return [];
+ }
+
+ if (serviceNodeName === SERVICE_NODE_NAME_MISSING) {
+ return [{ bool: { must_not: [{ exists: { field: SERVICE_NODE_NAME } }] } }];
+ }
+
+ return [{ term: { [SERVICE_NODE_NAME]: serviceNodeName } }];
+}
export function getMetricsProjection({
setup,
@@ -19,24 +36,20 @@ export function getMetricsProjection({
}) {
const { start, end, uiFiltersES, config } = setup;
- const serviceNodeNameFilters = serviceNodeName
- ? [{ term: { [SERVICE_NAME]: serviceName } }]
- : [];
+ const filter = [
+ { term: { [SERVICE_NAME]: serviceName } },
+ { term: { [PROCESSOR_EVENT]: 'metric' } },
+ { range: rangeFilter(start, end) },
+ ...getServiceNodeNameFilters(serviceNodeName),
+ ...uiFiltersES
+ ];
return {
index: config.get('apm_oss.metricsIndices'),
body: {
query: {
bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- ...serviceNodeNameFilters,
- { term: { [PROCESSOR_EVENT]: 'metric' } },
- {
- range: rangeFilter(start, end)
- },
- ...uiFiltersES
- ]
+ filter
}
}
}
diff --git a/x-pack/legacy/plugins/apm/common/projections/service_nodes.ts b/x-pack/legacy/plugins/apm/common/projections/service_nodes.ts
index 95f92fe623a13..10ce75785c3bc 100644
--- a/x-pack/legacy/plugins/apm/common/projections/service_nodes.ts
+++ b/x-pack/legacy/plugins/apm/common/projections/service_nodes.ts
@@ -5,42 +5,35 @@
*/
import { Setup } from '../../server/lib/helpers/setup_request';
-import {
- SERVICE_NAME,
- SERVICE_NODE_NAME,
- PROCESSOR_EVENT
-} from '../elasticsearch_fieldnames';
-import { rangeFilter } from '../../server/lib/helpers/range_filter';
+import { SERVICE_NODE_NAME } from '../elasticsearch_fieldnames';
+import { mergeProjection } from './util/merge_projection';
+import { getMetricsProjection } from './metrics';
export function getServiceNodesProjection({
setup,
- serviceName
+ serviceName,
+ serviceNodeName
}: {
setup: Setup;
serviceName: string;
+ serviceNodeName?: string;
}) {
- const { start, end, uiFiltersES, config } = setup;
-
- return {
- index: config.get('apm_oss.metricsIndices'),
- body: {
- query: {
- bool: {
- filter: [
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [PROCESSOR_EVENT]: 'metric' } },
- { range: rangeFilter(start, end) },
- ...uiFiltersES
- ]
- }
- },
- aggs: {
- nodes: {
- terms: {
- field: SERVICE_NODE_NAME
+ return mergeProjection(
+ getMetricsProjection({
+ setup,
+ serviceName,
+ serviceNodeName
+ }),
+ {
+ body: {
+ aggs: {
+ nodes: {
+ terms: {
+ field: SERVICE_NODE_NAME
+ }
}
}
}
}
- };
+ );
}
diff --git a/x-pack/legacy/plugins/apm/common/service_nodes.ts b/x-pack/legacy/plugins/apm/common/service_nodes.ts
new file mode 100644
index 0000000000000..ae20a40af87ac
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/common/service_nodes.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 const SERVICE_NODE_NAME_MISSING = '_service_node_name_missing_';
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx
index 3f009a990afa5..41fffc3b847b1 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx
@@ -189,7 +189,7 @@ export function TabContent({
}) {
const codeLanguage = idx(error, _ => _.service.language.name);
const excStackframes = idx(error, _ => _.error.exception[0].stacktrace);
- const logStackframes = idx(error, _ => _.error.exception[0].stacktrace);
+ const logStackframes = idx(error, _ => _.error.log.stacktrace);
switch (currentTab.key) {
case logStacktraceTab.key:
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
index 9a687c1e84287..2667d03ef8dde 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx
@@ -18,7 +18,7 @@ interface IBucket {
// TODO: cleanup duplication of this in distribution/get_distribution.ts (ErrorDistributionAPIResponse) and transactions/distribution/index.ts (TransactionDistributionAPIResponse)
interface IDistribution {
- totalHits: number;
+ noHits: boolean;
buckets: IBucket[];
bucketSize: number;
}
@@ -57,9 +57,7 @@ export function ErrorDistribution({ distribution, title }: Props) {
distribution.bucketSize
);
- const isEmpty = distribution.totalHits === 0;
-
- if (isEmpty) {
+ if (distribution.noHits) {
return (
List should render empty state 1`] = `
},
]
}
- hidePerPageOptions={false}
initialPageSize={25}
initialSortDirection="desc"
initialSortField="latestOccurrenceAt"
@@ -284,49 +283,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = `
>
-
-
+ />
@@ -404,7 +361,6 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
},
]
}
- hidePerPageOptions={false}
initialPageSize={25}
initialSortDirection="desc"
initialSortField="latestOccurrenceAt"
@@ -1088,49 +1044,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = `
>
-
-
+ />
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
index 96a8ad01bb8a1..37fa6499ff176 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx
@@ -159,7 +159,6 @@ const ErrorGroupList: React.FC
= props => {
initialSortField="latestOccurrenceAt"
initialSortDirection="desc"
sortItems={false}
- hidePerPageOptions={false}
/>
);
};
diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx
index d579b6dff9c6f..6c95095592fe4 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx
@@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n';
import React from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { npStart } from 'ui/new_platform';
+import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes';
import { ErrorGroupDetails } from '../../ErrorGroupDetails';
import { ServiceDetails } from '../../ServiceDetails';
import { TransactionDetails } from '../../TransactionDetails';
@@ -18,6 +19,7 @@ import { AgentConfigurations } from '../../Settings/AgentConfigurations';
import { toQuery } from '../../../shared/Links/url_helpers';
import { ServiceNodeMetrics } from '../../ServiceNodeMetrics';
import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams';
+import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n';
const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', {
defaultMessage: 'Metrics'
@@ -135,6 +137,11 @@ export const routes: BreadcrumbRoute[] = [
component: () => ,
breadcrumb: ({ location }) => {
const { serviceNodeName } = resolveUrlParams(location, {});
+
+ if (serviceNodeName === SERVICE_NODE_NAME_MISSING) {
+ return UNIDENTIFIED_SERVICE_NODES_LABEL;
+ }
+
return serviceNodeName || '';
},
name: RouteName.SERVICE_NODE_METRICS
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
index fa2beffbffe0f..2833b0476428c 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx
@@ -13,11 +13,14 @@ import {
EuiPanel,
EuiSpacer,
EuiStat,
- EuiToolTip
+ EuiToolTip,
+ EuiCallOut
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
import { ApmHeader } from '../../shared/ApmHeader';
import { useUrlParams } from '../../../hooks/useUrlParams';
import { useAgentName } from '../../../hooks/useAgentName';
@@ -26,6 +29,7 @@ import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
import { MetricsChart } from '../../shared/charts/MetricsChart';
import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher';
import { truncate, px, unit } from '../../../style/variables';
+import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink';
const INITIAL_DATA = {
host: '',
@@ -38,7 +42,7 @@ const Truncate = styled.span`
`;
export function ServiceNodeMetrics() {
- const { urlParams } = useUrlParams();
+ const { urlParams, uiFilters } = useUrlParams();
const { serviceName, serviceNodeName } = urlParams;
const { agentName } = useAgentName();
@@ -47,21 +51,28 @@ export function ServiceNodeMetrics() {
const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher(
callApmApi => {
- if (serviceName && serviceNodeName) {
+ if (serviceName && serviceNodeName && start && end) {
return callApmApi({
pathname:
'/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata',
params: {
- path: { serviceName, serviceNodeName }
+ path: { serviceName, serviceNodeName },
+ query: {
+ start,
+ end,
+ uiFilters: JSON.stringify(uiFilters)
+ }
}
});
}
},
- [serviceName, serviceNodeName]
+ [serviceName, serviceNodeName, start, end, uiFilters]
);
const isLoading = status === FETCH_STATUS.LOADING;
+ const isAggregatedData = serviceNodeName === SERVICE_NODE_NAME_MISSING;
+
return (
@@ -74,39 +85,71 @@ export function ServiceNodeMetrics() {
-
-
-
- {host}
-
+ {isAggregatedData ? (
+
-
-
-
+
+ {i18n.translate(
+ 'xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningDocumentationLink',
+ { defaultMessage: 'documentation of APM Server' }
+ )}
+
+ )
+ }}
+ >
+
+ ) : (
+
+
+
+ {host}
+
}
- )}
- title={
-
- {containerId}
-
- }
- >
-
-
+ >
+
+
+
+ {containerId}
+
+ }
+ >
+
+
+ )}
{agentName && serviceNodeName && (
diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
index 38cbd12f5a527..b69076b3a1f70 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx
@@ -7,6 +7,8 @@ import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
+import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n';
+import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes';
import { PROJECTION } from '../../../../common/projections/typings';
import { LocalUIFilters } from '../../shared/LocalUIFilters';
import { useUrlParams } from '../../../hooks/useUrlParams';
@@ -20,9 +22,9 @@ import {
import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink';
import { truncate, px, unit } from '../../../style/variables';
-const INITIAL_PAGE_SIZE = 10;
-const INITIAL_SORT_FIELD = 'name';
-const INITIAL_SORT_DIRECTION = 'asc';
+const INITIAL_PAGE_SIZE = 25;
+const INITIAL_SORT_FIELD = 'cpu';
+const INITIAL_SORT_DIRECTION = 'desc';
const ServiceNodeName = styled.div`
${truncate(px(8 * unit))}
@@ -73,19 +75,43 @@ const ServiceNodeOverview = () => {
const columns: Array> = [
{
- name: i18n.translate('xpack.apm.jvmsTable.nameColumnLabel', {
- defaultMessage: 'Name'
- }),
+ name: (
+
+ <>
+ {i18n.translate('xpack.apm.jvmsTable.nameColumnLabel', {
+ defaultMessage: 'Name'
+ })}
+ >
+
+ ),
field: 'name',
sortable: true,
render: (name: string) => {
+ const { displayedName, tooltip } =
+ name === SERVICE_NODE_NAME_MISSING
+ ? {
+ displayedName: UNIDENTIFIED_SERVICE_NODES_LABEL,
+ tooltip: i18n.translate(
+ 'xpack.apm.jvmsTable.explainServiceNodeNameMissing',
+ {
+ defaultMessage:
+ 'We could not identify which JVMs these metrics belong to. This is likely caused by running a version of APM Server that is older than 7.5. Upgrading to APM Server 7.5 or higher should resolve this issue.'
+ }
+ )
+ }
+ : { displayedName: name, tooltip: name };
+
return (
-
+
- {name}
+ {displayedName}
);
@@ -93,7 +119,7 @@ const ServiceNodeOverview = () => {
},
{
name: i18n.translate('xpack.apm.jvmsTable.cpuColumnLabel', {
- defaultMessage: 'CPU'
+ defaultMessage: 'CPU avg'
}),
field: 'cpu',
sortable: true,
@@ -101,7 +127,7 @@ const ServiceNodeOverview = () => {
},
{
name: i18n.translate('xpack.apm.jvmsTable.heapMemoryColumnLabel', {
- defaultMessage: 'Heap memory max'
+ defaultMessage: 'Heap memory avg'
}),
field: 'heapMemory',
sortable: true,
@@ -109,7 +135,7 @@ const ServiceNodeOverview = () => {
},
{
name: i18n.translate('xpack.apm.jvmsTable.nonHeapMemoryColumnLabel', {
- defaultMessage: 'Non-heap memory max'
+ defaultMessage: 'Non-heap memory avg'
}),
field: 'nonHeapMemory',
sortable: true,
@@ -117,7 +143,7 @@ const ServiceNodeOverview = () => {
},
{
name: i18n.translate('xpack.apm.jvmsTable.threadCountColumnLabel', {
- defaultMessage: 'Thread count'
+ defaultMessage: 'Thread count max'
}),
field: 'threadCount',
sortable: true,
@@ -141,7 +167,6 @@ const ServiceNodeOverview = () => {
initialPageSize={INITIAL_PAGE_SIZE}
initialSortField={INITIAL_SORT_FIELD}
initialSortDirection={INITIAL_SORT_DIRECTION}
- hidePerPageOptions={false}
/>
diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
index 36d01555495a3..db0ddb56e7088 100644
--- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx
@@ -111,7 +111,7 @@ export const TransactionDistribution: FunctionComponent = (
]);
// no data in response
- if (!distribution || !distribution.totalHits) {
+ if (!distribution || distribution.noHits) {
// only show loading state if there is no data - else show stale data until new data has loaded
if (isLoading) {
return ;
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx
index b63671f63a1e7..53a9fc8bafbd1 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx
@@ -12,7 +12,7 @@ import { metadata } from 'ui/metadata';
const STACK_VERSION = metadata.branch;
// union type constisting of valid guide sections that we link to
-type DocsSection = '/apm/get-started' | '/x-pack';
+type DocsSection = '/apm/get-started' | '/x-pack' | '/apm/server';
interface Props extends EuiLinkAnchorProps {
section: DocsSection;
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx
index a9400a8753b14..247e1c253d292 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx
@@ -6,56 +6,66 @@
import React from 'react';
import { mount } from 'enzyme';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
import { HttpStatusBadge } from '../index';
+import {
+ successColor,
+ neutralColor,
+ warningColor,
+ errorColor
+} from '../../../../../utils/httpStatusCodeToColor';
describe('HttpStatusBadge', () => {
describe('render', () => {
describe('with status code 100', () => {
- it('renders with the dark shade color', () => {
+ it('renders with neutral color', () => {
const wrapper = mount( );
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
- theme.euiColorDarkShade
+ neutralColor
);
});
});
+
describe('with status code 200', () => {
- it('renders with Secondary color', () => {
+ it('renders with success color', () => {
const wrapper = mount( );
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
- theme.euiColorSecondary
+ successColor
);
});
});
+
describe('with status code 301', () => {
- it('renders with dark shade color', () => {
+ it('renders with neutral color', () => {
const wrapper = mount( );
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
- theme.euiColorDarkShade
+ neutralColor
);
});
});
+
describe('with status code 404', () => {
- it('renders with Warning color', () => {
+ it('renders with warning color', () => {
const wrapper = mount( );
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
- theme.euiColorWarning
+ warningColor
);
});
});
+
describe('with status code 502', () => {
- it('renders with Danger color', () => {
+ it('renders with error color', () => {
const wrapper = mount( );
expect(wrapper.find('HttpStatusBadge EuiBadge').prop('color')).toEqual(
- theme.euiColorDanger
+ errorColor
);
});
});
+
describe('with other status code', () => {
it('renders with default color', () => {
const wrapper = mount( );
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx
index 9c351e1645d1b..76ec7565fb80e 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx
+++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx
@@ -7,27 +7,8 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip, EuiBadge } from '@elastic/eui';
-import theme from '@elastic/eui/dist/eui_theme_light.json';
import { statusCodes } from './statusCodes';
-
-const {
- euiColorDarkShade,
- euiColorSecondary,
- euiColorWarning,
- euiColorDanger
-} = theme;
-
-function getStatusColor(status: number) {
- const colors: { [key: string]: string } = {
- 1: euiColorDarkShade,
- 2: euiColorSecondary,
- 3: euiColorDarkShade,
- 4: euiColorWarning,
- 5: euiColorDanger
- };
-
- return colors[status.toString().substr(0, 1)] || 'default';
-}
+import { httpStatusCodeToColor } from '../../../../utils/httpStatusCodeToColor';
interface HttpStatusBadgeProps {
status: number;
@@ -39,7 +20,7 @@ export function HttpStatusBadge({ status }: HttpStatusBadgeProps) {
return (
-
+
{status} {statusCodes[status.toString()]}
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json
index bdfeb2afb8c21..e8b96b501af0f 100644
--- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json
+++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json
@@ -247,5 +247,5 @@
}
],
"overallAvgDuration": 467582.45401459857,
- "totalHits": 999
+ "noHits": false
}
diff --git a/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts
index 5bffabe43fc77..51a632ac5f0a6 100644
--- a/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts
+++ b/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts
@@ -17,7 +17,7 @@ export function useServiceMetricCharts(
urlParams: IUrlParams,
agentName?: string
) {
- const { serviceName, start, end } = urlParams;
+ const { serviceName, start, end, serviceNodeName } = urlParams;
const uiFilters = useUiFilters(urlParams);
const { data = INITIAL_DATA, error, status } = useFetcher(
callApmApi => {
@@ -30,13 +30,14 @@ export function useServiceMetricCharts(
start,
end,
agentName,
+ serviceNodeName,
uiFilters: JSON.stringify(uiFilters)
}
}
});
}
},
- [serviceName, start, end, agentName, uiFilters]
+ [serviceName, start, end, agentName, serviceNodeName, uiFilters]
);
return {
diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts
index afd838e88b90d..4c7e337212e55 100644
--- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts
+++ b/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts
@@ -11,7 +11,7 @@ import { TransactionDistributionAPIResponse } from '../../server/lib/transaction
const INITIAL_DATA = {
buckets: [] as TransactionDistributionAPIResponse['buckets'],
- totalHits: 0,
+ noHits: true,
bucketSize: 0
};
diff --git a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
index 741cb5f8415e8..80a1b96efb3d6 100644
--- a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
+++ b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
@@ -10,6 +10,11 @@ import {
getResponseTimeSeries,
getTpmSeries
} from '../chartSelectors';
+import {
+ successColor,
+ warningColor,
+ errorColor
+} from '../../utils/httpStatusCodeToColor';
describe('chartSelectors', () => {
describe('getAnomalyScoreSeries', () => {
@@ -93,21 +98,21 @@ describe('chartSelectors', () => {
it('produces correct series', () => {
expect(getTpmSeries(apmTimeseries, transactionType)).toEqual([
{
- color: '#00b3a4',
+ color: successColor,
data: [{ x: 0, y: 5 }, { x: 0, y: 2 }],
legendValue: '3.5 tpm',
title: 'HTTP 2xx',
type: 'linemark'
},
{
- color: '#f98510',
+ color: warningColor,
data: [{ x: 0, y: 1 }],
legendValue: '1.0 tpm',
title: 'HTTP 4xx',
type: 'linemark'
},
{
- color: '#db1374',
+ color: errorColor,
data: [{ x: 0, y: 0 }],
legendValue: '0.0 tpm',
title: 'HTTP 5xx',
@@ -124,7 +129,7 @@ describe('chartSelectors', () => {
...apmTimeseries,
tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }]
})[0].color
- ).toEqual(theme.euiColorVis0);
+ ).toEqual(theme.euiColorSecondary);
});
});
@@ -136,7 +141,7 @@ describe('chartSelectors', () => {
...apmTimeseries,
tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }]
})[0].color
- ).toEqual(theme.euiColorVis0);
+ ).toEqual(theme.euiColorSecondary);
});
});
@@ -148,7 +153,7 @@ describe('chartSelectors', () => {
...apmTimeseries,
tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }]
})[0].color
- ).toEqual(theme.euiColorVis0);
+ ).toEqual(theme.euiColorSecondary);
});
});
@@ -160,7 +165,7 @@ describe('chartSelectors', () => {
...apmTimeseries,
tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }]
})[0].color
- ).toEqual(theme.euiColorVis0);
+ ).toEqual(theme.euiColorSecondary);
});
});
@@ -172,7 +177,7 @@ describe('chartSelectors', () => {
...apmTimeseries,
tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }]
})[0].color
- ).toEqual(theme.euiColorVis2);
+ ).toEqual(theme.euiColorDanger);
});
});
@@ -184,7 +189,7 @@ describe('chartSelectors', () => {
...apmTimeseries,
tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }]
})[0].color
- ).toEqual(theme.euiColorVis2);
+ ).toEqual(theme.euiColorDanger);
});
});
@@ -196,7 +201,7 @@ describe('chartSelectors', () => {
...apmTimeseries,
tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }]
})[0].color
- ).toEqual(theme.euiColorVis2);
+ ).toEqual(theme.euiColorDanger);
});
});
@@ -208,7 +213,7 @@ describe('chartSelectors', () => {
...apmTimeseries,
tpmBuckets: [{ key, dataPoints: [{ x: 0, y: 0 }] }]
})[0].color
- ).toEqual(theme.euiColorVis2);
+ ).toEqual(theme.euiColorDanger);
});
});
diff --git a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts
index a614d396b196e..4b65190d8ef22 100644
--- a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts
+++ b/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts
@@ -20,6 +20,7 @@ import {
import { asDecimal, asMillis, tpmUnit } from '../utils/formatters';
import { IUrlParams } from '../context/UrlParamsContext/types';
import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries';
+import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor';
export interface ITpmBucket {
title: string;
@@ -181,21 +182,16 @@ export function getTpmSeries(
function colorMatch(key: string) {
if (/ok|success/i.test(key)) {
- return theme.euiColorVis0;
+ return theme.euiColorSecondary;
} else if (/error|fail/i.test(key)) {
- return theme.euiColorVis2;
+ return theme.euiColorDanger;
}
}
function getColorByKey(keys: string[]) {
- const assignedColors: StringMap = {
- 'HTTP 2xx': theme.euiColorVis0,
- 'HTTP 3xx': theme.euiColorVis5,
- 'HTTP 4xx': theme.euiColorVis7,
- 'HTTP 5xx': theme.euiColorVis2
- };
+ const assignedColors = ['HTTP 2xx', 'HTTP 3xx', 'HTTP 4xx', 'HTTP 5xx'];
- const unknownKeys = difference(keys, Object.keys(assignedColors));
+ const unknownKeys = difference(keys, assignedColors);
const unassignedColors: StringMap = zipObject(unknownKeys, [
theme.euiColorVis1,
theme.euiColorVis3,
@@ -206,5 +202,5 @@ function getColorByKey(keys: string[]) {
]);
return (key: string) =>
- colorMatch(key) || assignedColors[key] || unassignedColors[key];
+ colorMatch(key) || httpStatusCodeToColor(key) || unassignedColors[key];
}
diff --git a/x-pack/legacy/plugins/apm/public/utils/httpStatusCodeToColor.ts b/x-pack/legacy/plugins/apm/public/utils/httpStatusCodeToColor.ts
new file mode 100644
index 0000000000000..db1ed490eb7f2
--- /dev/null
+++ b/x-pack/legacy/plugins/apm/public/utils/httpStatusCodeToColor.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 theme from '@elastic/eui/dist/eui_theme_light.json';
+import { StringMap } from '../../typings/common';
+
+const { euiColorDarkShade, euiColorWarning } = theme;
+
+export const errorColor = '#c23c2b';
+export const neutralColor = euiColorDarkShade;
+export const successColor = '#327a42';
+export const warningColor = euiColorWarning;
+
+const httpStatusCodeColors: StringMap = {
+ 1: neutralColor,
+ 2: successColor,
+ 3: neutralColor,
+ 4: warningColor,
+ 5: errorColor
+};
+
+function getStatusColor(status: number) {
+ return httpStatusCodeColors[status.toString().substr(0, 1)];
+}
+
+/**
+ * Convert an HTTP status code to a color.
+ *
+ * If passed a string, it will remove all non-numeric characters
+ */
+export function httpStatusCodeToColor(status: string | number) {
+ if (typeof status === 'string') {
+ return getStatusColor(parseInt(status.replace(/\D/g, ''), 10));
+ } else {
+ return getStatusColor(status);
+ }
+}
diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_buckets.ts
index 826d1061d5935..9c49a7f613aaa 100644
--- a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_buckets.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_buckets.ts
@@ -72,7 +72,7 @@ export async function getBuckets({
}));
return {
- totalHits: resp.hits.total,
+ noHits: resp.hits.total.value === 0,
buckets
};
}
diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_distribution.ts
index 21a4173fbeac2..81019b5261044 100644
--- a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_distribution.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/get_distribution.ts
@@ -27,7 +27,7 @@ export async function getErrorDistribution({
setup: Setup;
}) {
const bucketSize = getBucketSize(setup);
- const { buckets, totalHits } = await getBuckets({
+ const { buckets, noHits } = await getBuckets({
serviceName,
groupId,
bucketSize,
@@ -35,7 +35,7 @@ export async function getErrorDistribution({
});
return {
- totalHits,
+ noHits,
buckets,
bucketSize
};
diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_group.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_group.ts
index 6850615f4928b..dee5deb4064e4 100644
--- a/x-pack/legacy/plugins/apm/server/lib/errors/get_error_group.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/errors/get_error_group.ts
@@ -67,6 +67,6 @@ export async function getErrorGroup({
return {
transaction,
error,
- occurrencesCount: resp.hits.total
+ occurrencesCount: resp.hits.total.value
};
}
diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts
index 896c558121992..03cfa7e0c8a83 100644
--- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts
@@ -10,7 +10,7 @@ import {
IndexDocumentParams,
IndicesDeleteParams,
IndicesCreateParams,
- AggregationSearchResponse
+ AggregationSearchResponseWithTotalHitsAsObject
} from 'elasticsearch';
import { Legacy } from 'kibana';
import { cloneDeep, has, isString, set } from 'lodash';
@@ -76,8 +76,7 @@ async function getParamsForSearchRequest(
const includeFrozen = await uiSettings.get('search:includeFrozen');
return {
...addFilterForLegacyData(apmIndices, params, apmOptions), // filter out pre-7.0 data
- ignore_throttled: !includeFrozen, // whether to query frozen indices or not
- rest_total_hits_as_int: true // ensure that ES returns accurate hits.total with pre-6.6 format
+ ignore_throttled: !includeFrozen // whether to query frozen indices or not
};
}
@@ -93,7 +92,7 @@ export function getESClient(req: Legacy.Request) {
search: async (
params: U,
apmOptions?: APMOptions
- ): Promise> => {
+ ): Promise> => {
const nextParams = await getParamsForSearchRequest(
req,
params,
@@ -111,8 +110,12 @@ export function getESClient(req: Legacy.Request) {
console.log(JSON.stringify(nextParams.body, null, 4));
}
- return cluster.callWithRequest(req, 'search', nextParams) as Promise<
- AggregationSearchResponse
+ return (cluster.callWithRequest(
+ req,
+ 'search',
+ nextParams
+ ) as unknown) as Promise<
+ AggregationSearchResponseWithTotalHitsAsObject
>;
},
index: (params: IndexDocumentParams) => {
diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts
index 91809fbaede03..6edac00b8a1ab 100644
--- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts
@@ -42,8 +42,7 @@ describe('setupRequest', () => {
}
}
},
- ignore_throttled: true,
- rest_total_hits_as_int: true
+ ignore_throttled: true
});
});
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap
index 16d03c0b50498..0b1fe575f7254 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap
@@ -1,6 +1,980 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`metrics queries fetches cpu chart data 1`] = `
+exports[`metrics queries with a service node name fetches cpu chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "processCPUAverage": Object {
+ "avg": Object {
+ "field": "system.process.cpu.total.norm.pct",
+ },
+ },
+ "processCPUMax": Object {
+ "max": Object {
+ "field": "system.process.cpu.total.norm.pct",
+ },
+ },
+ "systemCPUAverage": Object {
+ "avg": Object {
+ "field": "system.cpu.total.norm.pct",
+ },
+ },
+ "systemCPUMax": Object {
+ "max": Object {
+ "field": "system.cpu.total.norm.pct",
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "processCPUAverage": Object {
+ "avg": Object {
+ "field": "system.process.cpu.total.norm.pct",
+ },
+ },
+ "processCPUMax": Object {
+ "max": Object {
+ "field": "system.process.cpu.total.norm.pct",
+ },
+ },
+ "systemCPUAverage": Object {
+ "avg": Object {
+ "field": "system.cpu.total.norm.pct",
+ },
+ },
+ "systemCPUMax": Object {
+ "max": Object {
+ "field": "system.cpu.total.norm.pct",
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "service.node.name": "bar",
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with a service node name fetches heap memory chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "heapMemoryCommitted": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.committed",
+ },
+ },
+ "heapMemoryMax": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.max",
+ },
+ },
+ "heapMemoryUsed": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.used",
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "heapMemoryCommitted": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.committed",
+ },
+ },
+ "heapMemoryMax": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.max",
+ },
+ },
+ "heapMemoryUsed": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.used",
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "service.node.name": "bar",
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ Object {
+ "term": Object {
+ "agent.name": "java",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with a service node name fetches memory chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "memoryUsedAvg": Object {
+ "avg": Object {
+ "script": Object {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ },
+ },
+ },
+ "memoryUsedMax": Object {
+ "max": Object {
+ "script": Object {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ },
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "memoryUsedAvg": Object {
+ "avg": Object {
+ "script": Object {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ },
+ },
+ },
+ "memoryUsedMax": Object {
+ "max": Object {
+ "script": Object {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ },
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "service.node.name": "bar",
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ Object {
+ "exists": Object {
+ "field": "system.memory.actual.free",
+ },
+ },
+ Object {
+ "exists": Object {
+ "field": "system.memory.total",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with a service node name fetches non heap memory chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "nonHeapMemoryCommitted": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.committed",
+ },
+ },
+ "nonHeapMemoryMax": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.max",
+ },
+ },
+ "nonHeapMemoryUsed": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.used",
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "nonHeapMemoryCommitted": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.committed",
+ },
+ },
+ "nonHeapMemoryMax": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.max",
+ },
+ },
+ "nonHeapMemoryUsed": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.used",
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "service.node.name": "bar",
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ Object {
+ "term": Object {
+ "agent.name": "java",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with a service node name fetches thread count chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "threadCount": Object {
+ "avg": Object {
+ "field": "jvm.thread.count",
+ },
+ },
+ "threadCountMax": Object {
+ "max": Object {
+ "field": "jvm.thread.count",
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "threadCount": Object {
+ "avg": Object {
+ "field": "jvm.thread.count",
+ },
+ },
+ "threadCountMax": Object {
+ "max": Object {
+ "field": "jvm.thread.count",
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "service.node.name": "bar",
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ Object {
+ "term": Object {
+ "agent.name": "java",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with service_node_name_missing fetches cpu chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "processCPUAverage": Object {
+ "avg": Object {
+ "field": "system.process.cpu.total.norm.pct",
+ },
+ },
+ "processCPUMax": Object {
+ "max": Object {
+ "field": "system.process.cpu.total.norm.pct",
+ },
+ },
+ "systemCPUAverage": Object {
+ "avg": Object {
+ "field": "system.cpu.total.norm.pct",
+ },
+ },
+ "systemCPUMax": Object {
+ "max": Object {
+ "field": "system.cpu.total.norm.pct",
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "processCPUAverage": Object {
+ "avg": Object {
+ "field": "system.process.cpu.total.norm.pct",
+ },
+ },
+ "processCPUMax": Object {
+ "max": Object {
+ "field": "system.process.cpu.total.norm.pct",
+ },
+ },
+ "systemCPUAverage": Object {
+ "avg": Object {
+ "field": "system.cpu.total.norm.pct",
+ },
+ },
+ "systemCPUMax": Object {
+ "max": Object {
+ "field": "system.cpu.total.norm.pct",
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "bool": Object {
+ "must_not": Array [
+ Object {
+ "exists": Object {
+ "field": "service.node.name",
+ },
+ },
+ ],
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with service_node_name_missing fetches heap memory chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "heapMemoryCommitted": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.committed",
+ },
+ },
+ "heapMemoryMax": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.max",
+ },
+ },
+ "heapMemoryUsed": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.used",
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "heapMemoryCommitted": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.committed",
+ },
+ },
+ "heapMemoryMax": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.max",
+ },
+ },
+ "heapMemoryUsed": Object {
+ "avg": Object {
+ "field": "jvm.memory.heap.used",
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "bool": Object {
+ "must_not": Array [
+ Object {
+ "exists": Object {
+ "field": "service.node.name",
+ },
+ },
+ ],
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ Object {
+ "term": Object {
+ "agent.name": "java",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with service_node_name_missing fetches memory chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "memoryUsedAvg": Object {
+ "avg": Object {
+ "script": Object {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ },
+ },
+ },
+ "memoryUsedMax": Object {
+ "max": Object {
+ "script": Object {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ },
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "memoryUsedAvg": Object {
+ "avg": Object {
+ "script": Object {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ },
+ },
+ },
+ "memoryUsedMax": Object {
+ "max": Object {
+ "script": Object {
+ "lang": "expression",
+ "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ },
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "bool": Object {
+ "must_not": Array [
+ Object {
+ "exists": Object {
+ "field": "service.node.name",
+ },
+ },
+ ],
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ Object {
+ "exists": Object {
+ "field": "system.memory.actual.free",
+ },
+ },
+ Object {
+ "exists": Object {
+ "field": "system.memory.total",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with service_node_name_missing fetches non heap memory chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "nonHeapMemoryCommitted": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.committed",
+ },
+ },
+ "nonHeapMemoryMax": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.max",
+ },
+ },
+ "nonHeapMemoryUsed": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.used",
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "nonHeapMemoryCommitted": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.committed",
+ },
+ },
+ "nonHeapMemoryMax": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.max",
+ },
+ },
+ "nonHeapMemoryUsed": Object {
+ "avg": Object {
+ "field": "jvm.memory.non_heap.used",
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "bool": Object {
+ "must_not": Array [
+ Object {
+ "exists": Object {
+ "field": "service.node.name",
+ },
+ },
+ ],
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ Object {
+ "term": Object {
+ "agent.name": "java",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries with service_node_name_missing fetches thread count chart data 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "threadCount": Object {
+ "avg": Object {
+ "field": "jvm.thread.count",
+ },
+ },
+ "threadCountMax": Object {
+ "max": Object {
+ "field": "jvm.thread.count",
+ },
+ },
+ "timeseriesData": Object {
+ "aggs": Object {
+ "threadCount": Object {
+ "avg": Object {
+ "field": "jvm.thread.count",
+ },
+ },
+ "threadCountMax": Object {
+ "max": Object {
+ "field": "jvm.thread.count",
+ },
+ },
+ },
+ "date_histogram": Object {
+ "extended_bounds": Object {
+ "max": 1528977600000,
+ "min": 1528113600000,
+ },
+ "field": "@timestamp",
+ "fixed_interval": "10800s",
+ "min_doc_count": 0,
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "bool": Object {
+ "must_not": Array [
+ Object {
+ "exists": Object {
+ "field": "service.node.name",
+ },
+ },
+ ],
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ Object {
+ "term": Object {
+ "agent.name": "java",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`metrics queries without a service node name fetches cpu chart data 1`] = `
Object {
"body": Object {
"aggs": Object {
@@ -94,7 +1068,7 @@ Object {
}
`;
-exports[`metrics queries fetches heap memory chart data 1`] = `
+exports[`metrics queries without a service node name fetches heap memory chart data 1`] = `
Object {
"body": Object {
"aggs": Object {
@@ -183,7 +1157,7 @@ Object {
}
`;
-exports[`metrics queries fetches memory chart data 1`] = `
+exports[`metrics queries without a service node name fetches memory chart data 1`] = `
Object {
"body": Object {
"aggs": Object {
@@ -279,7 +1253,7 @@ Object {
}
`;
-exports[`metrics queries fetches non heap memory chart data 1`] = `
+exports[`metrics queries without a service node name fetches non heap memory chart data 1`] = `
Object {
"body": Object {
"aggs": Object {
@@ -368,7 +1342,7 @@ Object {
}
`;
-exports[`metrics queries fetches thread count chart data 1`] = `
+exports[`metrics queries without a service node name fetches thread count chart data 1`] = `
Object {
"body": Object {
"aggs": Object {
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts
index a43d8cd0dc2ea..7b8fc4ac44f64 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/fetchAndTransformGcMetrics.ts
@@ -10,7 +10,7 @@
*/
import { idx } from '@kbn/elastic-idx';
-import { sum } from 'lodash';
+import { sum, round } from 'lodash';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import { Setup } from '../../../../helpers/setup_request';
import { getMetricsDateHistogramParams } from '../../../../helpers/metrics';
@@ -23,6 +23,7 @@ import {
METRIC_JAVA_GC_COUNT,
METRIC_JAVA_GC_TIME
} from '../../../../../../common/elasticsearch_fieldnames';
+import { getBucketSize } from '../../../../helpers/get_bucket_size';
const colors = [
theme.euiColorVis0,
@@ -49,6 +50,8 @@ export async function fetchAndTransformGcMetrics({
}) {
const { start, end, client } = setup;
+ const { bucketSize } = getBucketSize(start, end, 'auto');
+
const projection = getMetricsProjection({
setup,
serviceName,
@@ -115,7 +118,7 @@ export async function fetchAndTransformGcMetrics({
if (!aggregations) {
return {
...chartBase,
- totalHits: 0,
+ noHits: true,
series: []
};
}
@@ -128,7 +131,7 @@ export async function fetchAndTransformGcMetrics({
// derivative/value will be undefined for the first hit and if the `max` value is null
const y =
'value' in bucket && bucket.value.value !== null
- ? bucket.value.value
+ ? round(bucket.value.value * (60 / bucketSize), 1)
: null;
return {
@@ -153,7 +156,7 @@ export async function fetchAndTransformGcMetrics({
return {
...chartBase,
- totalHits: response.hits.total,
+ noHits: response.hits.total.value === 0,
series
};
}
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts
index 7374171a36014..aa4533db0c33b 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcRateChart.ts
@@ -14,17 +14,17 @@ import { ChartBase } from '../../../types';
const series = {
[METRIC_JAVA_GC_COUNT]: {
title: i18n.translate('xpack.apm.agentMetrics.java.gcRate', {
- defaultMessage: 'GC count'
+ defaultMessage: 'GC rate'
}),
color: theme.euiColorVis0
}
};
const chartBase: ChartBase = {
- title: i18n.translate('xpack.apm.agentMetrics.java.gcCountChartTitle', {
- defaultMessage: 'Garbage collection count'
+ title: i18n.translate('xpack.apm.agentMetrics.java.gcRateChartTitle', {
+ defaultMessage: 'Garbage collection rate per minute'
}),
- key: 'gc_count_line_chart',
+ key: 'gc_rate_line_chart',
type: 'linemark',
yUnit: 'integer',
series
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts
index 43c10f5b97c54..b6e992acf62a9 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/gc/getGcTimeChart.ts
@@ -22,7 +22,7 @@ const series = {
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.agentMetrics.java.gcTimeChartTitle', {
- defaultMessage: 'Garbage collection time'
+ defaultMessage: 'Garbage collection time spent per minute'
}),
key: 'gc_time_line_chart',
type: 'linemark',
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts
index 8ee835675e50e..f276fa69e20e3 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/queries.test.ts
@@ -13,45 +13,66 @@ import {
SearchParamsMock,
inspectSearchParams
} from '../../../public/utils/testHelpers';
+import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes';
describe('metrics queries', () => {
let mock: SearchParamsMock;
- afterEach(() => {
- mock.teardown();
- });
+ const createTests = (serviceNodeName?: string) => {
+ it('fetches cpu chart data', async () => {
+ mock = await inspectSearchParams(setup =>
+ getCPUChartData(setup, 'foo', serviceNodeName)
+ );
- it('fetches cpu chart data', async () => {
- mock = await inspectSearchParams(setup => getCPUChartData(setup, 'foo'));
+ expect(mock.params).toMatchSnapshot();
+ });
- expect(mock.params).toMatchSnapshot();
- });
+ it('fetches memory chart data', async () => {
+ mock = await inspectSearchParams(setup =>
+ getMemoryChartData(setup, 'foo', serviceNodeName)
+ );
- it('fetches memory chart data', async () => {
- mock = await inspectSearchParams(setup => getMemoryChartData(setup, 'foo'));
+ expect(mock.params).toMatchSnapshot();
+ });
- expect(mock.params).toMatchSnapshot();
- });
+ it('fetches heap memory chart data', async () => {
+ mock = await inspectSearchParams(setup =>
+ getHeapMemoryChart(setup, 'foo', serviceNodeName)
+ );
- it('fetches heap memory chart data', async () => {
- mock = await inspectSearchParams(setup => getHeapMemoryChart(setup, 'foo'));
+ expect(mock.params).toMatchSnapshot();
+ });
- expect(mock.params).toMatchSnapshot();
- });
+ it('fetches non heap memory chart data', async () => {
+ mock = await inspectSearchParams(setup =>
+ getNonHeapMemoryChart(setup, 'foo', serviceNodeName)
+ );
+
+ expect(mock.params).toMatchSnapshot();
+ });
- it('fetches non heap memory chart data', async () => {
- mock = await inspectSearchParams(setup =>
- getNonHeapMemoryChart(setup, 'foo')
- );
+ it('fetches thread count chart data', async () => {
+ mock = await inspectSearchParams(setup =>
+ getThreadCountChart(setup, 'foo', serviceNodeName)
+ );
- expect(mock.params).toMatchSnapshot();
+ expect(mock.params).toMatchSnapshot();
+ });
+ };
+
+ afterEach(() => {
+ mock.teardown();
});
- it('fetches thread count chart data', async () => {
- mock = await inspectSearchParams(setup =>
- getThreadCountChart(setup, 'foo')
- );
+ describe('without a service node name', () => {
+ createTests();
+ });
+
+ describe('with service_node_name_missing', () => {
+ createTests(SERVICE_NODE_NAME_MISSING);
+ });
- expect(mock.params).toMatchSnapshot();
+ describe('with a service node name', () => {
+ createTests('bar');
});
});
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts
index e077105e9b2e5..fe8f269fdab7c 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.test.ts
@@ -8,7 +8,7 @@ import { ChartType, YUnit } from '../../../typings/timeseries';
test('transformDataToMetricsChart should transform an ES result into a chart object', () => {
const response = {
- hits: { total: 5000 },
+ hits: { total: { value: 5000 } },
aggregations: {
a: { value: 1000 },
b: { value: 1000 },
@@ -58,6 +58,7 @@ test('transformDataToMetricsChart should transform an ES result into a chart obj
expect(chart).toMatchInlineSnapshot(`
Object {
"key": "test_chart_key",
+ "noHits": false,
"series": Array [
Object {
"color": "red",
@@ -124,7 +125,6 @@ Object {
},
],
"title": "Test Chart Title",
- "totalHits": 5000,
"yUnit": "number",
}
`);
diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts
index af0605eb9d454..89bc3b4107a5f 100644
--- a/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/metrics/transform_metrics_chart.ts
@@ -4,7 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import theme from '@elastic/eui/dist/eui_theme_light.json';
-import { AggregationSearchResponse, AggregatedValue } from 'elasticsearch';
+import {
+ AggregationSearchResponseWithTotalHitsAsObject,
+ AggregatedValue
+} from 'elasticsearch';
import { idx } from '@kbn/elastic-idx';
import { ChartBase } from './types';
@@ -46,7 +49,7 @@ interface AggregatedParams {
}
export function transformDataToMetricsChart(
- result: AggregationSearchResponse,
+ result: AggregationSearchResponseWithTotalHitsAsObject,
chartBase: ChartBase
) {
const { aggregations, hits } = result;
@@ -56,7 +59,7 @@ export function transformDataToMetricsChart(
title: chartBase.title,
key: chartBase.key,
yUnit: chartBase.yUnit,
- totalHits: hits.total,
+ noHits: hits.total.value === 0,
series: Object.keys(chartBase.series).map((seriesKey, i) => {
const overallValue = idx(aggregations, _ => _[seriesKey].value);
diff --git a/x-pack/legacy/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap
index d20adeba1bcc7..ce07759669d6b 100644
--- a/x-pack/legacy/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap
@@ -1,5 +1,137 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`service node queries fetches metadata for a service node 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "containerId": Object {
+ "terms": Object {
+ "field": "container.id",
+ "size": 1,
+ },
+ },
+ "host": Object {
+ "terms": Object {
+ "field": "host.hostname",
+ "size": 1,
+ },
+ },
+ "nodes": Object {
+ "terms": Object {
+ "field": "service.node.name",
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "term": Object {
+ "service.node.name": "bar",
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
+exports[`service node queries fetches metadata for unidentified service nodes 1`] = `
+Object {
+ "body": Object {
+ "aggs": Object {
+ "containerId": Object {
+ "terms": Object {
+ "field": "container.id",
+ "size": 1,
+ },
+ },
+ "host": Object {
+ "terms": Object {
+ "field": "host.hostname",
+ "size": 1,
+ },
+ },
+ "nodes": Object {
+ "terms": Object {
+ "field": "service.node.name",
+ },
+ },
+ },
+ "query": Object {
+ "bool": Object {
+ "filter": Array [
+ Object {
+ "term": Object {
+ "service.name": "foo",
+ },
+ },
+ Object {
+ "term": Object {
+ "processor.event": "metric",
+ },
+ },
+ Object {
+ "range": Object {
+ "@timestamp": Object {
+ "format": "epoch_millis",
+ "gte": 1528113600000,
+ "lte": 1528977600000,
+ },
+ },
+ },
+ Object {
+ "bool": Object {
+ "must_not": Array [
+ Object {
+ "exists": Object {
+ "field": "service.node.name",
+ },
+ },
+ ],
+ },
+ },
+ Object {
+ "term": Object {
+ "service.environment": "prod",
+ },
+ },
+ ],
+ },
+ },
+ "size": 0,
+ },
+ "index": "myIndex",
+}
+`;
+
exports[`service node queries fetches services nodes 1`] = `
Object {
"body": Object {
@@ -29,6 +161,7 @@ Object {
},
"terms": Object {
"field": "service.node.name",
+ "missing": "_service_node_name_missing_",
"size": 10000,
},
},
diff --git a/x-pack/legacy/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/legacy/plugins/apm/server/lib/service_nodes/index.ts
index bef22584d8a46..afdf4795c4d29 100644
--- a/x-pack/legacy/plugins/apm/server/lib/service_nodes/index.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/service_nodes/index.ts
@@ -7,6 +7,7 @@
import { Setup } from '../helpers/setup_request';
import { getServiceNodesProjection } from '../../../common/projections/service_nodes';
import { mergeProjection } from '../../../common/projections/util/merge_projection';
+import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes';
import {
METRIC_PROCESS_CPU_PERCENT,
METRIC_JAVA_THREAD_COUNT,
@@ -30,7 +31,8 @@ const getServiceNodes = async ({
aggs: {
nodes: {
terms: {
- size: 10000
+ size: 10000,
+ missing: SERVICE_NODE_NAME_MISSING
},
aggs: {
cpu: {
diff --git a/x-pack/legacy/plugins/apm/server/lib/service_nodes/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/service_nodes/queries.test.ts
index b5b7e492386f4..80cd94b1549d7 100644
--- a/x-pack/legacy/plugins/apm/server/lib/service_nodes/queries.test.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/service_nodes/queries.test.ts
@@ -14,6 +14,8 @@ import {
SearchParamsMock,
inspectSearchParams
} from '../../../public/utils/testHelpers';
+import { getServiceNodeMetadata } from '../services/get_service_node_metadata';
+import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes';
describe('service node queries', () => {
let mock: SearchParamsMock;
@@ -29,4 +31,28 @@ describe('service node queries', () => {
expect(mock.params).toMatchSnapshot();
});
+
+ it('fetches metadata for a service node', async () => {
+ mock = await inspectSearchParams(setup =>
+ getServiceNodeMetadata({
+ setup,
+ serviceName: 'foo',
+ serviceNodeName: 'bar'
+ })
+ );
+
+ expect(mock.params).toMatchSnapshot();
+ });
+
+ it('fetches metadata for unidentified service nodes', async () => {
+ mock = await inspectSearchParams(setup =>
+ getServiceNodeMetadata({
+ setup,
+ serviceName: 'foo',
+ serviceNodeName: SERVICE_NODE_NAME_MISSING
+ })
+ );
+
+ expect(mock.params).toMatchSnapshot();
+ });
});
diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_node_metadata.ts
index 14bad7151757d..88e9670f4b444 100644
--- a/x-pack/legacy/plugins/apm/server/lib/services/get_service_node_metadata.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_node_metadata.ts
@@ -8,12 +8,11 @@ import { idx } from '@kbn/elastic-idx';
import { Setup } from '../helpers/setup_request';
import {
HOST_NAME,
- SERVICE_NAME,
- SERVICE_NODE_NAME,
- CONTAINER_ID,
- PROCESSOR_EVENT
+ CONTAINER_ID
} from '../../../common/elasticsearch_fieldnames';
import { NOT_AVAILABLE_LABEL } from '../../../common/i18n';
+import { mergeProjection } from '../../../common/projections/util/merge_projection';
+import { getServiceNodesProjection } from '../../../common/projections/service_nodes';
export async function getServiceNodeMetadata({
serviceName,
@@ -24,37 +23,34 @@ export async function getServiceNodeMetadata({
serviceNodeName: string;
setup: Setup;
}) {
- const { client, config } = setup;
+ const { client } = setup;
- const query = {
- index: config.get('apm_oss.metricsIndices'),
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- { term: { [PROCESSOR_EVENT]: 'metric' } },
- { term: { [SERVICE_NAME]: serviceName } },
- { term: { [SERVICE_NODE_NAME]: serviceNodeName } }
- ]
- }
- },
- aggs: {
- host: {
- terms: {
- field: HOST_NAME,
- size: 1
- }
- },
- containerId: {
- terms: {
- field: CONTAINER_ID,
- size: 1
+ const query = mergeProjection(
+ getServiceNodesProjection({
+ setup,
+ serviceName,
+ serviceNodeName
+ }),
+ {
+ body: {
+ size: 0,
+ aggs: {
+ host: {
+ terms: {
+ field: HOST_NAME,
+ size: 1
+ }
+ },
+ containerId: {
+ terms: {
+ field: CONTAINER_ID,
+ size: 1
+ }
}
}
}
}
- };
+ );
const response = await client.search(query);
diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_agent_status.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_agent_status.ts
index 68e5afd995203..d1654632dbb26 100644
--- a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_agent_status.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_agent_status.ts
@@ -42,6 +42,6 @@ export async function getAgentStatus(setup: Setup) {
};
const resp = await client.search(params);
- const hasHistorialAgentData = resp.hits.total > 0;
+ const hasHistorialAgentData = resp.hits.total.value > 0;
return hasHistorialAgentData;
}
diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts
index 2d59a4f028326..1379d79326add 100644
--- a/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/services/get_services/get_legacy_data_status.ts
@@ -31,6 +31,6 @@ export async function getLegacyDataStatus(setup: Setup) {
};
const resp = await client.search(params, { includeLegacyData: true });
- const hasLegacyData = resp.hits.total > 0;
+ const hasLegacyData = resp.hits.total.value > 0;
return hasLegacyData;
}
diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap
index 396e8540afdd6..4484f41d06ac8 100644
--- a/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap
+++ b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap
@@ -54,6 +54,7 @@ Object {
},
},
],
+ "track_total_hits": true,
},
"index": Array [
"myIndex",
diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts
index 00eeefb4b4fcc..74e16424b1098 100644
--- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts
@@ -44,7 +44,8 @@ export async function getTraceItems(traceId: string, setup: Setup) {
{ _score: { order: 'asc' } },
{ [TRANSACTION_DURATION]: { order: 'desc' } },
{ [SPAN_DURATION]: { order: 'desc' } }
- ]
+ ],
+ track_total_hits: true
}
};
@@ -52,6 +53,6 @@ export async function getTraceItems(traceId: string, setup: Setup) {
return {
items: resp.hits.hits.map(hit => hit._source),
- exceedsMax: resp.hits.total > maxTraceItems
+ exceedsMax: resp.hits.total.value > maxTraceItems
};
}
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts
index 74980caf8c350..827194edd6aa3 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/get_buckets/transform.ts
@@ -39,7 +39,7 @@ export function bucketTransformer(response: DistributionBucketResponse) {
).map(getBucket);
return {
- totalHits: response.hits.total,
+ noHits: response.hits.total.value === 0,
buckets
};
}
diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts
index 454e247a19cae..3efa996d609d8 100644
--- a/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts
+++ b/x-pack/legacy/plugins/apm/server/lib/transactions/distribution/index.ts
@@ -47,11 +47,11 @@ export async function getTransactionDistribution({
);
if (distributionMax == null) {
- return { totalHits: 0, buckets: [], bucketSize: 0 };
+ return { noHits: true, buckets: [], bucketSize: 0 };
}
const bucketSize = getBucketSize(distributionMax, setup);
- const { buckets, totalHits } = await getBuckets(
+ const { buckets, noHits } = await getBuckets(
serviceName,
transactionName,
transactionType,
@@ -63,7 +63,7 @@ export async function getTransactionDistribution({
);
return {
- totalHits,
+ noHits,
buckets,
bucketSize
};
diff --git a/x-pack/legacy/plugins/apm/server/routes/service_nodes.ts b/x-pack/legacy/plugins/apm/server/routes/service_nodes.ts
index a87ef7d8e5878..285dd5b1f10f5 100644
--- a/x-pack/legacy/plugins/apm/server/routes/service_nodes.ts
+++ b/x-pack/legacy/plugins/apm/server/routes/service_nodes.ts
@@ -17,7 +17,7 @@ export const serviceNodesRoute = createRoute(core => ({
}),
query: t.intersection([rangeRt, uiFiltersRt])
},
- handler: async (req, { path, query }) => {
+ handler: async (req, { path }) => {
const setup = await setupRequest(req);
const { serviceName } = path;
diff --git a/x-pack/legacy/plugins/apm/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts
index 99b8d3c7ba154..85d53925db86e 100644
--- a/x-pack/legacy/plugins/apm/server/routes/services.ts
+++ b/x-pack/legacy/plugins/apm/server/routes/services.ts
@@ -73,7 +73,8 @@ export const serviceNodeMetadataRoute = createRoute(() => ({
path: t.type({
serviceName: t.string,
serviceNodeName: t.string
- })
+ }),
+ query: t.intersection([uiFiltersRt, rangeRt])
},
handler: async (req, { path }) => {
const setup = await setupRequest(req);
diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts
index c1a8a7f9b3985..10cd7bcbf4f38 100644
--- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts
+++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts
@@ -81,6 +81,11 @@ declare module 'elasticsearch' {
value: number | null;
}
+ interface HitsTotal {
+ value: number;
+ relation: 'eq' | 'gte';
+ }
+
type AggregationResultMap = IndexAsString<
{
[AggregationName in keyof AggregationOption]: {
@@ -101,7 +106,7 @@ declare module 'elasticsearch' {
>;
top_hits: {
hits: {
- total: number;
+ total: HitsTotal;
max_score: number | null;
hits: Array<{
_source: AggregationOption[AggregationName] extends {
@@ -148,7 +153,10 @@ declare module 'elasticsearch' {
}
>;
- export type AggregationSearchResponse = Pick<
+ export type AggregationSearchResponseWithTotalHitsAsInt<
+ HitType,
+ SearchParams
+ > = Pick<
SearchResponse,
Exclude, 'aggregations'>
> &
@@ -158,6 +166,24 @@ declare module 'elasticsearch' {
}
: {});
+ type Hits = Pick<
+ SearchResponse['hits'],
+ Exclude['hits'], 'total'>
+ > & {
+ total: HitsTotal;
+ };
+
+ export type AggregationSearchResponseWithTotalHitsAsObject<
+ HitType,
+ SearchParams
+ > = Pick<
+ AggregationSearchResponseWithTotalHitsAsInt,
+ Exclude<
+ keyof AggregationSearchResponseWithTotalHitsAsInt,
+ 'hits'
+ >
+ > & { hits: Hits };
+
export interface ESFilter {
[key: string]: {
[key: string]: string | string[] | number | StringMap | ESFilter[];
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts
index 097aef69d4b4c..894df9dc1c6b9 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/index.ts
@@ -47,9 +47,10 @@ import { rounddate } from './rounddate';
import { rowCount } from './rowCount';
import { repeatImage } from './repeatImage';
import { revealImage } from './revealImage';
-import { savedMap } from './saved_map';
-import { savedSearch } from './saved_search';
-import { savedVisualization } from './saved_visualization';
+// TODO: elastic/kibana#44822 Disabling pending filters work
+// import { savedMap } from './saved_map';
+// import { savedSearch } from './saved_search';
+// import { savedVisualization } from './saved_visualization';
import { seriesStyle } from './seriesStyle';
import { shape } from './shape';
import { sort } from './sort';
@@ -106,9 +107,10 @@ export const functions = [
revealImage,
rounddate,
rowCount,
- savedMap,
- savedSearch,
- savedVisualization,
+ // TODO: elastic/kibana#44822 Disabling pending filters work
+ // savedMap,
+ // savedSearch,
+ // savedVisualization,
seriesStyle,
shape,
sort,
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts
index c36a05e405704..8c8f53ad0d74e 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts
@@ -35,6 +35,7 @@ interface SavedMapInput extends EmbeddableInput {
type Return = EmbeddableExpression;
export function savedMap(): ExpressionFunction<'savedMap', Filter | null, Arguments, Return> {
+ // @ts-ignore elastic/kibana#44822 Disabling pending filters work
const { help, args: argHelp } = getFunctionHelp().savedMap;
return {
name: 'savedMap',
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts
index 40bf235761b92..72ab334e68ca4 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_search.ts
@@ -23,6 +23,7 @@ interface Arguments {
type Return = EmbeddableExpression & { id: SearchInput['id'] }>;
export function savedSearch(): ExpressionFunction<'savedSearch', Filter | null, Arguments, Return> {
+ // @ts-ignore elastic/kibana#44822 Disabling pending filters work
const { help, args: argHelp } = getFunctionHelp().savedSearch;
return {
name: 'savedSearch',
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts
index 930bdb74ee363..b3bc9b8ca69b7 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_visualization.ts
@@ -26,6 +26,7 @@ export function savedVisualization(): ExpressionFunction<
Arguments,
Return
> {
+ // @ts-ignore elastic/kibana#44822 Disabling pending filters work
const { help, args: argHelp } = getFunctionHelp().savedVisualization;
return {
name: 'savedVisualization',
diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/function_help.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/function_help.ts
index 302f97d06ebed..327865ad1c61b 100644
--- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/function_help.ts
+++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/function_help.ts
@@ -207,8 +207,12 @@ export const getFunctionHelp = (): FunctionHelpDict => ({
revealImage,
rounddate,
rowCount,
+ // TODO: elastic/kibana#44822 Disabling pending filters work
+ // @ts-ignore
savedMap,
+ // @ts-ignore
savedSearch,
+ // @ts-ignore
savedVisualization,
seriesStyle,
shape,
diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts
index 3c73dd4090757..c7434a3814b38 100644
--- a/x-pack/legacy/plugins/canvas/i18n/components.ts
+++ b/x-pack/legacy/plugins/canvas/i18n/components.ts
@@ -465,6 +465,24 @@ export const ComponentStrings = {
},
}),
},
+ FunctionFormContextError: {
+ getContextErrorMessage: (errorMessage: string) =>
+ i18n.translate('xpack.canvas.functionForm.contextError', {
+ defaultMessage: 'ERROR: {errorMessage}',
+ values: {
+ errorMessage,
+ },
+ }),
+ },
+ FunctionFormFunctionUnknown: {
+ getUnknownArgumentTypeErrorMessage: (expressionType: string) =>
+ i18n.translate('xpack.canvas.functionForm.functionUnknown.unknownArgumentTypeError', {
+ defaultMessage: 'Unknown expression type "{expressionType}"',
+ values: {
+ expressionType,
+ },
+ }),
+ },
GroupSettings: {
getSaveGroupDescription: () =>
i18n.translate('xpack.canvas.groupSettings.saveGroupDescription', {
diff --git a/x-pack/legacy/plugins/canvas/public/components/function_form/function_form_context_error.js b/x-pack/legacy/plugins/canvas/public/components/function_form/function_form_context_error.js
deleted file mode 100644
index 513d5a4138ad8..0000000000000
--- a/x-pack/legacy/plugins/canvas/public/components/function_form/function_form_context_error.js
+++ /dev/null
@@ -1,16 +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 PropTypes from 'prop-types';
-
-export const FunctionFormContextError = ({ context }) => (
- ERROR: {context.error}
-);
-
-FunctionFormContextError.propTypes = {
- context: PropTypes.object,
-};
diff --git a/x-pack/legacy/plugins/canvas/public/components/function_form/function_form_context_error.tsx b/x-pack/legacy/plugins/canvas/public/components/function_form/function_form_context_error.tsx
new file mode 100644
index 0000000000000..2be047d0fb3b0
--- /dev/null
+++ b/x-pack/legacy/plugins/canvas/public/components/function_form/function_form_context_error.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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, { FunctionComponent } from 'react';
+import PropTypes from 'prop-types';
+import { ComponentStrings } from '../../../i18n/components';
+
+interface Props {
+ context: {
+ error: string;
+ };
+}
+
+const { FunctionFormContextError: strings } = ComponentStrings;
+
+export const FunctionFormContextError: FunctionComponent = ({ context }) => (
+
+ {strings.getContextErrorMessage(context.error)}
+
+);
+
+FunctionFormContextError.propTypes = {
+ context: PropTypes.shape({ error: PropTypes.string }).isRequired,
+};
diff --git a/x-pack/legacy/plugins/canvas/public/components/function_form/function_unknown.js b/x-pack/legacy/plugins/canvas/public/components/function_form/function_unknown.tsx
similarity index 53%
rename from x-pack/legacy/plugins/canvas/public/components/function_form/function_unknown.js
rename to x-pack/legacy/plugins/canvas/public/components/function_form/function_unknown.tsx
index 462fce9f1c90a..c6b676ca0a166 100644
--- a/x-pack/legacy/plugins/canvas/public/components/function_form/function_unknown.js
+++ b/x-pack/legacy/plugins/canvas/public/components/function_form/function_unknown.tsx
@@ -4,12 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { FunctionComponent } from 'react';
import PropTypes from 'prop-types';
+import { ComponentStrings } from '../../../i18n';
-export const FunctionUnknown = ({ argType }) => (
+interface Props {
+ /** the type of the argument */
+ argType: string;
+}
+const { FunctionFormFunctionUnknown: strings } = ComponentStrings;
+
+export const FunctionUnknown: FunctionComponent = ({ argType }) => (
- Unknown expression type "{argType}"
+ {strings.getUnknownArgumentTypeErrorMessage(argType)}
);
diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx
index 31ad0593f58bb..b05e1f5b757c2 100644
--- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx
+++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx
@@ -13,7 +13,6 @@ import {
EuiFlexGroup,
EuiButtonIcon,
EuiButton,
- EuiButtonEmpty,
EuiOverlayMask,
EuiModal,
EuiModalFooter,
@@ -194,11 +193,14 @@ export class WorkpadHeader extends React.PureComponent {
+ {/*
+ TODO: elastic/kibana#44822 Disabling pending filters work
{strings.getEmbedObjectButtonLabel()}
+ */}
{
const git = await this.openGit(uri);
const commit = await this.getCommitOr404(uri, revision);
+ if (!revision.includes('..')) {
+ revision = `${revision}..${revision}~1`;
+ }
const diffs = await git.diffSummary([revision]);
const commitDiff: CommitDiff = {
diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/charts.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/charts.tsx
index 7e481d515d64a..32c9ac36b5c52 100644
--- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/charts.tsx
+++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/charts.tsx
@@ -44,7 +44,7 @@ export const MetricsExplorerCharts = ({
timeRange,
onTimeChange,
}: Props) => {
- if (!data && loading) {
+ if (loading) {
return (
{
if (!source) {
- return null;
+ return ;
}
const {
diff --git a/x-pack/legacy/plugins/infra/types/eui.d.ts b/x-pack/legacy/plugins/infra/types/eui.d.ts
index f117181eae43d..530cda2d57731 100644
--- a/x-pack/legacy/plugins/infra/types/eui.d.ts
+++ b/x-pack/legacy/plugins/infra/types/eui.d.ts
@@ -96,6 +96,7 @@ declare module '@elastic/eui' {
message?: any;
rowProps?: any;
cellProps?: any;
+ responsive?: boolean;
};
export const EuiInMemoryTable: React.SFC;
}
diff --git a/x-pack/legacy/plugins/lens/server/routes/field_stats.ts b/x-pack/legacy/plugins/lens/server/routes/field_stats.ts
index dee623329c1f1..e7e378e6b20e4 100644
--- a/x-pack/legacy/plugins/lens/server/routes/field_stats.ts
+++ b/x-pack/legacy/plugins/lens/server/routes/field_stats.ts
@@ -7,7 +7,7 @@
import Boom from 'boom';
import DateMath from '@elastic/datemath';
import { schema } from '@kbn/config-schema';
-import { AggregationSearchResponse } from 'elasticsearch';
+import { AggregationSearchResponseWithTotalHitsAsInt } from 'elasticsearch';
import { CoreSetup } from 'src/core/server';
import { FieldStatsResponse, BASE_API_URL } from '../../common';
@@ -135,10 +135,9 @@ export async function getNumberHistogram(
},
};
- const minMaxResult = (await aggSearchWithBody(searchBody)) as AggregationSearchResponse<
- unknown,
- { body: { aggs: typeof searchBody } }
- >;
+ const minMaxResult = (await aggSearchWithBody(
+ searchBody
+ )) as AggregationSearchResponseWithTotalHitsAsInt;
const minValue = minMaxResult.aggregations!.sample.min_value.value;
const maxValue = minMaxResult.aggregations!.sample.max_value.value;
@@ -179,7 +178,9 @@ export async function getNumberHistogram(
},
},
};
- const histogramResult = (await aggSearchWithBody(histogramBody)) as AggregationSearchResponse<
+ const histogramResult = (await aggSearchWithBody(
+ histogramBody
+ )) as AggregationSearchResponseWithTotalHitsAsInt<
unknown,
{ body: { aggs: typeof histogramBody } }
>;
@@ -213,7 +214,9 @@ export async function getStringSamples(
},
},
};
- const topValuesResult = (await aggSearchWithBody(topValuesBody)) as AggregationSearchResponse<
+ const topValuesResult = (await aggSearchWithBody(
+ topValuesBody
+ )) as AggregationSearchResponseWithTotalHitsAsInt<
unknown,
{ body: { aggs: typeof topValuesBody } }
>;
@@ -260,7 +263,9 @@ export async function getDateHistogram(
const histogramBody = {
histo: { date_histogram: { field: field.name, fixed_interval: fixedInterval } },
};
- const results = (await aggSearchWithBody(histogramBody)) as AggregationSearchResponse<
+ const results = (await aggSearchWithBody(
+ histogramBody
+ )) as AggregationSearchResponseWithTotalHitsAsInt<
unknown,
{ body: { aggs: typeof histogramBody } }
>;
diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js
index fe6fbe9d8debf..b159cd02a7c28 100644
--- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js
+++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js
@@ -23,7 +23,7 @@ import {
unregisterCancelCallback
} from '../reducers/non_serializable_instances';
import { updateFlyout } from '../actions/ui_actions';
-import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants';
+import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN } from '../../common/constants';
export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER';
export const SET_TRANSIENT_LAYER = 'SET_TRANSIENT_LAYER';
@@ -203,11 +203,36 @@ function setLayerDataLoadErrorStatus(layerId, errorMessage) {
};
}
-export function clearTooltipStateForLayer(layerId) {
+export function cleanTooltipStateForLayer(layerId, layerFeatures = []) {
return (dispatch, getState) => {
const tooltipState = getTooltipState(getState());
- if (tooltipState && tooltipState.layerId === layerId) {
+
+ if (!tooltipState) {
+ return;
+ }
+
+ const nextTooltipFeatures = tooltipState.features.filter(tooltipFeature => {
+ if (tooltipFeature.layerId !== layerId) {
+ // feature from another layer, keep it
+ return true;
+ }
+
+ // Keep feature if it is still in layer
+ return layerFeatures.some(layerFeature => {
+ return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id;
+ });
+ });
+
+ if (tooltipState.features.length === nextTooltipFeatures.length) {
+ // no features got removed, nothing to update
+ return;
+ }
+
+ if (nextTooltipFeatures.length === 0) {
+ // all features removed from tooltip, close tooltip
dispatch(setTooltipState(null));
+ } else {
+ dispatch(setTooltipState({ ...tooltipState, features: nextTooltipFeatures }));
}
};
}
@@ -223,7 +248,7 @@ export function toggleLayerVisible(layerId) {
const makeVisible = !layer.isVisible();
if (!makeVisible) {
- dispatch(clearTooltipStateForLayer(layerId));
+ dispatch(cleanTooltipStateForLayer(layerId));
}
await dispatch({
@@ -456,7 +481,8 @@ export function updateSourceDataRequest(layerId, newData) {
export function endDataLoad(layerId, dataId, requestToken, data, meta) {
return async (dispatch) => {
dispatch(unregisterCancelCallback(requestToken));
- dispatch(clearTooltipStateForLayer(layerId));
+ const features = data ? data.features : [];
+ dispatch(cleanTooltipStateForLayer(layerId, features));
dispatch({
type: LAYER_DATA_LOAD_ENDED,
layerId,
@@ -478,7 +504,7 @@ export function endDataLoad(layerId, dataId, requestToken, data, meta) {
export function onDataLoadError(layerId, dataId, requestToken, errorMessage) {
return async (dispatch) => {
dispatch(unregisterCancelCallback(requestToken));
- dispatch(clearTooltipStateForLayer(layerId));
+ dispatch(cleanTooltipStateForLayer(layerId));
dispatch({
type: LAYER_DATA_LOAD_ERROR,
data: null,
@@ -599,7 +625,7 @@ export function removeLayer(layerId) {
layerGettingRemoved.getInFlightRequestTokens().forEach(requestToken => {
dispatch(cancelRequest(requestToken));
});
- dispatch(clearTooltipStateForLayer(layerId));
+ dispatch(cleanTooltipStateForLayer(layerId));
layerGettingRemoved.destroy();
dispatch({
type: REMOVE_LAYER,
diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js
index 10ef1ee517c3c..4a5715af64d0a 100644
--- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js
+++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js
@@ -11,6 +11,7 @@ import {
DECIMAL_DEGREES_PRECISION,
ES_GEO_FIELD_TYPE,
ES_SPATIAL_RELATIONS,
+ FEATURE_ID_PROPERTY_NAME,
GEO_JSON_TYPE,
POLYGON_COORDINATES_EXTERIOR_INDEX,
LON_INDEX,
@@ -74,6 +75,11 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) {
// don't include geometry field value in properties
delete properties[geoFieldName];
+ // _id is unique to Elasticsearch documents.
+ // Move _id to FEATURE_ID_PROPERTY_NAME to standardize featureId keys across all sources
+ properties[FEATURE_ID_PROPERTY_NAME] = properties._id;
+ delete properties._id;
+
//create new geojson Feature for every individual geojson geometry.
for (let j = 0; j < tmpGeometriesAccumulator.length; j++) {
features.push({
diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js
index fe65a307f8905..5f2ae9fd7b19a 100644
--- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js
+++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.test.js
@@ -7,6 +7,7 @@
jest.mock('ui/new_platform');
jest.mock('ui/index_patterns');
+import { FEATURE_ID_PROPERTY_NAME } from '../common/constants';
import {
hitsToGeoJson,
geoPointToGeometry,
@@ -15,7 +16,6 @@ import {
convertMapExtentToPolygon,
roundCoordinates,
} from './elasticsearch_geo_utils';
-
import { flattenHitWrapper } from 'ui/index_patterns';
const geoFieldName = 'location';
@@ -33,10 +33,33 @@ const flattenHitMock = hit => {
properties[fieldName] = hit._source[fieldName];
}
}
+ for (const fieldName in hit.fields) {
+ if (hit.fields.hasOwnProperty(fieldName)) {
+ properties[fieldName] = hit.fields[fieldName];
+ }
+ }
+ properties._id = hit._id;
+
return properties;
};
describe('hitsToGeoJson', () => {
+ it('Should set FEATURE_ID_PROPERTY_NAME to _id', () => {
+ const docId = 'if3mu20BBQNX22Q14Ppm';
+ const hits = [
+ {
+ _id: docId,
+ fields: {
+ [geoFieldName]: '20,100'
+ }
+ }
+ ];
+ const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point');
+ expect(geojson.type).toBe('FeatureCollection');
+ expect(geojson.features.length).toBe(1);
+ expect(geojson.features[0].properties[FEATURE_ID_PROPERTY_NAME]).toBe(docId);
+ });
+
it('Should convert elasitcsearch hits to geojson', () => {
const hits = [
{
@@ -63,7 +86,6 @@ describe('hitsToGeoJson', () => {
});
});
-
it('Should handle documents where geoField is not populated', () => {
const hits = [
{
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js
index 5faaf459dc314..3af1378e8e016 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js
@@ -7,7 +7,7 @@
import { AbstractVectorSource } from '../vector_source';
import { VECTOR_SHAPE_TYPES } from '../vector_feature_types';
import React from 'react';
-import { EMS_FILE } from '../../../../common/constants';
+import { EMS_FILE, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
import { getEMSClient } from '../../../meta';
import { EMSFileCreateSourceEditor } from './create_source_editor';
import { i18n } from '@kbn/i18n';
@@ -79,6 +79,16 @@ export class EMSFileSource extends AbstractVectorSource {
featureCollectionPath: 'data',
fetchUrl: emsFileLayer.getDefaultFormatUrl()
});
+
+ const emsIdField = emsFileLayer._config.fields.find(field => {
+ return field.type === 'id';
+ });
+ featureCollection.features.forEach((feature, index) => {
+ feature.properties[FEATURE_ID_PROPERTY_NAME] = emsIdField
+ ? feature.properties[emsIdField.id]
+ : index;
+ });
+
return {
data: featureCollection,
meta: {}
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js
index 11003013f351a..e06568285dd6b 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js
@@ -6,7 +6,7 @@
import { RENDER_AS } from './render_as';
import { getTileBoundingBox } from './geo_tile_utils';
-import { EMPTY_FEATURE_COLLECTION } from '../../../../common/constants';
+import { EMPTY_FEATURE_COLLECTION, FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
export function convertToGeoJson({ table, renderAs }) {
@@ -35,7 +35,9 @@ export function convertToGeoJson({ table, renderAs }) {
return;
}
- const properties = {};
+ const properties = {
+ [FEATURE_ID_PROPERTY_NAME]: gridKey
+ };
metricColumns.forEach(metricColumn => {
properties[metricColumn.aggConfig.id] = row[metricColumn.id];
});
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js
index ce5efb3b44176..c334776e6c4e8 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js
@@ -6,6 +6,8 @@
import _ from 'lodash';
+import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
+
const LAT_INDEX = 0;
const LON_INDEX = 1;
@@ -27,7 +29,7 @@ export function convertToLines(esResponse) {
const sourceBuckets = _.get(destBucket, 'sourceGrid.buckets', []);
for (let j = 0; j < sourceBuckets.length; j++) {
const {
- key, // eslint-disable-line no-unused-vars
+ key,
sourceCentroid,
...rest
} = sourceBuckets[j];
@@ -46,6 +48,7 @@ export function convertToLines(esResponse) {
coordinates: [[sourceCentroid.location.lon, sourceCentroid.location.lat], dest]
},
properties: {
+ [FEATURE_ID_PROPERTY_NAME]: `${dest.join()},${key}`,
...rest
}
});
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js
index 2fc0cf06b4020..502ed034a4416 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js
@@ -14,7 +14,13 @@ import { SearchSource } from '../../../kibana_services';
import { hitsToGeoJson } from '../../../elasticsearch_geo_utils';
import { CreateSourceEditor } from './create_source_editor';
import { UpdateSourceEditor } from './update_source_editor';
-import { ES_SEARCH, ES_GEO_FIELD_TYPE, ES_SIZE_LIMIT, SORT_ORDER } from '../../../../common/constants';
+import {
+ ES_SEARCH,
+ ES_GEO_FIELD_TYPE,
+ ES_SIZE_LIMIT,
+ FEATURE_ID_PROPERTY_NAME,
+ SORT_ORDER,
+} from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { ESTooltipProperty } from '../../tooltips/es_tooltip_property';
@@ -359,7 +365,7 @@ export class ESSearchSource extends AbstractESSource {
async filterAndFormatPropertiesToHtml(properties) {
const indexPattern = await this._getIndexPattern();
- const propertyValues = await this._loadTooltipProperties(properties._id, indexPattern);
+ const propertyValues = await this._loadTooltipProperties(properties[FEATURE_ID_PROPERTY_NAME], indexPattern);
return this._descriptor.tooltipProperties.map(propertyName => {
return new ESTooltipProperty(propertyName, propertyName, propertyValues[propertyName], indexPattern);
@@ -466,7 +472,7 @@ export class ESSearchSource extends AbstractESSource {
return {
index: properties._index, // Can not use index pattern title because it may reference many indices
- id: properties._id,
+ id: properties[FEATURE_ID_PROPERTY_NAME],
path: geoField.name,
};
}
diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js
index 0f52937f2211f..c75c5600aaf92 100644
--- a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js
+++ b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js
@@ -10,6 +10,7 @@ import { CreateSourceEditor } from './create_source_editor';
import { getKibanaRegionList } from '../../../meta';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
+import { FEATURE_ID_PROPERTY_NAME } from '../../../../common/constants';
export class KibanaRegionmapSource extends AbstractVectorSource {
@@ -80,6 +81,9 @@ export class KibanaRegionmapSource extends AbstractVectorSource {
featureCollectionPath: vectorFileMeta.meta.feature_collection_path,
fetchUrl: vectorFileMeta.url
});
+ featureCollection.features.forEach((feature, index) => {
+ feature.properties[FEATURE_ID_PROPERTY_NAME] = index;
+ });
return {
data: featureCollection
};
diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
index 829078d98996e..ffe2ce379436d 100644
--- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
+++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js
@@ -544,10 +544,8 @@ export class VectorLayer extends AbstractLayer {
for (let i = 0; i < featureCollection.features.length; i++) {
const id = randomizedIds[i];
const feature = featureCollection.features[i];
- feature.properties[FEATURE_ID_PROPERTY_NAME] = id;
- feature.id = id;
+ feature.id = id; // Mapbox feature state id, must be integer
}
-
}
async syncData(syncContext) {
diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.js b/x-pack/legacy/plugins/ml/common/util/job_utils.js
index 518e95b405648..2bc8bfaff566e 100644
--- a/x-pack/legacy/plugins/ml/common/util/job_utils.js
+++ b/x-pack/legacy/plugins/ml/common/util/job_utils.js
@@ -380,15 +380,15 @@ export function basicJobValidation(job, fields, limits, skipMmlChecks = false) {
messages.push({ id: 'bucket_span_empty' });
valid = false;
} else {
- const bucketSpan = parseInterval(job.analysis_config.bucket_span, false);
- if (bucketSpan === null || bucketSpan.asMilliseconds() === 0) {
- messages.push({ id: 'bucket_span_invalid' });
- valid = false;
- } else {
+ if (isValidTimeFormat(job.analysis_config.bucket_span)) {
messages.push({
id: 'bucket_span_valid',
bucketSpan: job.analysis_config.bucket_span
});
+
+ } else {
+ messages.push({ id: 'bucket_span_invalid' });
+ valid = false;
}
}
@@ -440,26 +440,29 @@ export function basicJobValidation(job, fields, limits, skipMmlChecks = false) {
export function basicDatafeedValidation(datafeed) {
const messages = [];
- const valid = true;
+ let valid = true;
if (datafeed) {
- // if (_.isEmpty(datafeed.query)) {
- // messages.push({ id: 'query_empty' });
- // valid = false;
- // } else if (isValidJson(datafeed.query) === false) {
- // messages.push({ id: 'query_invalid' });
- // valid = false;
- // } else {
- // messages.push({ id: 'query_valid' });
- // }
- messages.push({ id: 'query_delay_valid' });
+ let queryDelayMessage = { id: 'query_delay_valid' };
+ if (isValidTimeFormat(datafeed.query_delay) === false) {
+ queryDelayMessage = { id: 'query_delay_invalid' };
+ valid = false;
+ }
+ messages.push(queryDelayMessage);
+
+ let frequencyMessage = { id: 'frequency_valid' };
+ if (isValidTimeFormat(datafeed.frequency) === false) {
+ frequencyMessage = { id: 'frequency_invalid' };
+ valid = false;
+ }
+ messages.push(frequencyMessage);
}
return {
messages,
valid,
- contains: id => (messages.some(m => id === m.id)),
- find: id => (messages.find(m => id === m.id)),
+ contains: id => messages.some(m => id === m.id),
+ find: id => messages.find(m => id === m.id),
};
}
@@ -532,6 +535,14 @@ export function validateGroupNames(job) {
};
}
+function isValidTimeFormat(value) {
+ if (value === undefined) {
+ return true;
+ }
+ const interval = parseInterval(value, false);
+ return (interval !== null && interval.asMilliseconds() !== 0);
+}
+
// Returns the latest of the last source data and last processed bucket timestamp,
// as used for example in setting the end time of results views for cases where
// anomalies might have been raised after the point at which data ingest has stopped.
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx
index cf5a4cf957548..700be992a1080 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/directive.tsx
@@ -16,7 +16,7 @@ import { I18nContext } from 'ui/i18n';
import { IPrivate } from 'ui/private';
import { InjectorService } from '../../../../common/types/angular';
-import { SearchItemsProvider } from '../../../jobs/new_job/utils/new_job_utils';
+import { SearchItemsProvider } from '../../../jobs/new_job_new/utils/new_job_utils';
import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana';
diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx
index 38b7b6df18991..5eb1981e9a5aa 100644
--- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx
+++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/directive.tsx
@@ -16,7 +16,7 @@ import { I18nContext } from 'ui/i18n';
import { IPrivate } from 'ui/private';
import { InjectorService } from '../../../../common/types/angular';
-import { SearchItemsProvider } from '../../../jobs/new_job/utils/new_job_utils';
+import { SearchItemsProvider } from '../../../jobs/new_job_new/utils/new_job_utils';
import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana';
diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.js b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.js
index 413e41efebbc3..fa4e0c2654e97 100644
--- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.js
+++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer_directive.js
@@ -16,7 +16,7 @@ import { getFileDataVisualizerBreadcrumbs } from './breadcrumbs';
import { checkBasicLicense } from '../../license/check_license';
import { checkFindFileStructurePrivilege } from '../../privilege/check_privilege';
import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes';
-import { loadNewJobDefaults } from '../../jobs/new_job/utils/new_job_defaults';
+import { loadNewJobDefaults } from '../../jobs/new_job_new/utils/new_job_defaults';
import { loadIndexPatterns } from '../../util/index_utils';
import { FileDataVisualizerPage } from './file_datavisualizer';
diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx
index 720ae09369116..e621bdd1d8b92 100644
--- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx
+++ b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/directive.tsx
@@ -17,7 +17,7 @@ import { IPrivate } from 'ui/private';
import { InjectorService } from '../../../common/types/angular';
import { KibanaConfigTypeFix, KibanaContext } from '../../contexts/kibana/kibana_context';
-import { SearchItemsProvider } from '../../jobs/new_job/utils/new_job_utils';
+import { SearchItemsProvider } from '../../jobs/new_job_new/utils/new_job_utils';
import { Page } from './page';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js
index a5519314c7c68..9e26ea820f600 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_flyout.js
@@ -24,8 +24,8 @@ import {
import { toastNotifications } from 'ui/notify';
import { loadFullJob } from '../utils';
-import { mlCreateWatchService } from '../../../../jobs/new_job/simple/components/watcher/create_watch_service';
-import { CreateWatch } from '../../../../jobs/new_job/simple/components/watcher/create_watch_view';
+import { mlCreateWatchService } from './create_watch_service';
+import { CreateWatch } from './create_watch_view';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js
similarity index 95%
rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js
rename to x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js
index f114283eff102..d2a3ae009884f 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_service.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_service.js
@@ -7,17 +7,17 @@
import chrome from 'ui/chrome';
-import _ from 'lodash';
-import { http } from '../../../../../services/http_service';
+import { template } from 'lodash';
+import { http } from '../../../../services/http_service';
import emailBody from './email.html';
-import emailInfluencersBody from './email-influencers.html';
+import emailInfluencersBody from './email_influencers.html';
import { watch } from './watch.js';
import { i18n } from '@kbn/i18n';
-const compiledEmailBody = _.template(emailBody);
-const compiledEmailInfluencersBody = _.template(emailInfluencersBody);
+const compiledEmailBody = template(emailBody);
+const compiledEmailInfluencersBody = template(emailInfluencersBody);
const emailSection = {
send_email: {
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_view.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js
similarity index 96%
rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_view.js
rename to x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js
index bcd6bd733e118..716ece4b0e2dc 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_view.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/create_watch_view.js
@@ -27,9 +27,9 @@ import { has } from 'lodash';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
-import { parseInterval } from '../../../../../../common/util/parse_interval';
-import { ml } from '../../../../../services/ml_api_service';
-import { SelectSeverity } from '../../../../../components/controls/select_severity/select_severity';
+import { parseInterval } from '../../../../../common/util/parse_interval';
+import { ml } from '../../../../services/ml_api_service';
+import { SelectSeverity } from '../../../../components/controls/select_severity/select_severity';
import { mlCreateWatchService } from './create_watch_service';
const STATUS = mlCreateWatchService.STATUS;
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/email.html b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email.html
similarity index 100%
rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/email.html
rename to x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email.html
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/email-influencers.html b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email_influencers.html
similarity index 100%
rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/email-influencers.html
rename to x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/email_influencers.html
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/watch.js
similarity index 98%
rename from x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js
rename to x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/watch.js
index 447869ff8fdb4..c45894c36b702 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/watch.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/create_watch_flyout/watch.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ML_RESULTS_INDEX_PATTERN } from '../../../../../../common/constants/index_patterns';
+import { ML_RESULTS_INDEX_PATTERN } from '../../../../../common/constants/index_patterns';
export const watch = {
trigger: {
diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js
index b021b567777b8..8bd3d9228b857 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_utils.js
@@ -7,7 +7,7 @@
import { difference } from 'lodash';
import chrome from 'ui/chrome';
-import { newJobLimits } from 'plugins/ml/jobs/new_job/utils/new_job_defaults';
+import { newJobLimits } from 'plugins/ml/jobs/new_job_new/utils/new_job_defaults';
import { mlJobService } from 'plugins/ml/services/job_service';
import { processCreatedBy } from '../../../../../common/util/job_utils';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js
index c9e431dee846e..e7e99830372d8 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/tabs/datafeed.js
@@ -19,7 +19,7 @@ import {
} from '@elastic/eui';
import { calculateDatafeedFrequencyDefaultSeconds } from 'plugins/ml/../common/util/job_utils';
-import { newJobDefaults } from 'plugins/ml/jobs/new_job/utils/new_job_defaults';
+import { newJobDefaults } from 'plugins/ml/jobs/new_job_new/utils/new_job_defaults';
import { parseInterval } from 'plugins/ml/../common/util/parse_interval';
import { MLJobEditor } from '../../ml_job_editor';
import { FormattedMessage } from '@kbn/i18n/react';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js
index 98de6416cc577..05f9ec9d943f9 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/validate_job.js
@@ -5,8 +5,8 @@
*/
-import { newJobLimits } from '../../new_job/utils/new_job_defaults';
-import { populateValidationMessages } from '../../new_job/simple/components/utils/validate_job';
+import { newJobLimits } from '../../new_job_new/utils/new_job_defaults';
+import { populateValidationMessages } from '../../new_job_new/common/job_validator/util';
import {
validateModelMemoryLimit as validateModelMemoryLimitUtils,
diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js
index 0b347a35ce73a..3267c78deecc1 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/directive.js
@@ -16,7 +16,7 @@ import { checkFullLicense } from 'plugins/ml/license/check_license';
import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
import { getMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
import { getJobManagementBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs';
-import { loadNewJobDefaults } from 'plugins/ml/jobs/new_job/utils/new_job_defaults';
+import { loadNewJobDefaults } from 'plugins/ml/jobs/new_job_new/utils/new_job_defaults';
import uiRoutes from 'ui/routes';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_options_directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_options_directive.js
index f7aedd140c6af..73d66708bacca 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_options_directive.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_options_directive.js
@@ -8,7 +8,7 @@
import { postSaveService } from './post_save_service';
import { i18n } from '@kbn/i18n';
-import { mlCreateWatchService } from 'plugins/ml/jobs/new_job/simple/components/watcher/create_watch_service';
+// import { mlCreateWatchService } from 'plugins/ml/jobs/new_job/simple/components/watcher/create_watch_service';
import { xpackFeatureAvailable } from 'plugins/ml/license/check_license';
import template from './post_save_options.html';
@@ -30,9 +30,9 @@ module.directive('mlPostSaveOptions', function () {
$scope.status = postSaveService.status;
$scope.STATUS = postSaveService.STATUS;
- mlCreateWatchService.reset();
+ // mlCreateWatchService.reset();
- mlCreateWatchService.config.includeInfluencers = $scope.includeInfluencers;
+ // mlCreateWatchService.config.includeInfluencers = $scope.includeInfluencers;
$scope.runInRealtime = false;
$scope.createWatch = false;
$scope.embedded = true;
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_service.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_service.js
index a38a3cd689309..c910f9b3935e3 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_service.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/post_save_options/post_save_service.js
@@ -9,7 +9,7 @@
import { mlJobService } from 'plugins/ml/services/job_service';
import { i18n } from '@kbn/i18n';
-import { mlCreateWatchService } from 'plugins/ml/jobs/new_job/simple/components/watcher/create_watch_service';
+// import { mlCreateWatchService } from 'plugins/ml/jobs/new_job/simple/components/watcher/create_watch_service';
import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service';
const msgs = mlMessageBarService;
@@ -26,7 +26,7 @@ class PostSaveService {
realtimeJob: null,
watch: null
};
- mlCreateWatchService.status = this.status;
+ // mlCreateWatchService.status = this.status;
this.externalCreateWatch;
}
@@ -63,11 +63,11 @@ class PostSaveService {
this.startRealtimeJob(jobId, i18n)
.then(() => {
if (createWatch) {
- mlCreateWatchService.createNewWatch(jobId)
- .catch(() => {})
- .then(() => {
- resolve();
- });
+ // mlCreateWatchService.createNewWatch(jobId)
+ // .catch(() => {})
+ // .then(() => {
+ // resolve();
+ // });
} else {
resolve();
}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_directive.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_directive.js
deleted file mode 100644
index f463f50cbccb0..0000000000000
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/create_watch_directive.js
+++ /dev/null
@@ -1,73 +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 _ from 'lodash';
-import { parseInterval } from '../../../../../../common/util/parse_interval';
-import { mlCreateWatchService } from 'plugins/ml/jobs/new_job/simple/components/watcher/create_watch_service';
-
-import template from './create_watch.html';
-import { ml } from 'plugins/ml/services/ml_api_service';
-
-import { uiModules } from 'ui/modules';
-const module = uiModules.get('apps/ml');
-
-module.directive('mlCreateWatch', function () {
- return {
- restrict: 'AE',
- replace: false,
- scope: {
- jobId: '=',
- bucketSpan: '=',
- embedded: '='
- },
- template,
- link: function ($scope) {
- $scope.config = mlCreateWatchService.config;
- $scope.status = mlCreateWatchService.status;
- $scope.STATUS = mlCreateWatchService.STATUS;
-
- $scope.ui = {
- emailEnabled: false,
- embedded: $scope.embedded,
- watchAlreadyExists: false
- };
-
- // make the interval 2 times the bucket span
- if ($scope.bucketSpan) {
- const interval = parseInterval($scope.bucketSpan);
- let bs = interval.asMinutes() * 2;
- if (bs < 1) {
- bs = 1;
- }
- $scope.config.interval = `${bs}m`;
- }
-
- // load elasticsearch settings to see if email has been configured
- ml.getNotificationSettings()
- .then((resp) => {
- if (_.has(resp, 'defaults.xpack.notification.email')) {
- $scope.ui.emailEnabled = true;
- $scope.$applyAsync();
- }
- });
-
- // check to see whether a watch for this job has already been created.
- // display a warning if it has.
- mlCreateWatchService.loadWatch($scope.jobId)
- .then(() => {
- $scope.ui.watchAlreadyExists = true;
- })
- .catch(() => {
- $scope.ui.watchAlreadyExists = false;
- })
- .then(() => {
- $scope.$applyAsync();
- });
- }
- };
-});
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/index.js b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/index.js
index 7fc4d77b0d229..21be947a3a54b 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/index.js
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/simple/components/watcher/index.js
@@ -6,6 +6,4 @@
-import './create_watch_directive.js';
-import './create_watch_service.js';
import '../severity_control';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/advanced_job_creator.ts
index 510e4e5186e8e..ff42f391cb61a 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/advanced_job_creator.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/advanced_job_creator.ts
@@ -14,6 +14,7 @@ import { createBasicDetector } from './util/default_configs';
import { JOB_TYPE } from './util/constants';
import { getRichDetectors } from './util/general';
import { isValidJson } from '../../../../../common/util/validation_utils';
+import { ml } from '../../../../services/ml_api_service';
export interface RichDetector {
agg: Aggregation | null;
@@ -138,6 +139,21 @@ export class AdvancedJobCreator extends JobCreator {
return isValidJson(this._queryString);
}
+ // load the start and end times for the selected index
+ // and apply them to the job creator
+ public async autoSetTimeRange() {
+ try {
+ const { start, end } = await ml.getTimeFieldRange({
+ index: this._indexPatternTitle,
+ timeFieldName: this.timeFieldName,
+ query: this.query,
+ });
+ this.setTimeRange(start.epoch, end.epoch);
+ } catch (error) {
+ throw Error(error);
+ }
+ }
+
public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
this._overrideConfigs(job, datafeed);
const detectors = getRichDetectors(job, datafeed, true);
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts
index 88b7a66a623ed..358bbf67bee48 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts
@@ -6,7 +6,7 @@
import { ReactElement } from 'react';
import { basicJobValidation, basicDatafeedValidation } from '../../../../../common/util/job_utils';
-import { newJobLimits } from '../../../new_job/utils/new_job_defaults';
+import { newJobLimits } from '../../../new_job_new/utils/new_job_defaults';
import { JobCreatorType } from '../job_creator';
import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util';
import { ExistingJobsAndGroups } from '../../../../services/job_service';
@@ -42,6 +42,7 @@ export class JobValidator {
private _jobCreator: JobCreatorType;
private _validationSummary: ValidationSummary;
private _lastJobConfig: string;
+ private _lastDatafeedConfig: string;
private _validateTimeout: ReturnType | null = null;
private _existingJobsAndGroups: ExistingJobsAndGroups;
private _basicValidations: BasicValidations = {
@@ -60,6 +61,7 @@ export class JobValidator {
constructor(jobCreator: JobCreatorType, existingJobsAndGroups: ExistingJobsAndGroups) {
this._jobCreator = jobCreator;
this._lastJobConfig = this._jobCreator.formattedJobJson;
+ this._lastDatafeedConfig = this._jobCreator.formattedDatafeedJson;
this._validationSummary = {
basic: false,
advanced: false,
@@ -70,13 +72,19 @@ export class JobValidator {
public validate(callback: () => void, forceValidate: boolean = false) {
this._validating = true;
const formattedJobConfig = this._jobCreator.formattedJobJson;
+ const formattedDatafeedConfig = this._jobCreator.formattedDatafeedJson;
// only validate if the config has changed
- if (forceValidate || formattedJobConfig !== this._lastJobConfig) {
+ if (
+ forceValidate ||
+ formattedJobConfig !== this._lastJobConfig ||
+ formattedDatafeedConfig !== this._lastDatafeedConfig
+ ) {
if (this._validateTimeout !== null) {
// clear any previous on going validation timeouts
clearTimeout(this._validateTimeout);
}
this._lastJobConfig = formattedJobConfig;
+ this._lastDatafeedConfig = formattedDatafeedConfig;
this._validateTimeout = setTimeout(() => {
this._runBasicValidation();
this._validating = false;
@@ -107,10 +115,15 @@ export class JobValidator {
// run standard basic validation
const basicJobResults = basicJobValidation(jobConfig, undefined, limits);
- populateValidationMessages(basicJobResults, this._basicValidations, jobConfig);
+ populateValidationMessages(basicJobResults, this._basicValidations, jobConfig, datafeedConfig);
const basicDatafeedResults = basicDatafeedValidation(datafeedConfig);
- populateValidationMessages(basicDatafeedResults, this._basicValidations, jobConfig);
+ populateValidationMessages(
+ basicDatafeedResults,
+ this._basicValidations,
+ jobConfig,
+ datafeedConfig
+ );
// run addition job and group id validation
const idResults = checkForExistingJobAndGroupIds(
@@ -118,7 +131,7 @@ export class JobValidator {
this._jobCreator.groups,
this._existingJobsAndGroups
);
- populateValidationMessages(idResults, this._basicValidations, jobConfig);
+ populateValidationMessages(idResults, this._basicValidations, jobConfig, datafeedConfig);
this._validationSummary.basic = this._isOverallBasicValid();
}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/util.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/util.ts
index ca85e776fcf49..224d9ebf55823 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/util.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/util.ts
@@ -6,16 +6,17 @@
import { i18n } from '@kbn/i18n';
import { BasicValidations } from './job_validator';
-import { Job } from '../job_creator/configs';
+import { Job, Datafeed } from '../job_creator/configs';
import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../../../../../common/constants/validation';
-import { newJobLimits } from '../../../new_job/utils/new_job_defaults';
+import { newJobLimits } from '../../../new_job_new/utils/new_job_defaults';
import { ValidationResults, ValidationMessage } from '../../../../../common/util/job_utils';
import { ExistingJobsAndGroups } from '../../../../services/job_service';
export function populateValidationMessages(
validationResults: ValidationResults,
basicValidations: BasicValidations,
- jobConfig: Job
+ jobConfig: Job,
+ datafeedConfig: Datafeed
) {
const limits = newJobLimits();
@@ -102,12 +103,13 @@ export function populateValidationMessages(
if (validationResults.contains('model_memory_limit_invalid')) {
basicValidations.modelMemoryLimit.valid = false;
+ const maxModelMemoryLimit = (limits.max_model_memory_limit || '').toUpperCase();
const msg = i18n.translate(
'xpack.ml.newJob.wizard.validateJob.modelMemoryLimitRangeInvalidErrorMessage',
{
defaultMessage:
'Model memory limit cannot be higher than the maximum value of {maxModelMemoryLimit}',
- values: { maxModelMemoryLimit: limits.max_model_memory_limit.toUpperCase() },
+ values: { maxModelMemoryLimit },
}
);
basicValidations.modelMemoryLimit.message = msg;
@@ -136,20 +138,9 @@ export function populateValidationMessages(
basicValidations.bucketSpan.message = msg;
} else if (validationResults.contains('bucket_span_invalid')) {
basicValidations.bucketSpan.valid = false;
- const msg = i18n.translate(
- 'xpack.ml.newJob.wizard.validateJob.bucketSpanInvalidTimeIntervalFormatErrorMessage',
- {
- defaultMessage:
- '{bucketSpan} is not a valid time interval format e.g. {tenMinutes}, {oneHour}. It also needs to be higher than zero.',
- values: {
- bucketSpan: jobConfig.analysis_config.bucket_span,
- tenMinutes: '10m',
- oneHour: '1h',
- },
- }
+ basicValidations.bucketSpan.message = invalidTimeFormatMessage(
+ jobConfig.analysis_config.bucket_span
);
-
- basicValidations.bucketSpan.message = msg;
}
if (validationResults.contains('query_empty')) {
@@ -165,6 +156,16 @@ export function populateValidationMessages(
});
basicValidations.query.message = msg;
}
+
+ if (validationResults.contains('query_delay_invalid')) {
+ basicValidations.queryDelay.valid = false;
+ basicValidations.queryDelay.message = invalidTimeFormatMessage(datafeedConfig.query_delay);
+ }
+
+ if (validationResults.contains('frequency_invalid')) {
+ basicValidations.frequency.valid = false;
+ basicValidations.frequency.message = invalidTimeFormatMessage(datafeedConfig.frequency);
+ }
}
export function checkForExistingJobAndGroupIds(
@@ -196,3 +197,18 @@ export function checkForExistingJobAndGroupIds(
find: (id: string) => messages.find(m => id === m.id),
};
}
+
+function invalidTimeFormatMessage(value: string | undefined) {
+ return i18n.translate(
+ 'xpack.ml.newJob.wizard.validateJob.frequencyInvalidTimeIntervalFormatErrorMessage',
+ {
+ defaultMessage:
+ '{value} is not a valid time interval format e.g. {tenMinutes}, {oneHour}. It also needs to be higher than zero.',
+ values: {
+ value,
+ tenMinutes: '10m',
+ oneHour: '1h',
+ },
+ }
+ );
+}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/common/model_memory_limit/model_memory_limit_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/common/model_memory_limit/model_memory_limit_input.tsx
index b11395ea89759..3a3bd0c5a13a4 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/common/model_memory_limit/model_memory_limit_input.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/common/model_memory_limit/model_memory_limit_input.tsx
@@ -6,7 +6,7 @@
import React, { FC, useState, useContext, useEffect } from 'react';
import { EuiFieldText } from '@elastic/eui';
-import { newJobDefaults } from '../../../../../new_job/utils/new_job_defaults';
+import { newJobDefaults } from '../../../../../new_job_new/utils/new_job_defaults';
import { JobCreatorContext } from '../../job_creator_context';
import { Description } from './description';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx
index ed01e2bc464a7..bc3b90688a78a 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/datafeed_step/components/scroll_size/scroll_size_input.tsx
@@ -6,7 +6,7 @@
import React, { FC, useState, useContext, useEffect } from 'react';
import { EuiFieldNumber } from '@elastic/eui';
-import { newJobDefaults } from '../../../../../../new_job/utils/new_job_defaults';
+import { newJobDefaults } from '../../../../../../new_job_new/utils/new_job_defaults';
import { JobCreatorContext } from '../../../job_creator_context';
import { Description } from './description';
@@ -20,7 +20,7 @@ export const ScrollSizeInput: FC = () => {
);
const { datafeeds } = newJobDefaults();
- const { scroll_size: scrollSizeDefault } = datafeeds;
+ const scrollSizeDefault = datafeeds.scroll_size !== undefined ? `${datafeeds.scroll_size}` : '';
useEffect(() => {
jobCreator.scrollSize = scrollSizeString === '' ? null : +scrollSizeString;
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx
index 4ad689a943160..592dce3ce5b17 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/job_type/directive.tsx
@@ -17,7 +17,7 @@ import { I18nContext } from 'ui/i18n';
import { IPrivate } from 'ui/private';
import { InjectorService } from '../../../../../common/types/angular';
-import { SearchItemsProvider } from '../../../new_job/utils/new_job_utils';
+import { SearchItemsProvider } from '../../../new_job_new/utils/new_job_utils';
import { Page } from './page';
import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx
index 6513d9c69da1b..815e0228882ce 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/directive.tsx
@@ -17,7 +17,7 @@ import { I18nContext } from 'ui/i18n';
import { IPrivate } from 'ui/private';
import { InjectorService } from '../../../../../common/types/angular';
-import { SearchItemsProvider } from '../../../new_job/utils/new_job_utils';
+import { SearchItemsProvider } from '../../utils/new_job_utils';
import { Page, PageProps } from './page';
import { JOB_TYPE } from '../../common/job_creator/util/constants';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/page.tsx
index 0d25c2b99a66a..017fdf901ef93 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/page.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/page.tsx
@@ -7,9 +7,11 @@
import React, { FC, useEffect, Fragment } from 'react';
import { EuiPage, EuiPageBody, EuiPageContentBody } from '@elastic/eui';
+import { toastNotifications } from 'ui/notify';
+import { i18n } from '@kbn/i18n';
import { Wizard } from './wizard';
import { WIZARD_STEPS } from '../components/step_types';
-import { jobCreatorFactory } from '../../common/job_creator';
+import { jobCreatorFactory, isAdvancedJobCreator } from '../../common/job_creator';
import {
JOB_TYPE,
DEFAULT_MODEL_MEMORY_LIMIT,
@@ -96,6 +98,21 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => {
// ML job config holds no reference to the saved search ID.
jobCreator.createdBy = null;
}
+
+ if (isAdvancedJobCreator(jobCreator)) {
+ // for advanced jobs, load the full time range start and end times
+ // so they can be used for job validation and bucket span estimation
+ try {
+ jobCreator.autoSetTimeRange();
+ } catch (error) {
+ toastNotifications.addDanger({
+ title: i18n.translate('xpack.ml.newJob.wizard.autoSetJobCreatorTimeRange.error', {
+ defaultMessage: `Error retrieving beginning and end times of index`,
+ }),
+ text: error,
+ });
+ }
+ }
}
const chartInterval = new TimeBuckets();
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts
index 6301d38aee490..964dc1eee5140 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/route.ts
@@ -22,7 +22,7 @@ import { Route } from '../../../../../common/types/kibana';
import { loadNewJobCapabilities } from '../../../../services/new_job_capabilities_service';
-import { loadNewJobDefaults } from '../../../new_job/utils/new_job_defaults';
+import { loadNewJobDefaults } from '../../utils/new_job_defaults';
import { mlJobService } from '../../../../services/job_service';
import { JOB_TYPE } from '../../common/job_creator/util/constants';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx
index 5755a381aaadd..890db5e29387a 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx
@@ -20,6 +20,7 @@ import { JobValidator } from '../../common/job_validator';
import { newJobCapsService } from '../../../../services/new_job_capabilities_service';
import { WizardSteps } from './wizard_steps';
import { WizardHorizontalSteps } from './wizard_horizontal_steps';
+import { JOB_TYPE } from '../../common/job_creator/util/constants';
interface Props {
jobCreator: JobCreatorType;
@@ -62,8 +63,13 @@ export const Wizard: FC = ({
existingJobsAndGroups,
};
- const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.TIME_RANGE);
- const [highestStep, setHighestStep] = useState(WIZARD_STEPS.TIME_RANGE);
+ const firstStep =
+ jobCreator.type === JOB_TYPE.ADVANCED
+ ? WIZARD_STEPS.ADVANCED_CONFIGURE_DATAFEED
+ : WIZARD_STEPS.TIME_RANGE;
+
+ const [currentStep, setCurrentStep] = useState(firstStep);
+ const [highestStep, setHighestStep] = useState(firstStep);
const [disableSteps, setDisableSteps] = useState(false);
const [progress, setProgress] = useState(resultsLoader.progress);
const [stringifiedConfigs, setStringifiedConfigs] = useState(
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx
index 593db836b5ddb..cf80cad30b7ca 100644
--- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/recognize/directive.tsx
@@ -17,7 +17,7 @@ import { I18nContext } from 'ui/i18n';
import { IPrivate } from 'ui/private';
import { InjectorService } from '../../../../common/types/angular';
-import { SearchItemsProvider } from '../../new_job/utils/new_job_utils';
+import { SearchItemsProvider } from '../../new_job_new/utils/new_job_utils';
import { Page } from './page';
import { KibanaContext, KibanaConfigTypeFix } from '../../../contexts/kibana';
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_defaults.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_defaults.ts
new file mode 100644
index 0000000000000..c86a5a7861b64
--- /dev/null
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_defaults.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { ml } from '../../../services/ml_api_service';
+
+export interface MlServerDefaults {
+ anomaly_detectors: {
+ categorization_examples_limit?: number;
+ model_memory_limit?: string;
+ model_snapshot_retention_days?: number;
+ };
+ datafeeds: { scroll_size?: number };
+}
+
+export interface MlServerLimits {
+ max_model_memory_limit?: string;
+}
+
+let defaults: MlServerDefaults = {
+ anomaly_detectors: {},
+ datafeeds: {},
+};
+let limits: MlServerLimits = {};
+
+export async function loadNewJobDefaults() {
+ try {
+ const resp = await ml.mlInfo();
+ defaults = resp.defaults;
+ limits = resp.limits;
+ return { defaults, limits };
+ } catch (error) {
+ return { defaults, limits };
+ }
+}
+
+export function newJobDefaults(): MlServerDefaults {
+ return defaults;
+}
+
+export function newJobLimits(): MlServerLimits {
+ return limits;
+}
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_utils.ts
new file mode 100644
index 0000000000000..0a37cff050eb0
--- /dev/null
+++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/utils/new_job_utils.ts
@@ -0,0 +1,101 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types';
+import { IndexPattern } from 'ui/index_patterns';
+import { buildEsQuery, getEsQueryConfig } from '@kbn/es-query';
+import { KibanaConfigTypeFix } from '../../../contexts/kibana';
+import { InjectorService } from '../../../../common/types/angular';
+
+export interface SearchItems {
+ indexPattern: IndexPattern;
+ savedSearch: SavedSearch;
+ query: any;
+ combinedQuery: any;
+}
+
+// Provider for creating the items used for searching and job creation.
+// Uses the $route object to retrieve the indexPattern and savedSearch from the url
+export function SearchItemsProvider($injector: InjectorService) {
+ const kibanaConfig = $injector.get('config');
+ const $route = $injector.get('$route');
+
+ function createSearchItems() {
+ let indexPattern = $route.current.locals.indexPattern;
+
+ // query is only used by the data visualizer as it needs
+ // a lucene query_string.
+ // Using a blank query will cause match_all:{} to be used
+ // when passed through luceneStringToDsl
+ let query = {
+ query: '',
+ language: 'lucene',
+ };
+
+ let combinedQuery: any = {
+ bool: {
+ must: [
+ {
+ match_all: {},
+ },
+ ],
+ },
+ };
+
+ const savedSearch = $route.current.locals.savedSearch;
+ if (indexPattern.id === undefined && savedSearch.id !== undefined) {
+ const searchSource = savedSearch.searchSource;
+ indexPattern = searchSource.getField('index');
+
+ query = searchSource.getField('query');
+ const fs = searchSource.getField('filter');
+
+ const filters = fs.length ? fs : [];
+
+ const esQueryConfigs = getEsQueryConfig(kibanaConfig);
+ combinedQuery = buildEsQuery(indexPattern, [query], filters, esQueryConfigs);
+ }
+
+ return {
+ indexPattern,
+ savedSearch,
+ query,
+ combinedQuery,
+ };
+ }
+
+ return createSearchItems;
+}
+
+// Only model plot cardinality relevant
+// format:[{id:"cardinality_model_plot_high",modelPlotCardinality:11405}, {id:"cardinality_partition_field",fieldName:"clientip"}]
+interface CheckCardinalitySuccessResponse {
+ success: boolean;
+ highCardinality?: any;
+}
+export function checkCardinalitySuccess(data: any) {
+ const response: CheckCardinalitySuccessResponse = {
+ success: true,
+ };
+ // There were no fields to run cardinality on.
+ if (Array.isArray(data) && data.length === 0) {
+ return response;
+ }
+
+ for (let i = 0; i < data.length; i++) {
+ if (data[i].id === 'success_cardinality') {
+ break;
+ }
+
+ if (data[i].id === 'cardinality_model_plot_high') {
+ response.success = false;
+ response.highCardinality = data[i].modelPlotCardinality;
+ break;
+ }
+ }
+
+ return response;
+}
diff --git a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts
index b63919041e139..7962fedd20fee 100644
--- a/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts
+++ b/x-pack/legacy/plugins/ml/public/services/ml_api_service/index.d.ts
@@ -9,6 +9,7 @@ import { AggFieldNamePair } from '../../../common/types/fields';
import { ExistingJobsAndGroups } from '../job_service';
import { PrivilegesResponse } from '../../../common/types/privileges';
import { MlSummaryJobs } from '../../../common/types/jobs';
+import { MlServerDefaults, MlServerLimits } from '../../jobs/new_job_new/utils/new_job_defaults';
// TODO This is not a complete representation of all methods of `ml.*`.
// It just satisfies needs for other parts of the code area which use
@@ -24,6 +25,16 @@ export interface GetTimeFieldRangeResponse {
end: { epoch: number; string: string };
}
+export interface MlInfoResponse {
+ defaults: MlServerDefaults;
+ limits: MlServerLimits;
+ native_code: {
+ build_hash: string;
+ version: string;
+ };
+ upgrade_mode: boolean;
+}
+
declare interface Ml {
annotations: {
deleteAnnotation(id: string | undefined): Promise;
@@ -119,6 +130,7 @@ declare interface Ml {
): Promise<{ name: string; ms: number; error?: boolean; message?: { msg: string } | string }>;
mlNodeCount(): Promise<{ count: number }>;
+ mlInfo(): Promise;
}
declare const ml: Ml;
diff --git a/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts b/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts
index 0ebce3d1451bf..115e7fe6ba434 100644
--- a/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts
+++ b/x-pack/legacy/plugins/ml/server/lib/spaces_utils.ts
@@ -5,7 +5,7 @@
*/
import { Request } from 'hapi';
-import { SpacesPlugin } from '../../../spaces';
+import { LegacySpacesPlugin } from '../../../spaces';
import { Space } from '../../../spaces/common/model/space';
interface GetActiveSpaceResponse {
@@ -13,7 +13,7 @@ interface GetActiveSpaceResponse {
space?: Space;
}
-export function spacesUtilsProvider(spacesPlugin: SpacesPlugin, request: Request) {
+export function spacesUtilsProvider(spacesPlugin: LegacySpacesPlugin, request: Request) {
async function activeSpace(): Promise {
try {
return {
diff --git a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
index fd33bc79eeaf8..3a872b4c1e327 100644
--- a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
+++ b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
@@ -68,7 +68,7 @@ class GetCsvReportPanelAction implements IAction {
return {};
}
- return searchEmbeddable.searchScope.searchSource.getSearchRequestBody();
+ return searchEmbeddable.getSavedSearch().searchSource.getSearchRequestBody();
}
public isCompatible = async (context: ActionContext) => {
diff --git a/x-pack/legacy/plugins/security/common/constants.ts b/x-pack/legacy/plugins/security/common/constants.ts
index 2a255ecd335e5..2ec429b4d9c4c 100644
--- a/x-pack/legacy/plugins/security/common/constants.ts
+++ b/x-pack/legacy/plugins/security/common/constants.ts
@@ -8,3 +8,4 @@ export const GLOBAL_RESOURCE = '*';
export const IGNORED_TYPES = ['space'];
export const APPLICATION_PREFIX = 'kibana-';
export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*';
+export const INTERNAL_API_BASE_PATH = '/internal/security';
diff --git a/x-pack/legacy/plugins/security/common/model/api_key.ts b/x-pack/legacy/plugins/security/common/model/api_key.ts
new file mode 100644
index 0000000000000..acdf999da4a0f
--- /dev/null
+++ b/x-pack/legacy/plugins/security/common/model/api_key.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface ApiKey {
+ id: string;
+ name: string;
+ username: string;
+ realm: string;
+ creation: number;
+ expiration: number;
+ invalidated: boolean;
+}
+
+export interface ApiKeyToInvalidate {
+ id: string;
+ name: string;
+}
diff --git a/x-pack/legacy/plugins/security/common/model/index.ts b/x-pack/legacy/plugins/security/common/model/index.ts
index 31757543ac3f8..19243c25fef7e 100644
--- a/x-pack/legacy/plugins/security/common/model/index.ts
+++ b/x-pack/legacy/plugins/security/common/model/index.ts
@@ -8,6 +8,7 @@ export { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role';
export { FeaturesPrivileges } from './features_privileges';
export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges';
export { KibanaPrivileges } from './kibana_privileges';
+export { ApiKey } from './api_key';
export { User, EditUser, getUserDisplayName } from '../../../../../plugins/security/common/model';
export {
AuthenticatedUser,
diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js
index 980af19cc8362..f9e82f575ce2e 100644
--- a/x-pack/legacy/plugins/security/index.js
+++ b/x-pack/legacy/plugins/security/index.js
@@ -7,6 +7,7 @@
import { resolve } from 'path';
import { initAuthenticateApi } from './server/routes/api/v1/authenticate';
import { initUsersApi } from './server/routes/api/v1/users';
+import { initApiKeysApi } from './server/routes/api/v1/api_keys';
import { initExternalRolesApi } from './server/routes/api/external/roles';
import { initPrivilegesApi } from './server/routes/api/external/privileges';
import { initIndicesApi } from './server/routes/api/v1/indices';
@@ -195,6 +196,7 @@ export const security = (kibana) => new kibana.Plugin({
initAPIAuthorization(server, authorization);
initAppAuthorization(server, xpackMainPlugin, authorization);
initUsersApi(securityPlugin, server);
+ initApiKeysApi(server);
initExternalRolesApi(server);
initIndicesApi(server);
initPrivilegesApi(server);
diff --git a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts
new file mode 100644
index 0000000000000..c6dcef392af98
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts
@@ -0,0 +1,48 @@
+/*
+ * 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 { kfetch } from 'ui/kfetch';
+import { ApiKey, ApiKeyToInvalidate } from '../../common/model/api_key';
+import { INTERNAL_API_BASE_PATH } from '../../common/constants';
+
+interface CheckPrivilegesResponse {
+ areApiKeysEnabled: boolean;
+ isAdmin: boolean;
+}
+
+interface InvalidateApiKeysResponse {
+ itemsInvalidated: ApiKeyToInvalidate[];
+ errors: any[];
+}
+
+interface GetApiKeysResponse {
+ apiKeys: ApiKey[];
+}
+
+const apiKeysUrl = `${INTERNAL_API_BASE_PATH}/api_key`;
+
+export class ApiKeysApi {
+ public static async checkPrivileges(): Promise {
+ return kfetch({ pathname: `${apiKeysUrl}/privileges` });
+ }
+
+ public static async getApiKeys(isAdmin: boolean = false): Promise {
+ const query = {
+ isAdmin,
+ };
+
+ return kfetch({ pathname: apiKeysUrl, query });
+ }
+
+ public static async invalidateApiKeys(
+ apiKeys: ApiKeyToInvalidate[],
+ isAdmin: boolean = false
+ ): Promise {
+ const pathname = `${apiKeysUrl}/invalidate`;
+ const body = JSON.stringify({ apiKeys, isAdmin });
+ return kfetch({ pathname, method: 'POST', body });
+ }
+}
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html
new file mode 100644
index 0000000000000..e46c6f72b5d20
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js
new file mode 100644
index 0000000000000..f143df8c9742f
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js
@@ -0,0 +1,33 @@
+/*
+ * 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 { render, unmountComponentAtNode } from 'react-dom';
+import routes from 'ui/routes';
+import template from './api_keys.html';
+import { API_KEYS_PATH } from '../management_urls';
+import { getApiKeysBreadcrumbs } from '../breadcrumbs';
+import { I18nContext } from 'ui/i18n';
+import { ApiKeysGridPage } from './components';
+
+routes.when(API_KEYS_PATH, {
+ template,
+ k7Breadcrumbs: getApiKeysBreadcrumbs,
+ controller($scope) {
+ $scope.$$postDigest(() => {
+ const domNode = document.getElementById('apiKeysGridReactRoot');
+
+ render(
+
+
+ , domNode);
+
+ // unmount react on controller destroy
+ $scope.$on('$destroy', () => {
+ unmountComponentAtNode(domNode);
+ });
+ });
+ },
+});
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx
new file mode 100644
index 0000000000000..6bebf17c943a4
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx
@@ -0,0 +1,528 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Component } from 'react';
+import {
+ EuiBadge,
+ EuiButton,
+ EuiButtonIcon,
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiInMemoryTable,
+ EuiPageContent,
+ EuiPageContentBody,
+ EuiPageContentHeader,
+ EuiPageContentHeaderSection,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+ EuiToolTip,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import moment from 'moment-timezone';
+import _ from 'lodash';
+import { toastNotifications } from 'ui/notify';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { SectionLoading } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/section_loading';
+import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model/api_key';
+import { ApiKeysApi } from '../../../../lib/api_keys_api';
+import { PermissionDenied } from './permission_denied';
+import { EmptyPrompt } from './empty_prompt';
+import { NotEnabled } from './not_enabled';
+import { InvalidateProvider } from './invalidate_provider';
+
+interface State {
+ isLoadingApp: boolean;
+ isLoadingTable: boolean;
+ isAdmin: boolean;
+ areApiKeysEnabled: boolean;
+ apiKeys: ApiKey[];
+ selectedItems: ApiKey[];
+ permissionDenied: boolean;
+ error: any;
+}
+
+const DATE_FORMAT = 'MMMM Do YYYY HH:mm:ss';
+
+export class ApiKeysGridPage extends Component {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ isLoadingApp: true,
+ isLoadingTable: false,
+ isAdmin: false,
+ areApiKeysEnabled: false,
+ apiKeys: [],
+ permissionDenied: false,
+ selectedItems: [],
+ error: undefined,
+ };
+ }
+
+ public componentDidMount() {
+ this.checkPrivileges();
+ }
+
+ public render() {
+ const {
+ permissionDenied,
+ isLoadingApp,
+ isLoadingTable,
+ areApiKeysEnabled,
+ isAdmin,
+ error,
+ apiKeys,
+ } = this.state;
+
+ if (permissionDenied) {
+ return ;
+ }
+
+ if (isLoadingApp) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (error) {
+ const {
+ body: { error: errorTitle, message, statusCode },
+ } = error;
+
+ return (
+
+
+ }
+ color="danger"
+ iconType="alert"
+ >
+ {statusCode}: {errorTitle} - {message}
+
+
+ );
+ }
+
+ if (!areApiKeysEnabled) {
+ return (
+
+
+
+ );
+ }
+
+ if (!isLoadingTable && apiKeys && apiKeys.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ const description = (
+
+
+ {isAdmin ? (
+
+ ) : (
+
+ )}
+
+
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ {description}
+
+
+
+ {this.renderTable()}
+
+ );
+ }
+
+ private renderTable = () => {
+ const { apiKeys, selectedItems, isLoadingTable, isAdmin } = this.state;
+
+ const message = isLoadingTable ? (
+
+ ) : (
+ undefined
+ );
+
+ const sorting = {
+ sort: {
+ field: 'expiration',
+ direction: 'asc',
+ },
+ };
+
+ const pagination = {
+ initialPageSize: 20,
+ pageSizeOptions: [10, 20, 50],
+ };
+
+ const selection = {
+ onSelectionChange: (newSelectedItems: ApiKey[]) => {
+ this.setState({
+ selectedItems: newSelectedItems,
+ });
+ },
+ };
+
+ const search = {
+ toolsLeft: selectedItems.length ? (
+
+ {invalidateApiKeyPrompt => {
+ return (
+
+ invalidateApiKeyPrompt(
+ selectedItems.map(({ name, id }) => ({ name, id })),
+ this.onApiKeysInvalidated
+ )
+ }
+ color="danger"
+ data-test-subj="bulkInvalidateActionButton"
+ >
+
+
+ );
+ }}
+
+ ) : (
+ undefined
+ ),
+ toolsRight: (
+ this.reloadApiKeys()}
+ data-test-subj="reloadButton"
+ >
+
+
+ ),
+ box: {
+ incremental: true,
+ },
+ filters: isAdmin
+ ? [
+ {
+ type: 'field_value_selection',
+ field: 'username',
+ name: i18n.translate('xpack.security.management.apiKeys.table.userFilterLabel', {
+ defaultMessage: 'User',
+ }),
+ multiSelect: false,
+ options: Object.keys(
+ apiKeys.reduce((apiKeysMap: any, apiKey) => {
+ apiKeysMap[apiKey.username] = true;
+ return apiKeysMap;
+ }, {})
+ ).map(username => {
+ return {
+ value: username,
+ view: username,
+ };
+ }),
+ },
+ {
+ type: 'field_value_selection',
+ field: 'realm',
+ name: i18n.translate('xpack.security.management.apiKeys.table.realmFilterLabel', {
+ defaultMessage: 'Realm',
+ }),
+ multiSelect: false,
+ options: Object.keys(
+ apiKeys.reduce((apiKeysMap: any, apiKey) => {
+ apiKeysMap[apiKey.realm] = true;
+ return apiKeysMap;
+ }, {})
+ ).map(realm => {
+ return {
+ value: realm,
+ view: realm,
+ };
+ }),
+ },
+ ]
+ : undefined,
+ };
+
+ return (
+ <>
+ {isAdmin ? (
+ <>
+
+ }
+ color="success"
+ iconType="user"
+ size="s"
+ />
+
+
+ >
+ ) : (
+ undefined
+ )}
+
+ {
+ {
+ return {
+ 'data-test-subj': 'apiKeyRow',
+ };
+ }}
+ />
+ }
+ >
+ );
+ };
+
+ private getColumnConfig = () => {
+ const { isAdmin } = this.state;
+
+ let config = [
+ {
+ field: 'name',
+ name: i18n.translate('xpack.security.management.apiKeys.table.nameColumnName', {
+ defaultMessage: 'Name',
+ }),
+ sortable: true,
+ },
+ ];
+
+ if (isAdmin) {
+ config = config.concat([
+ {
+ field: 'username',
+ name: i18n.translate('xpack.security.management.apiKeys.table.userNameColumnName', {
+ defaultMessage: 'User',
+ }),
+ sortable: true,
+ },
+ {
+ field: 'realm',
+ name: i18n.translate('xpack.security.management.apiKeys.table.realmColumnName', {
+ defaultMessage: 'Realm',
+ }),
+ sortable: true,
+ },
+ ]);
+ }
+
+ config = config.concat([
+ {
+ field: 'creation',
+ name: i18n.translate('xpack.security.management.apiKeys.table.creationDateColumnName', {
+ defaultMessage: 'Created',
+ }),
+ sortable: true,
+ // @ts-ignore
+ render: (creationDateMs: number) => moment(creationDateMs).format(DATE_FORMAT),
+ },
+ {
+ field: 'expiration',
+ name: i18n.translate('xpack.security.management.apiKeys.table.expirationDateColumnName', {
+ defaultMessage: 'Expires',
+ }),
+ sortable: true,
+ // @ts-ignore
+ render: (expirationDateMs: number) => {
+ if (expirationDateMs === undefined) {
+ return (
+
+ {i18n.translate(
+ 'xpack.security.management.apiKeys.table.expirationDateNeverMessage',
+ {
+ defaultMessage: 'Never',
+ }
+ )}
+
+ );
+ }
+
+ return moment(expirationDateMs).format(DATE_FORMAT);
+ },
+ },
+ {
+ name: i18n.translate('xpack.security.management.apiKeys.table.statusColumnName', {
+ defaultMessage: 'Status',
+ }),
+ render: ({ expiration }: any) => {
+ const now = Date.now();
+
+ if (now > expiration) {
+ return Expired ;
+ }
+
+ return Active ;
+ },
+ },
+ {
+ name: i18n.translate('xpack.security.management.apiKeys.table.actionsColumnName', {
+ defaultMessage: 'Actions',
+ }),
+ actions: [
+ {
+ render: ({ name, id }: any) => {
+ return (
+
+
+
+ {invalidateApiKeyPrompt => {
+ return (
+
+
+ invalidateApiKeyPrompt([{ id, name }], this.onApiKeysInvalidated)
+ }
+ />
+
+ );
+ }}
+
+
+
+ );
+ },
+ },
+ ],
+ },
+ ]);
+
+ return config;
+ };
+
+ private onApiKeysInvalidated = (apiKeysInvalidated: ApiKeyToInvalidate[]): void => {
+ if (apiKeysInvalidated.length) {
+ this.reloadApiKeys();
+ }
+ };
+
+ private async checkPrivileges() {
+ try {
+ const { isAdmin, areApiKeysEnabled } = await ApiKeysApi.checkPrivileges();
+ this.setState({ isAdmin, areApiKeysEnabled });
+
+ if (areApiKeysEnabled) {
+ this.initiallyLoadApiKeys();
+ } else {
+ // We're done loading and will just show the "Disabled" error.
+ this.setState({ isLoadingApp: false });
+ }
+ } catch (e) {
+ if (_.get(e, 'body.statusCode') === 403) {
+ this.setState({ permissionDenied: true, isLoadingApp: false });
+ } else {
+ toastNotifications.addDanger(
+ this.props.i18n.translate(
+ 'xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage',
+ {
+ defaultMessage: 'Error checking privileges: {message}',
+ values: { message: _.get(e, 'body.message', '') },
+ }
+ )
+ );
+ }
+ }
+ }
+
+ private initiallyLoadApiKeys = () => {
+ this.setState({ isLoadingApp: true, isLoadingTable: false });
+ this.loadApiKeys();
+ };
+
+ private reloadApiKeys = () => {
+ this.setState({ apiKeys: [], isLoadingApp: false, isLoadingTable: true });
+ this.loadApiKeys();
+ };
+
+ private loadApiKeys = async () => {
+ try {
+ const { isAdmin } = this.state;
+ const { apiKeys } = await ApiKeysApi.getApiKeys(isAdmin);
+ this.setState({ apiKeys });
+ } catch (e) {
+ this.setState({ error: e });
+ }
+
+ this.setState({ isLoadingApp: false, isLoadingTable: false });
+ };
+}
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx
new file mode 100644
index 0000000000000..957ca7010a1a0
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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, { Fragment } from 'react';
+import { EuiEmptyPrompt, EuiButton, EuiLink } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { documentationLinks } from '../../services/documentation_links';
+
+interface Props {
+ isAdmin: boolean;
+}
+
+export const EmptyPrompt: React.FunctionComponent = ({ isAdmin }) => (
+
+ {isAdmin ? (
+
+ ) : (
+
+ )}
+
+ }
+ body={
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+ }
+ actions={
+
+
+
+ }
+ data-test-subj="emptyPrompt"
+ />
+);
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/index.ts b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/index.ts
new file mode 100644
index 0000000000000..982e34a0ceed5
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/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 { EmptyPrompt } from './empty_prompt';
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/index.ts b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/index.ts
new file mode 100644
index 0000000000000..9f4d4239d6b4c
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/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 { ApiKeysGridPage } from './api_keys_grid_page';
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/index.ts b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/index.ts
new file mode 100644
index 0000000000000..17bfb41fa88b5
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/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 { InvalidateProvider } from './invalidate_provider';
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx
new file mode 100644
index 0000000000000..fe9ffc651db29
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx
@@ -0,0 +1,182 @@
+/*
+ * 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, { Fragment, useRef, useState } from 'react';
+import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
+import { toastNotifications } from 'ui/notify';
+import { i18n } from '@kbn/i18n';
+import { ApiKeyToInvalidate } from '../../../../../../common/model/api_key';
+import { ApiKeysApi } from '../../../../../lib/api_keys_api';
+
+interface Props {
+ isAdmin: boolean;
+ children: (invalidateApiKeys: InvalidateApiKeys) => React.ReactElement;
+}
+
+export type InvalidateApiKeys = (
+ apiKeys: ApiKeyToInvalidate[],
+ onSuccess?: OnSuccessCallback
+) => void;
+
+type OnSuccessCallback = (apiKeysInvalidated: ApiKeyToInvalidate[]) => void;
+
+export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, children }) => {
+ const [apiKeys, setApiKeys] = useState([]);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const onSuccessCallback = useRef(null);
+
+ const invalidateApiKeyPrompt: InvalidateApiKeys = (keys, onSuccess = () => undefined) => {
+ if (!keys || !keys.length) {
+ throw new Error('No API key IDs specified for invalidation');
+ }
+ setIsModalOpen(true);
+ setApiKeys(keys);
+ onSuccessCallback.current = onSuccess;
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ setApiKeys([]);
+ };
+
+ const invalidateApiKey = async () => {
+ let result;
+ let error;
+ let errors;
+
+ try {
+ result = await ApiKeysApi.invalidateApiKeys(apiKeys, isAdmin);
+ } catch (e) {
+ error = e;
+ }
+
+ closeModal();
+
+ if (result) {
+ const { itemsInvalidated } = result;
+ ({ errors } = result);
+
+ // Surface success notifications
+ if (itemsInvalidated && itemsInvalidated.length) {
+ const hasMultipleSuccesses = itemsInvalidated.length > 1;
+ const successMessage = hasMultipleSuccesses
+ ? i18n.translate(
+ 'xpack.security.management.apiKeys.invalidateApiKey.successMultipleNotificationTitle',
+ {
+ defaultMessage: 'Invalidated {count} API keys',
+ values: { count: itemsInvalidated.length },
+ }
+ )
+ : i18n.translate(
+ 'xpack.security.management.apiKeys.invalidateApiKey.successSingleNotificationTitle',
+ {
+ defaultMessage: "Invalidated API key '{name}'",
+ values: { name: itemsInvalidated[0].name },
+ }
+ );
+ toastNotifications.addSuccess(successMessage);
+ if (onSuccessCallback.current) {
+ onSuccessCallback.current([...itemsInvalidated]);
+ }
+ }
+ }
+
+ // Surface error notifications
+ // `error` is generic server error
+ // `errors` are specific errors with removing particular API keys
+ if (error || (errors && errors.length)) {
+ const hasMultipleErrors = (errors && errors.length > 1) || (error && apiKeys.length > 1);
+ const errorMessage = hasMultipleErrors
+ ? i18n.translate(
+ 'xpack.security.management.apiKeys.invalidateApiKey.errorMultipleNotificationTitle',
+ {
+ defaultMessage: 'Error deleting {count} apiKeys',
+ values: {
+ count: (errors && errors.length) || apiKeys.length,
+ },
+ }
+ )
+ : i18n.translate(
+ 'xpack.security.management.apiKeys.invalidateApiKey.errorSingleNotificationTitle',
+ {
+ defaultMessage: "Error deleting API key '{name}'",
+ values: { name: (errors && errors[0].name) || apiKeys[0].name },
+ }
+ );
+ toastNotifications.addDanger(errorMessage);
+ }
+ };
+
+ const renderModal = () => {
+ if (!isModalOpen) {
+ return null;
+ }
+
+ const isSingle = apiKeys.length === 1;
+
+ return (
+
+
+ {!isSingle ? (
+
+
+ {i18n.translate(
+ 'xpack.security.management.apiKeys.invalidateApiKey.confirmModal.invalidateMultipleListDescription',
+ { defaultMessage: 'You are about to invalidate these API keys:' }
+ )}
+
+
+ {apiKeys.map(({ name, id }) => (
+ {name}
+ ))}
+
+
+ ) : null}
+
+
+ );
+ };
+
+ return (
+
+ {children(invalidateApiKeyPrompt)}
+ {renderModal()}
+
+ );
+};
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts
new file mode 100644
index 0000000000000..faa788342fefa
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/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 { NotEnabled } from './not_enabled';
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx
new file mode 100644
index 0000000000000..c419e15450c1e
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiCallOut, EuiLink } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { documentationLinks } from '../../services/documentation_links';
+
+export const NotEnabled: React.FunctionComponent = () => (
+
+ }
+ color="danger"
+ iconType="alert"
+ >
+
+
+
+ ),
+ }}
+ />
+
+);
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/permission_denied/index.ts b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/permission_denied/index.ts
new file mode 100644
index 0000000000000..8b0bc67f3f777
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/permission_denied/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 { PermissionDenied } from './permission_denied';
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/permission_denied/permission_denied.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/permission_denied/permission_denied.tsx
new file mode 100644
index 0000000000000..d406b1684b3ff
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/permission_denied/permission_denied.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { EuiEmptyPrompt, EuiFlexGroup, EuiPageContent } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React from 'react';
+
+export const PermissionDenied = () => (
+
+
+
+
+
+ }
+ body={
+
+
+
+ }
+ />
+
+
+);
diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts
new file mode 100644
index 0000000000000..1f03763eb542a
--- /dev/null
+++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.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 { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links';
+
+class DocumentationLinksService {
+ private esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
+
+ public getApiKeyServiceSettingsDocUrl(): string {
+ return `${this.esDocBasePath}security-settings.html#api-key-service-settings`;
+ }
+
+ public getCreateApiKeyDocUrl(): string {
+ return `${this.esDocBasePath}security-api-create-api-key.html`;
+ }
+}
+
+export const documentationLinks = new DocumentationLinksService();
diff --git a/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts b/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts
index a77c8317c1a2c..7d345ac13dc41 100644
--- a/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts
+++ b/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts
@@ -74,3 +74,15 @@ export function getCreateRoleBreadcrumbs() {
},
];
}
+
+export function getApiKeysBreadcrumbs() {
+ return [
+ MANAGEMENT_BREADCRUMB,
+ {
+ text: i18n.translate('xpack.security.apiKeys.breadcrumb', {
+ defaultMessage: 'API Keys',
+ }),
+ href: '#/management/security/api_keys',
+ },
+ ];
+}
diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx
index 427fb621d5ddd..282ce4eea160c 100644
--- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx
+++ b/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx
@@ -418,165 +418,159 @@ class EditUserPageUI extends Component {
/>
) : null}
-
+ )}
+
+ )}
+
diff --git a/x-pack/legacy/plugins/security/public/views/management/management.js b/x-pack/legacy/plugins/security/public/views/management/management.js
index 7cce644553380..8417191b4ee67 100644
--- a/x-pack/legacy/plugins/security/public/views/management/management.js
+++ b/x-pack/legacy/plugins/security/public/views/management/management.js
@@ -8,12 +8,13 @@ import 'plugins/security/views/management/change_password_form/change_password_f
import 'plugins/security/views/management/password_form/password_form';
import 'plugins/security/views/management/users_grid/users';
import 'plugins/security/views/management/roles_grid/roles';
+import 'plugins/security/views/management/api_keys_grid/api_keys';
import 'plugins/security/views/management/edit_user/edit_user';
import 'plugins/security/views/management/edit_role/index';
import routes from 'ui/routes';
import { xpackInfo } from 'plugins/xpack_main/services/xpack_info';
import '../../services/shield_user';
-import { ROLES_PATH, USERS_PATH } from './management_urls';
+import { ROLES_PATH, USERS_PATH, API_KEYS_PATH } from './management_urls';
import { management } from 'ui/management';
import { i18n } from '@kbn/i18n';
@@ -76,6 +77,18 @@ routes.defaults(/^\/management\/security(\/|$)/, {
url: `#${ROLES_PATH}`,
});
}
+
+ if (!security.hasItem('apiKeys')) {
+ security.register('apiKeys', {
+ name: 'securityApiKeysLink',
+ order: 30,
+ display: i18n.translate(
+ 'xpack.security.management.apiKeysTitle', {
+ defaultMessage: 'API Keys',
+ }),
+ url: `#${API_KEYS_PATH}`,
+ });
+ }
}
if (!showSecurityLinks) {
diff --git a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts b/x-pack/legacy/plugins/security/public/views/management/management_urls.ts
index 443b2a313aa5e..ea0cba9f7f3a7 100644
--- a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts
+++ b/x-pack/legacy/plugins/security/public/views/management/management_urls.ts
@@ -11,3 +11,4 @@ export const EDIT_ROLES_PATH = `${ROLES_PATH}/edit`;
export const CLONE_ROLES_PATH = `${ROLES_PATH}/clone`;
export const USERS_PATH = `${SECURITY_PATH}/users`;
export const EDIT_USERS_PATH = `${USERS_PATH}/edit`;
+export const API_KEYS_PATH = `${SECURITY_PATH}/api_keys`;
diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts
index 234667b1a4d3c..6df9d6801e2dc 100644
--- a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts
+++ b/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SpacesPlugin } from '../../../../spaces';
+import { LegacySpacesPlugin } from '../../../../spaces';
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically';
@@ -23,7 +23,7 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => {
getBasePath: jest.fn(),
getScopedSpacesClient: jest.fn(),
getActiveSpace: jest.fn(),
- } as OptionalPlugin;
+ } as OptionalPlugin;
const request = Symbol();
const privilegeOrPrivileges = ['foo', 'bar'];
const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory(
@@ -45,7 +45,7 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => {
const mockCheckPrivilegesWithRequest = jest.fn().mockReturnValue(mockCheckPrivileges);
const mockSpaces = {
isEnabled: false,
- } as OptionalPlugin;
+ } as OptionalPlugin;
const request = Symbol();
const privilegeOrPrivileges = ['foo', 'bar'];
const checkPrivilegesDynamically = checkPrivilegesDynamicallyWithRequestFactory(
diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.ts b/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.ts
index 537bc07f8e80c..243ad100c5715 100644
--- a/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.ts
+++ b/x-pack/legacy/plugins/security/server/lib/authorization/check_privileges_dynamically.ts
@@ -13,7 +13,7 @@ import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from '.
* you may not use this file except in compliance with the Elastic License.
*/
-import { SpacesPlugin } from '../../../../spaces';
+import { LegacySpacesPlugin } from '../../../../spaces';
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
export type CheckPrivilegesDynamically = (
@@ -26,7 +26,7 @@ export type CheckPrivilegesDynamicallyWithRequest = (
export function checkPrivilegesDynamicallyWithRequestFactory(
checkPrivilegesWithRequest: CheckPrivilegesWithRequest,
- spaces: OptionalPlugin
+ spaces: OptionalPlugin
): CheckPrivilegesDynamicallyWithRequest {
return function checkPrivilegesDynamicallyWithRequest(request: Legacy.Request) {
const checkPrivileges = checkPrivilegesWithRequest(request);
diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.test.ts b/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.test.ts
index 82bd784a917cd..7fa02330fac97 100644
--- a/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.test.ts
+++ b/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.test.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SpacesPlugin } from '../../../../spaces';
+import { LegacySpacesPlugin } from '../../../../spaces';
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges';
@@ -19,7 +19,7 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => {
const mockSpaces = ({
isEnabled: true,
namespaceToSpaceId: jest.fn().mockReturnValue(spaceId),
- } as unknown) as OptionalPlugin;
+ } as unknown) as OptionalPlugin;
const request = Symbol();
const privilegeOrPrivileges = ['foo', 'bar'];
@@ -50,7 +50,7 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => {
namespaceToSpaceId: jest.fn().mockImplementation(() => {
throw new Error('should not be called');
}),
- } as unknown) as OptionalPlugin;
+ } as unknown) as OptionalPlugin;
const request = Symbol();
diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.ts b/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.ts
index e20a843537541..fb1d258b5a05f 100644
--- a/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.ts
+++ b/x-pack/legacy/plugins/security/server/lib/authorization/check_saved_objects_privileges.ts
@@ -5,7 +5,7 @@
*/
import { Legacy } from 'kibana';
-import { SpacesPlugin } from '../../../../spaces';
+import { LegacySpacesPlugin } from '../../../../spaces';
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
import { CheckPrivilegesAtResourceResponse, CheckPrivilegesWithRequest } from './check_privileges';
@@ -19,7 +19,7 @@ export type CheckSavedObjectsPrivileges = (
export const checkSavedObjectsPrivilegesWithRequestFactory = (
checkPrivilegesWithRequest: CheckPrivilegesWithRequest,
- spaces: OptionalPlugin
+ spaces: OptionalPlugin
): CheckSavedObjectsPrivilegesWithRequest => {
return function checkSavedObjectsPrivilegesWithRequest(request: Legacy.Request) {
return async function checkSavedObjectsPrivileges(
diff --git a/x-pack/legacy/plugins/security/server/lib/authorization/service.ts b/x-pack/legacy/plugins/security/server/lib/authorization/service.ts
index e939deb64bc09..3d248adb9f8b8 100644
--- a/x-pack/legacy/plugins/security/server/lib/authorization/service.ts
+++ b/x-pack/legacy/plugins/security/server/lib/authorization/service.ts
@@ -7,7 +7,7 @@
import { Server } from 'hapi';
import { getClient } from '../../../../../server/lib/get_client_shield';
-import { SpacesPlugin } from '../../../../spaces';
+import { LegacySpacesPlugin } from '../../../../spaces';
import { XPackFeature, XPackMainPlugin } from '../../../../xpack_main/xpack_main';
import { APPLICATION_PREFIX } from '../../../common/constants';
import { OptionalPlugin } from '../../../../../server/lib/optional_plugin';
@@ -38,7 +38,7 @@ export function createAuthorizationService(
server: Server,
securityXPackFeature: XPackFeature,
xpackMainPlugin: XPackMainPlugin,
- spaces: OptionalPlugin
+ spaces: OptionalPlugin
): AuthorizationService {
const shieldClient = getClient(server);
const config = server.config();
diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js
new file mode 100644
index 0000000000000..a236badcd0d6b
--- /dev/null
+++ b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js
@@ -0,0 +1,45 @@
+/*
+ * 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 Joi from 'joi';
+import { wrapError } from '../../../../../../../../plugins/security/server';
+import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants';
+
+export function initGetApiKeysApi(server, callWithRequest, routePreCheckLicenseFn) {
+ server.route({
+ method: 'GET',
+ path: `${INTERNAL_API_BASE_PATH}/api_key`,
+ async handler(request) {
+ try {
+ const { isAdmin } = request.query;
+
+ const result = await callWithRequest(
+ request,
+ 'shield.getAPIKeys',
+ {
+ owner: !isAdmin
+ }
+ );
+
+ const validKeys = result.api_keys.filter(({ invalidated }) => !invalidated);
+
+ return {
+ apiKeys: validKeys,
+ };
+ } catch (error) {
+ return wrapError(error);
+ }
+ },
+ config: {
+ pre: [routePreCheckLicenseFn],
+ validate: {
+ query: Joi.object().keys({
+ isAdmin: Joi.bool().required(),
+ }).required(),
+ },
+ }
+ });
+}
diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js
new file mode 100644
index 0000000000000..ade1f0974096c
--- /dev/null
+++ b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js
@@ -0,0 +1,23 @@
+/*
+ * 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 { getClient } from '../../../../../../../server/lib/get_client_shield';
+import { routePreCheckLicense } from '../../../../lib/route_pre_check_license';
+import { initCheckPrivilegesApi } from './privileges';
+import { initGetApiKeysApi } from './get';
+import { initInvalidateApiKeysApi } from './invalidate';
+
+export function initApiKeysApi(server) {
+ const callWithRequest = getClient(server).callWithRequest;
+ const routePreCheckLicenseFn = routePreCheckLicense(server);
+
+ const { authorization } = server.plugins.security;
+ const { application } = authorization;
+
+ initCheckPrivilegesApi(server, callWithRequest, routePreCheckLicenseFn, application);
+ initGetApiKeysApi(server, callWithRequest, routePreCheckLicenseFn, application);
+ initInvalidateApiKeysApi(server, callWithRequest, routePreCheckLicenseFn, application);
+}
diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js
new file mode 100644
index 0000000000000..293142c60be67
--- /dev/null
+++ b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import Joi from 'joi';
+import { wrapError } from '../../../../../../../../plugins/security/server';
+import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants';
+
+export function initInvalidateApiKeysApi(server, callWithRequest, routePreCheckLicenseFn) {
+ server.route({
+ method: 'POST',
+ path: `${INTERNAL_API_BASE_PATH}/api_key/invalidate`,
+ async handler(request) {
+ try {
+ const { apiKeys, isAdmin } = request.payload;
+ const itemsInvalidated = [];
+ const errors = [];
+
+ // Send the request to invalidate the API key and return an error if it could not be deleted.
+ const sendRequestToInvalidateApiKey = async (id) => {
+ try {
+ const body = { id };
+
+ if (!isAdmin) {
+ body.owner = true;
+ }
+
+ await callWithRequest(request, 'shield.invalidateAPIKey', { body });
+ return null;
+ } catch (error) {
+ return wrapError(error);
+ }
+ };
+
+ const invalidateApiKey = async ({ id, name }) => {
+ const error = await sendRequestToInvalidateApiKey(id);
+ if (error) {
+ errors.push({ id, name, error });
+ } else {
+ itemsInvalidated.push({ id, name });
+ }
+ };
+
+ // Invalidate all API keys in parallel.
+ await Promise.all(apiKeys.map((key) => invalidateApiKey(key)));
+
+ return {
+ itemsInvalidated,
+ errors,
+ };
+ } catch (error) {
+ return wrapError(error);
+ }
+ },
+ config: {
+ pre: [routePreCheckLicenseFn],
+ validate: {
+ payload: Joi.object({
+ apiKeys: Joi.array().items(Joi.object({
+ id: Joi.string().required(),
+ name: Joi.string().required(),
+ })).required(),
+ isAdmin: Joi.bool().required(),
+ })
+ },
+ }
+ });
+}
diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js
new file mode 100644
index 0000000000000..3aa30c9a3b9bb
--- /dev/null
+++ b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js
@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { wrapError } from '../../../../../../../../plugins/security/server';
+import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants';
+
+export function initCheckPrivilegesApi(server, callWithRequest, routePreCheckLicenseFn) {
+ server.route({
+ method: 'GET',
+ path: `${INTERNAL_API_BASE_PATH}/api_key/privileges`,
+ async handler(request) {
+ try {
+ const result = await Promise.all([
+ callWithRequest(
+ request,
+ 'shield.hasPrivileges',
+ {
+ body: {
+ cluster: [
+ 'manage_security',
+ 'manage_api_key',
+ ],
+ },
+ }
+ ),
+ new Promise(async (resolve, reject) => {
+ try {
+ const result = await callWithRequest(
+ request,
+ 'shield.getAPIKeys',
+ {
+ owner: true
+ }
+ );
+ // If the API returns a truthy result that means it's enabled.
+ resolve({ areApiKeysEnabled: !!result });
+ } catch (e) {
+ // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759.
+ if (e.message.includes('api keys are not enabled')) {
+ return resolve({ areApiKeysEnabled: false });
+ }
+
+ // It's a real error, so rethrow it.
+ reject(e);
+ }
+ }),
+ ]);
+
+ const [{
+ cluster: {
+ manage_security: manageSecurity,
+ manage_api_key: manageApiKey,
+ }
+ }, {
+ areApiKeysEnabled,
+ }] = result;
+
+ const isAdmin = manageSecurity || manageApiKey;
+
+ return {
+ areApiKeysEnabled,
+ isAdmin,
+ };
+ } catch (error) {
+ return wrapError(error);
+ }
+ },
+ config: {
+ pre: [routePreCheckLicenseFn]
+ }
+ });
+}
diff --git a/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts b/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts
index dc94826c637cc..d043c1587d3c3 100644
--- a/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts
+++ b/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts
@@ -53,7 +53,7 @@ export const sharedSchema = gql`
source
}
- enum FlowTargetNew {
+ enum FlowTargetSourceDest {
destination
source
}
diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx
index 02bb853a6adf6..0a29845b4f64c 100644
--- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx
@@ -15,16 +15,16 @@ import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_a
import { EmbeddablePanel } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public';
import { Loader } from '../loader';
-import { useIndexPatterns } from '../ml_popover/hooks/use_index_patterns';
+import { useIndexPatterns } from '../../hooks/use_index_patterns';
import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
-import { getIndexPatternTitleIdMapping } from '../ml_popover/helpers';
import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt';
import { MapEmbeddable, SetQuery } from './types';
import * as i18n from './translations';
import { useStateToaster } from '../toasters';
import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers';
import { MapToolTip } from './map_tool_tip/map_tool_tip';
+import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers';
const EmbeddableWrapper = styled(EuiFlexGroup)`
position: relative;
diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts
index 2178bdb9684f3..6c3b6444578d6 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts
@@ -71,6 +71,8 @@ export const useAnomaliesTableData = ({
const [anomalyScore] = useKibanaUiSetting(DEFAULT_ANOMALY_SCORE);
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
+ const siemJobIds = siemJobs.filter(job => job.isInstalled).map(job => job.id);
+
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
@@ -82,11 +84,11 @@ export const useAnomaliesTableData = ({
earliestMs: number,
latestMs: number
) {
- if (userPermissions && !skip && siemJobs.length > 0) {
+ if (userPermissions && !skip && siemJobIds.length > 0) {
try {
const data = await anomaliesTableData(
{
- jobIds: siemJobs,
+ jobIds: siemJobIds,
criteriaFields: criteriaFieldsInput,
aggregationInterval: 'auto',
threshold: getThreshold(anomalyScore, threshold),
@@ -114,7 +116,7 @@ export const useAnomaliesTableData = ({
}
} else if (!userPermissions && isSubscribed) {
setLoading(false);
- } else if (siemJobs.length === 0 && isSubscribed) {
+ } else if (siemJobIds.length === 0 && isSubscribed) {
setLoading(false);
} else if (isSubscribed) {
setTableData(null);
@@ -134,7 +136,7 @@ export const useAnomaliesTableData = ({
endDate,
skip,
userPermissions,
- siemJobs.join(),
+ siemJobIds.sort().join(),
]);
return [loading, tableData];
diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts
index a3781f7b894e7..12c68467fe117 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts
@@ -9,7 +9,7 @@ import chrome from 'ui/chrome';
import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_KBN_VERSION } from '../../../../common/constants';
import { Anomalies, InfluencerInput, CriteriaFields } from '../types';
-import { throwIfNotOk } from './throw_if_not_ok';
+import { throwIfNotOk } from '../../../hooks/api/api';
export interface Body {
jobIds: string[];
criteriaFields: CriteriaFields[];
@@ -25,7 +25,7 @@ export interface Body {
export const anomaliesTableData = async (
body: Body,
- headers: Record,
+ headers: Record,
signal: AbortSignal
): Promise => {
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts
index 9f1af8db5eddc..deec12aacfc42 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts
@@ -9,7 +9,7 @@ import chrome from 'ui/chrome';
import { DEFAULT_KBN_VERSION } from '../../../../common/constants';
import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
import { InfluencerInput, MlCapabilities } from '../types';
-import { throwIfNotOk } from './throw_if_not_ok';
+import { throwIfNotOk } from '../../../hooks/api/api';
export interface Body {
jobIds: string[];
@@ -25,7 +25,7 @@ export interface Body {
}
export const getMlCapabilities = async (
- headers: Record,
+ headers: Record,
signal: AbortSignal
): Promise => {
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts
index 9c6493c9c9b5a..374fbd830f920 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts
@@ -6,14 +6,13 @@
import fetchMock from 'fetch-mock';
import {
- throwIfNotOk,
- parseJsonFromBody,
+ isMlStartJobError,
MessageBody,
- tryParseResponse,
+ parseJsonFromBody,
throwIfErrorAttached,
- isMlStartJobError,
- ToasterErrors,
throwIfErrorAttachedToSetup,
+ ToasterErrors,
+ tryParseResponse,
} from './throw_if_not_ok';
import { SetupMlResponse } from '../../ml_popover/types';
@@ -22,32 +21,6 @@ describe('throw_if_not_ok', () => {
fetchMock.reset();
});
- describe('#throwIfNotOk', () => {
- test('does a throw if it is given response that is not ok and the body is not parsable', async () => {
- fetchMock.mock('http://example.com', 500);
- const response = await fetch('http://example.com');
- await expect(throwIfNotOk(response)).rejects.toThrow('Network Error: Internal Server Error');
- });
-
- test('does a throw and returns a body if it is parsable', async () => {
- fetchMock.mock('http://example.com', {
- status: 500,
- body: {
- statusCode: 500,
- message: 'I am a custom message',
- },
- });
- const response = await fetch('http://example.com');
- await expect(throwIfNotOk(response)).rejects.toThrow('I am a custom message');
- });
-
- test('does NOT do a throw if it is given response is not ok', async () => {
- fetchMock.mock('http://example.com', 200);
- const response = await fetch('http://example.com');
- await expect(throwIfNotOk(response)).resolves.toEqual(undefined);
- });
- });
-
describe('#parseJsonFromBody', () => {
test('parses a json from the body correctly', async () => {
fetchMock.mock('http://example.com', {
diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts
index b26ee42cf470a..5d28f4b40ab60 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts
@@ -35,21 +35,6 @@ export class ToasterErrors extends Error implements ToasterErrorsType {
}
}
-export const throwIfNotOk = async (response: Response): Promise => {
- if (!response.ok) {
- const body = await parseJsonFromBody(response);
- if (body != null && body.message) {
- if (body.statusCode != null) {
- throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.statusCode}`]);
- } else {
- throw new ToasterErrors([body.message]);
- }
- } else {
- throw new ToasterErrors([`${i18n.NETWORK_ERROR} ${response.statusText}`]);
- }
- }
-};
-
export const parseJsonFromBody = async (response: Response): Promise => {
try {
const text = await response.text();
@@ -67,10 +52,13 @@ export const tryParseResponse = (response: string): string => {
}
};
-export const throwIfErrorAttachedToSetup = (setupResponse: SetupMlResponse): void => {
+export const throwIfErrorAttachedToSetup = (
+ setupResponse: SetupMlResponse,
+ jobIdErrorFilter: string[] = []
+): void => {
const jobErrors = setupResponse.jobs.reduce(
(accum, job) =>
- job.error != null
+ job.error != null && jobIdErrorFilter.includes(job.id)
? [
...accum,
job.error.msg,
@@ -83,7 +71,7 @@ export const throwIfErrorAttachedToSetup = (setupResponse: SetupMlResponse): voi
const dataFeedErrors = setupResponse.datafeeds.reduce(
(accum, dataFeed) =>
- dataFeed.error != null
+ dataFeed.error != null && jobIdErrorFilter.includes(dataFeed.id.substr('datafeed-'.length))
? [
...accum,
dataFeed.error.msg,
diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/translations.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/translations.ts
index 2bf5a1a54626f..87edc531758fb 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml/api/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml/api/translations.ts
@@ -12,10 +12,3 @@ export const STATUS_CODE = i18n.translate(
defaultMessage: 'Status Code:',
}
);
-
-export const NETWORK_ERROR = i18n.translate(
- 'xpack.siem.components.ml.api.errors.networkErrorFailureTitle',
- {
- defaultMessage: 'Network Error:',
- }
-);
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__mocks__/api.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/__mocks__/api.tsx
index 09e8502f33069..76c276cf69b63 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/__mocks__/api.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/__mocks__/api.tsx
@@ -4,7 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Group, IndexPatternSavedObject, Job } from '../types';
+import {
+ Group,
+ JobSummary,
+ Module,
+ RecognizerModule,
+ SetupMlResponse,
+ SiemJob,
+ StartDatafeedResponse,
+ StopDatafeedResponse,
+} from '../types';
export const mockGroupsResponse: Group[] = [
{
@@ -22,7 +31,7 @@ export const mockGroupsResponse: Group[] = [
{ id: 'suricata', jobIds: ['suricata_alert_rate'], calendarIds: [] },
];
-export const mockOpenedJob: Job = {
+export const mockOpenedJob: JobSummary = {
datafeedId: 'datafeed-siem-api-rare_process_linux_ecs',
datafeedIndices: ['auditbeat-*'],
datafeedState: 'started',
@@ -39,7 +48,7 @@ export const mockOpenedJob: Job = {
processed_record_count: 3425264,
};
-export const mockJobsSummaryResponse: Job[] = [
+export const mockJobsSummaryResponse: JobSummary[] = [
{
id: 'rc-rare-process-windows-5',
description:
@@ -99,44 +108,446 @@ export const mockJobsSummaryResponse: Job[] = [
},
];
-export const mockConfigTemplates = [
+export const mockGetModuleResponse: Module[] = [
{
- name: 'siem_auditbeat_ecs',
+ id: 'siem_auditbeat',
+ title: 'SIEM Auditbeat',
+ description:
+ 'Detect suspicious network activity and unusual processes in Auditbeat data (beta)',
+ type: 'Auditbeat data',
+ logoFile: 'logo.json',
defaultIndexPattern: 'auditbeat-*',
- jobs: ['siem-api-rare_process_linux_ecs', 'siem-api-suspicious_login_activity_ecs'],
+ query: { bool: { filter: [{ term: { 'agent.type': 'auditbeat' } }] } },
+ jobs: [
+ {
+ id: 'rare_process_by_host_linux_ecs',
+ config: {
+ job_type: 'anomaly_detector',
+ description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)',
+ groups: ['siem', 'auditbeat', 'process'],
+ analysis_config: {
+ bucket_span: '15m',
+ detectors: [
+ {
+ detector_description: 'rare process executions on Linux',
+ function: 'rare',
+ by_field_name: 'process.name',
+ partition_field_name: 'host.name',
+ },
+ ],
+ influencers: ['host.name', 'process.name', 'user.name'],
+ },
+ analysis_limits: { model_memory_limit: '256mb' },
+ data_description: { time_field: '@timestamp' },
+ custom_settings: {
+ created_by: 'ml-module-siem-auditbeat',
+ custom_urls: [
+ {
+ url_name: 'Host Details by process name',
+ url_value:
+ "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Host Details by user name',
+ url_value:
+ "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Hosts Overview by process name',
+ url_value:
+ "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Hosts Overview by user name',
+ url_value:
+ "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ ],
+ },
+ },
+ },
+ ],
+ datafeeds: [
+ {
+ id: 'datafeed-rare_process_by_host_linux_ecs',
+ config: {
+ job_id: 'rare_process_by_host_linux_ecs',
+ indexes: ['INDEX_PATTERN_NAME'],
+ query: {
+ bool: {
+ filter: [
+ { terms: { 'event.action': ['process_started', 'executed'] } },
+ { term: { 'agent.type': 'auditbeat' } },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ kibana: {},
},
{
- name: 'siem_winlogbeat_ecs',
+ id: 'siem_winlogbeat',
+ title: 'SIEM Winlogbeat',
+ description: 'Detect unusual processes and network activity in Winlogbeat data (beta)',
+ type: 'Winlogbeat data',
+ logoFile: 'logo.json',
defaultIndexPattern: 'winlogbeat-*',
- jobs: ['siem-api-rare_process_windows_ecs'],
+ query: { bool: { filter: [{ term: { 'agent.type': 'winlogbeat' } }] } },
+ jobs: [
+ {
+ id: 'windows_anomalous_network_activity_ecs',
+ config: {
+ job_type: 'anomaly_detector',
+ description:
+ 'SIEM Winlogbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)',
+ groups: ['siem', 'winlogbeat', 'network'],
+ analysis_config: {
+ bucket_span: '15m',
+ detectors: [
+ {
+ detector_description: 'rare by "process.name"',
+ function: 'rare',
+ by_field_name: 'process.name',
+ },
+ ],
+ influencers: ['host.name', 'process.name', 'user.name', 'destination.ip'],
+ },
+ analysis_limits: { model_memory_limit: '64mb' },
+ data_description: { time_field: '@timestamp' },
+ custom_settings: {
+ created_by: 'ml-module-siem-winlogbeat',
+ custom_urls: [
+ {
+ url_name: 'Host Details by process name',
+ url_value:
+ "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Host Details by user name',
+ url_value:
+ "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Hosts Overview by process name',
+ url_value:
+ "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Hosts Overview by user name',
+ url_value:
+ "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'windows_anomalous_path_activity_ecs',
+ config: {
+ job_type: 'anomaly_detector',
+ groups: ['siem', 'winlogbeat', 'process'],
+ description:
+ 'SIEM Winlogbeat: Looks for activity in unusual paths that may indicate execution of malware or persistence mechanisms. Windows payloads often execute from user profile paths (beta)',
+ analysis_config: {
+ bucket_span: '15m',
+ detectors: [
+ {
+ detector_description: 'rare by "process.working_directory"',
+ function: 'rare',
+ by_field_name: 'process.working_directory',
+ },
+ ],
+ influencers: ['host.name', 'process.name', 'user.name'],
+ },
+ analysis_limits: { model_memory_limit: '256mb' },
+ data_description: { time_field: '@timestamp' },
+ custom_settings: {
+ created_by: 'ml-module-siem-winlogbeat',
+ custom_urls: [
+ {
+ url_name: 'Host Details by process name',
+ url_value:
+ "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Host Details by user name',
+ url_value:
+ "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Hosts Overview by process name',
+ url_value:
+ "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ {
+ url_name: 'Hosts Overview by user name',
+ url_value:
+ "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
+ },
+ ],
+ },
+ },
+ },
+ ],
+ datafeeds: [
+ {
+ id: 'datafeed-windows_anomalous_path_activity_ecs',
+ config: {
+ job_id: 'windows_anomalous_path_activity_ecs',
+ indices: ['INDEX_PATTERN_NAME'],
+ query: {
+ bool: {
+ filter: [
+ { term: { 'event.action': 'Process Create (rule: ProcessCreate)' } },
+ { term: { 'agent.type': 'winlogbeat' } },
+ ],
+ },
+ },
+ },
+ },
+ {
+ id: 'datafeed-windows_anomalous_network_activity_ecs',
+ config: {
+ job_id: 'windows_anomalous_network_activity_ecs',
+ indices: ['INDEX_PATTERN_NAME'],
+ query: {
+ bool: {
+ filter: [
+ { term: { 'event.action': 'Network connection detected (rule: NetworkConnect)' } },
+ { term: { 'agent.type': 'winlogbeat' } },
+ ],
+ must_not: [
+ {
+ bool: {
+ should: [
+ { term: { 'destination.ip': '127.0.0.1' } },
+ { term: { 'destination.ip': '127.0.0.53' } },
+ { term: { 'destination.ip': '::1' } },
+ ],
+ minimum_should_match: 1,
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ kibana: {},
},
];
-export const mockInstalledJobIds = ['siem-api-rare_process_linux_ecs'];
-
-export const mockEmbeddedJobIds = [
- 'siem-api-rare_process_linux_ecs',
- 'siem-api-suspicious_login_activity_ecs',
- 'siem-api-rare_process_windows_ecs',
+export const checkRecognizerSuccess: RecognizerModule[] = [
+ {
+ id: 'siem_auditbeat',
+ title: 'SIEM Auditbeat',
+ query: { bool: { filter: [{ term: { 'agent.type': 'auditbeat' } }] } },
+ description:
+ 'Detect suspicious network activity and unusual processes in Auditbeat data (beta)',
+ logo: { icon: 'securityAnalyticsApp' },
+ },
];
-export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [
- {
- type: 'index-pattern',
- id: '2d1fe420-eeee-11e9-ad95-4b5e687c2aee',
- attributes: {
- title: 'filebeat-*',
+export const mockSetupMlJobAllError: SetupMlResponse = {
+ jobs: [
+ {
+ id: 'linux_anomalous_network_url_activity_ecs',
+ success: false,
+ error: {
+ msg:
+ "[resource_already_exists_exception] The job cannot be created with the Id 'linux_anomalous_network_url_activity_ecs'. The Id is already used.",
+ path: '/_ml/anomaly_detectors/linux_anomalous_network_url_activity_ecs',
+ query: {},
+ body:
+ '{"job_type":"anomaly_detector","groups":["siem","auditbeat","process"],"description":"SIEM Auditbeat: Looks for an unusual web URL request from a Linux instance. Curl and wget web request activity is very common but unusual web requests from a Linux server can sometimes be malware delivery or execution (beta)","analysis_config":{"bucket_span":"15m","detectors":[{"detector_description":"rare by \\"process.title\\"","function":"rare","by_field_name":"process.title"}],"influencers":["host.name","destination.ip","destination.port"]},"analysis_limits":{"model_memory_limit":"32mb"},"data_description":{"time_field":"@timestamp"},"custom_settings":{"created_by":"ml-module-siem-auditbeat","custom_urls":[{"url_name":"Host Details","url_value":"siem#/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"}]},"results_index_name":"linux_anomalous_network_url_activity_ecs"}',
+ statusCode: 400,
+ response:
+ '{"error":{"root_cause":[{"type":"resource_already_exists_exception","reason":"The job cannot be created with the Id \'linux_anomalous_network_url_activity_ecs\'. The Id is already used."}],"type":"resource_already_exists_exception","reason":"The job cannot be created with the Id \'linux_anomalous_network_url_activity_ecs\'. The Id is already used."},"status":400}',
+ },
+ },
+ {
+ id: 'linux_anomalous_network_port_activity_ecs',
+ success: false,
+ error: {
+ msg:
+ "[resource_already_exists_exception] The job cannot be created with the Id 'linux_anomalous_network_port_activity_ecs'. The Id is already used.",
+ path: '/_ml/anomaly_detectors/linux_anomalous_network_port_activity_ecs',
+ query: {},
+ body:
+ '{"job_type":"anomaly_detector","description":"SIEM Auditbeat: Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity (beta)","groups":["siem","auditbeat","process"],"analysis_config":{"bucket_span":"15m","detectors":[{"detector_description":"rare by \\"destination.port\\"","function":"rare","by_field_name":"destination.port"}],"influencers":["host.name","process.name","user.name","destination.ip"]},"analysis_limits":{"model_memory_limit":"32mb"},"data_description":{"time_field":"@timestamp"},"custom_settings":{"created_by":"ml-module-siem-auditbeat","custom_urls":[{"url_name":"Host Details by process name","url_value":"siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:\'process.name%20:%20%22$process.name$%22\',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"},{"url_name":"Host Details by user name","url_value":"siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:\'user.name%20:%20%22$user.name$%22\',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"},{"url_name":"Hosts Overview by process name","url_value":"siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:\'process.name%20:%20%22$process.name$%22\',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"},{"url_name":"Hosts Overview by user name","url_value":"siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:\'user.name%20:%20%22$user.name$%22\',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"}]},"results_index_name":"linux_anomalous_network_port_activity_ecs"}',
+ statusCode: 400,
+ response:
+ '{"error":{"root_cause":[{"type":"resource_already_exists_exception","reason":"The job cannot be created with the Id \'linux_anomalous_network_port_activity_ecs\'. The Id is already used."}],"type":"resource_already_exists_exception","reason":"The job cannot be created with the Id \'linux_anomalous_network_port_activity_ecs\'. The Id is already used."},"status":400}',
+ },
+ },
+ ],
+ datafeeds: [
+ {
+ id: 'datafeed-linux_anomalous_network_activity_ecs',
+ success: false,
+ started: false,
+ error: {
+ msg:
+ '[status_exception] A datafeed [datafeed-linux_anomalous_network_activity_ecs] already exists for job [linux_anomalous_network_activity_ecs]',
+ path: '/_ml/datafeeds/datafeed-linux_anomalous_network_activity_ecs',
+ query: {},
+ body:
+ '{"job_id":"linux_anomalous_network_activity_ecs","indices":["auditbeat-*"],"query":{"bool":{"filter":[{"term":{"event.action":"connected-to"}},{"term":{"agent.type":"auditbeat"}}],"must_not":[{"bool":{"should":[{"term":{"destination.ip":"127.0.0.1"}},{"term":{"destination.ip":"127.0.0.53"}},{"term":{"destination.ip":"::1"}}],"minimum_should_match":1}}]}}}',
+ statusCode: 409,
+ response:
+ '{"error":{"root_cause":[{"type":"status_exception","reason":"A datafeed [datafeed-linux_anomalous_network_activity_ecs] already exists for job [linux_anomalous_network_activity_ecs]"}],"type":"status_exception","reason":"A datafeed [datafeed-linux_anomalous_network_activity_ecs] already exists for job [linux_anomalous_network_activity_ecs]"},"status":409}',
+ },
+ },
+ {
+ id: 'datafeed-linux_anomalous_network_port_activity_ecs',
+ success: false,
+ started: false,
+ error: {
+ msg:
+ '[status_exception] A datafeed [datafeed-linux_anomalous_network_port_activity_ecs] already exists for job [linux_anomalous_network_port_activity_ecs]',
+ path: '/_ml/datafeeds/datafeed-linux_anomalous_network_port_activity_ecs',
+ query: {},
+ body:
+ '{"job_id":"linux_anomalous_network_port_activity_ecs","indices":["auditbeat-*"],"query":{"bool":{"filter":[{"term":{"event.action":"connected-to"}},{"term":{"agent.type":"auditbeat"}}],"must_not":[{"bool":{"should":[{"term":{"destination.ip":"::1"}},{"term":{"destination.ip":"127.0.0.1"}},{"term":{"destination.ip":"::"}},{"term":{"user.name_map.uid":"jenkins"}}],"minimum_should_match":1}}]}}}',
+ statusCode: 409,
+ response:
+ '{"error":{"root_cause":[{"type":"status_exception","reason":"A datafeed [datafeed-linux_anomalous_network_port_activity_ecs] already exists for job [linux_anomalous_network_port_activity_ecs]"}],"type":"status_exception","reason":"A datafeed [datafeed-linux_anomalous_network_port_activity_ecs] already exists for job [linux_anomalous_network_port_activity_ecs]"},"status":409}',
+ },
+ },
+ ],
+ kibana: {},
+};
+
+export const mockSetupMlJobSingleErrorSingleSuccess: SetupMlResponse = {
+ jobs: [
+ {
+ id: 'linux_anomalous_network_activity_ecs',
+ success: false,
+ error: {
+ msg:
+ "[resource_already_exists_exception] The job cannot be created with the Id 'linux_anomalous_network_activity_ecs'. The Id is already used.",
+ path: '/_ml/anomaly_detectors/linux_anomalous_network_activity_ecs',
+ query: {},
+ body:
+ '{"job_type":"anomaly_detector","description":"SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)","groups":["siem","auditbeat","network"],"analysis_config":{"bucket_span":"15m","detectors":[{"detector_description":"rare by \\"process.name\\"","function":"rare","by_field_name":"process.name"}],"influencers":["host.name","process.name","user.name","destination.ip"]},"analysis_limits":{"model_memory_limit":"64mb"},"data_description":{"time_field":"@timestamp"},"custom_settings":{"created_by":"ml-module-siem-auditbeat","custom_urls":[{"url_name":"Host Details by process name","url_value":"siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:\'process.name%20:%20%22$process.name$%22\',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"},{"url_name":"Host Details by user name","url_value":"siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:\'user.name%20:%20%22$user.name$%22\',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"},{"url_name":"Hosts Overview by process name","url_value":"siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:\'process.name%20:%20%22$process.name$%22\',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"},{"url_name":"Hosts Overview by user name","url_value":"siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:\'user.name%20:%20%22$user.name$%22\',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')),timeline:(linkTo:!(global),timerange:(from:\'$earliest$\',kind:absolute,to:\'$latest$\')))"}]},"results_index_name":"linux_anomalous_network_activity_ecs"}',
+ statusCode: 400,
+ response:
+ '{"error":{"root_cause":[{"type":"resource_already_exists_exception","reason":"The job cannot be created with the Id \'linux_anomalous_network_activity_ecs\'. The Id is already used."}],"type":"resource_already_exists_exception","reason":"The job cannot be created with the Id \'linux_anomalous_network_activity_ecs\'. The Id is already used."},"status":400}',
+ },
+ },
+ { id: 'linux_anomalous_network_port_activity_ecs', success: true },
+ ],
+ datafeeds: [
+ {
+ id: 'datafeed-linux_anomalous_network_activity_ecs',
+ success: false,
+ started: false,
+ error: {
+ msg:
+ '[status_exception] A datafeed [datafeed-linux_anomalous_network_activity_ecs] already exists for job [linux_anomalous_network_activity_ecs]',
+ path: '/_ml/datafeeds/datafeed-linux_anomalous_network_activity_ecs',
+ query: {},
+ body:
+ '{"job_id":"linux_anomalous_network_activity_ecs","indices":["auditbeat-*"],"query":{"bool":{"filter":[{"term":{"event.action":"connected-to"}},{"term":{"agent.type":"auditbeat"}}],"must_not":[{"bool":{"should":[{"term":{"destination.ip":"127.0.0.1"}},{"term":{"destination.ip":"127.0.0.53"}},{"term":{"destination.ip":"::1"}}],"minimum_should_match":1}}]}}}',
+ statusCode: 409,
+ response:
+ '{"error":{"root_cause":[{"type":"status_exception","reason":"A datafeed [datafeed-linux_anomalous_network_activity_ecs] already exists for job [linux_anomalous_network_activity_ecs]"}],"type":"status_exception","reason":"A datafeed [datafeed-linux_anomalous_network_activity_ecs] already exists for job [linux_anomalous_network_activity_ecs]"},"status":409}',
+ },
+ },
+
+ { id: 'datafeed-linux_anomalous_network_port_activity_ecs', success: true, started: false },
+ ],
+ kibana: {},
+};
+
+export const mockSetupMlJobAllSuccess: SetupMlResponse = {
+ jobs: [
+ {
+ id: 'linux_anomalous_network_activity_ecs',
+ success: true,
},
- updated_at: '2019-08-26T04:30:09.111Z',
- version: 'WzE4LLwxXQ==',
+ { id: 'linux_anomalous_network_port_activity_ecs', success: true },
+ ],
+ datafeeds: [
+ { id: 'datafeed-linux_anomalous_network_activity_ecs', success: true, started: false },
+
+ { id: 'datafeed-linux_anomalous_network_port_activity_ecs', success: true, started: false },
+ ],
+ kibana: {},
+};
+
+export const mockStartDatafeedsError: StartDatafeedResponse = {
+ 'datafeed-linux_anomalous_network_service': { started: false, error: 'Job has no datafeed' },
+};
+
+export const mockStartDatafeedsSuccess: StartDatafeedResponse = {
+ 'datafeed-linux_anomalous_network_service': { started: true },
+};
+
+export const mockStopDatafeedsErrorDoesNotExist: StopDatafeedResponse = {};
+
+export const mockStopDatafeedsSuccess: StopDatafeedResponse = {
+ 'datafeed-linux_anomalous_network_service': { stopped: true },
+};
+
+export const mockSiemJobs: SiemJob[] = [
+ {
+ id: 'linux_anomalous_network_activity_ecs',
+ description:
+ 'SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)',
+ groups: ['auditbeat', 'process', 'siem'],
+ processed_record_count: 32010,
+ memory_status: 'ok',
+ jobState: 'closed',
+ hasDatafeed: true,
+ datafeedId: 'datafeed-linux_anomalous_network_activity_ecs',
+ datafeedIndices: ['auditbeat-*'],
+ datafeedState: 'stopped',
+ latestTimestampMs: 1571022859393,
+ earliestTimestampMs: 1569812391387,
+ latestResultsTimestampMs: 1571022900000,
+ isSingleMetricViewerJob: true,
+ moduleId: 'siem_auditbeat',
+ defaultIndexPattern: 'auditbeat-*',
+ isCompatible: true,
+ isInstalled: true,
+ isElasticJob: true,
},
{
- type: 'index-pattern',
- id: '5463ec70-c7ba-ffff-ad95-4b5e687c2aee',
- attributes: {
- title: 'auditbeat-*',
- },
- updated_at: '2019-08-26T04:31:12.934Z',
- version: 'WzELLywxXQ==',
+ id: 'rare_process_by_host_linux_ecs',
+ description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)',
+ groups: ['auditbeat', 'process', 'siem'],
+ processed_record_count: 0,
+ memory_status: 'ok',
+ jobState: 'closed',
+ hasDatafeed: true,
+ datafeedId: 'datafeed-rare_process_by_host_linux_ecs',
+ datafeedIndices: ['auditbeat-*'],
+ datafeedState: 'stopped',
+ isSingleMetricViewerJob: true,
+ moduleId: 'siem_auditbeat',
+ defaultIndexPattern: 'auditbeat-*',
+ isCompatible: true,
+ isInstalled: true,
+ isElasticJob: true,
+ },
+ {
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ hasDatafeed: false,
+ isSingleMetricViewerJob: false,
+ jobState: '',
+ memory_status: '',
+ processed_record_count: 0,
+ id: 'rare_process_by_host_windows_ecs',
+ description: 'SIEM Winlogbeat: Detect unusually rare processes on Windows (beta)',
+ groups: ['process', 'siem', 'winlogbeat'],
+ defaultIndexPattern: 'winlogbeat-*',
+ moduleId: 'siem_winlogbeat',
+ isCompatible: false,
+ isInstalled: false,
+ isElasticJob: true,
},
];
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap
index 83dc5446de37e..5eeaee17ee72a 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`FilterGroup renders correctly against snapshot 1`] = `
+exports[`JobsTableFilters renders correctly against snapshot 1`] = `
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap
index fbad5012f1e5e..cf924f3a06edc 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`FilterGroup renders correctly against snapshot 1`] = `
+exports[`JobsTableFilters renders correctly against snapshot 1`] = `
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx
index ee8298c77f94f..6f74df4aed384 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx
@@ -6,42 +6,75 @@
import chrome from 'ui/chrome';
import {
+ CheckRecognizerProps,
CloseJobsResponse,
- Group,
- IndexPatternResponse,
- IndexPatternSavedObject,
- Job,
+ ErrorResponse,
+ GetModulesProps,
+ JobSummary,
MlSetupArgs,
+ Module,
+ RecognizerModule,
SetupMlResponse,
StartDatafeedResponse,
StopDatafeedResponse,
} from './types';
-import {
- throwIfNotOk,
- throwIfErrorAttached,
- throwIfErrorAttachedToSetup,
-} from '../ml/api/throw_if_not_ok';
+import { throwIfErrorAttached, throwIfErrorAttachedToSetup } from '../ml/api/throw_if_not_ok';
import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
import { DEFAULT_KBN_VERSION } from '../../../common/constants';
+import { throwIfNotOk } from '../../hooks/api/api';
-const emptyIndexPattern: IndexPatternSavedObject[] = [];
+/**
+ * Checks the ML Recognizer API to see if a given indexPattern has any compatible modules
+ *
+ * @param indexPatternName ES index pattern to check for compatible modules
+ * @param headers optional headers to add
+ * @param signal to cancel request
+ */
+export const checkRecognizer = async ({
+ indexPatternName,
+ headers = {},
+ signal,
+}: CheckRecognizerProps): Promise => {
+ const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
+ const response = await fetch(
+ `${chrome.getBasePath()}/api/ml/modules/recognize/${indexPatternName}`,
+ {
+ method: 'GET',
+ credentials: 'same-origin',
+ headers: {
+ 'kbn-system-api': 'true',
+ 'content-type': 'application/json',
+ 'kbn-version': kbnVersion,
+ 'kbn-xsrf': kbnVersion,
+ ...headers,
+ },
+ signal,
+ }
+ );
+ await throwIfNotOk(response);
+ return response.json();
+};
/**
- * Fetches ML Groups Data
+ * Returns ML Module for given moduleId. Returns all modules if no moduleId specified
*
- * @param headers
+ * @param moduleId id of the module to retrieve
+ * @param headers optional headers to add optional headers to add
+ * @param signal to cancel request
*/
-export const groupsData = async (
- headers: Record,
- signal: AbortSignal
-): Promise => {
+export const getModules = async ({
+ moduleId = '',
+ headers = {},
+ signal,
+}: GetModulesProps): Promise => {
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
- const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/groups`, {
+ const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/get_module/${moduleId}`, {
method: 'GET',
credentials: 'same-origin',
headers: {
- 'content-type': 'application/json',
'kbn-system-api': 'true',
+ 'content-type': 'application/json',
+ 'kbn-version': kbnVersion,
'kbn-xsrf': kbnVersion,
...headers,
},
@@ -56,13 +89,15 @@ export const groupsData = async (
*
* @param configTemplate - name of configTemplate to setup
* @param indexPatternName - default index pattern configTemplate should be installed with
+ * @param jobIdErrorFilter - if provided, filters all errors except for given jobIds
* @param groups - list of groups to add to jobs being installed
* @param prefix - prefix to be added to job name
- * @param headers
+ * @param headers optional headers to add
*/
export const setupMlJob = async ({
configTemplate,
indexPatternName = 'auditbeat-*',
+ jobIdErrorFilter = [],
groups = ['siem'],
prefix = '',
headers = {},
@@ -81,13 +116,14 @@ export const setupMlJob = async ({
headers: {
'kbn-system-api': 'true',
'content-type': 'application/json',
+ 'kbn-version': kbnVersion,
'kbn-xsrf': kbnVersion,
...headers,
},
});
await throwIfNotOk(response);
const json = await response.json();
- throwIfErrorAttachedToSetup(json);
+ throwIfErrorAttachedToSetup(json, jobIdErrorFilter);
return json;
};
@@ -96,13 +132,17 @@ export const setupMlJob = async ({
*
* @param datafeedIds
* @param start
- * @param headers
+ * @param headers optional headers to add
*/
-export const startDatafeeds = async (
- datafeedIds: string[],
- headers: Record,
- start = 0
-): Promise => {
+export const startDatafeeds = async ({
+ datafeedIds,
+ headers,
+ start = 0,
+}: {
+ datafeedIds: string[];
+ start: number;
+ headers?: Record;
+}): Promise => {
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/force_start_datafeeds`, {
method: 'POST',
@@ -114,6 +154,7 @@ export const startDatafeeds = async (
headers: {
'kbn-system-api': 'true',
'content-type': 'application/json',
+ 'kbn-version': kbnVersion,
'kbn-xsrf': kbnVersion,
...headers,
},
@@ -128,12 +169,15 @@ export const startDatafeeds = async (
* Stops the given dataFeedIds and sets the corresponding Job's jobState to closed
*
* @param datafeedIds
- * @param headers
+ * @param headers optional headers to add
*/
-export const stopDatafeeds = async (
- datafeedIds: string[],
- headers: Record
-): Promise<[StopDatafeedResponse, CloseJobsResponse]> => {
+export const stopDatafeeds = async ({
+ datafeedIds,
+ headers,
+}: {
+ datafeedIds: string[];
+ headers?: Record;
+}): Promise<[StopDatafeedResponse | ErrorResponse, CloseJobsResponse]> => {
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
const stopDatafeedsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/stop_datafeeds`, {
method: 'POST',
@@ -176,63 +220,27 @@ export const stopDatafeeds = async (
};
/**
- * Fetches Job Details for given jobIds
+ * Fetches a summary of all ML jobs currently installed
*
- * @param jobIds
- * @param headers
+ * NOTE: If not sending jobIds in the body, you must at least send an empty body or the server will
+ * return a 500
+ *
+ * @param signal to cancel request
*/
-export const jobsSummary = async (
- jobIds: string[],
- headers: Record,
- signal: AbortSignal
-): Promise => {
+export const getJobsSummary = async (signal: AbortSignal): Promise => {
const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/jobs_summary`, {
method: 'POST',
credentials: 'same-origin',
- body: JSON.stringify({ jobIds }),
+ body: JSON.stringify({}),
headers: {
'content-type': 'application/json',
- 'kbn-xsrf': kbnVersion,
'kbn-system-api': 'true',
- ...headers,
+ 'kbn-version': kbnVersion,
+ 'kbn-xsrf': kbnVersion,
},
signal,
});
await throwIfNotOk(response);
return response.json();
};
-
-/**
- * Fetches Configured Index Patterns from the Kibana saved objects API (as ML does during create job flow)
- * TODO: Used by more than just ML now -- refactor to shared component https://github.com/elastic/siem-team/issues/448
- * @param headers
- */
-export const getIndexPatterns = async (
- headers: Record,
- signal: AbortSignal
-): Promise => {
- const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
- const response = await fetch(
- `${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`,
- {
- method: 'GET',
- credentials: 'same-origin',
- headers: {
- 'content-type': 'application/json',
- 'kbn-xsrf': kbnVersion,
- 'kbn-system-api': 'true',
- ...headers,
- },
- signal,
- }
- );
- await throwIfNotOk(response);
- const results: IndexPatternResponse = await response.json();
-
- if (results.saved_objects && Array.isArray(results.saved_objects)) {
- return results.saved_objects;
- } else {
- return emptyIndexPattern;
- }
-};
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/config_templates.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/config_templates.tsx
deleted file mode 100644
index b6391668762d1..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/config_templates.tsx
+++ /dev/null
@@ -1,42 +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 { ConfigTemplate } from './types';
-
-/**
- * Config Templates w/ corresponding defaultIndexPattern and jobId's of the SIEM Jobs embedded
- * in ML. Added as part of: https://github.com/elastic/kibana/pull/39678/files
- */
-export const configTemplates: ConfigTemplate[] = [
- {
- name: 'siem_auditbeat_ecs',
- defaultIndexPattern: 'auditbeat-*',
- jobs: [
- 'rare_process_by_host_linux_ecs',
- 'suspicious_login_activity_ecs',
- 'linux_anomalous_network_activity_ecs',
- 'linux_anomalous_network_port_activity_ecs',
- 'linux_anomalous_network_service',
- 'linux_anomalous_network_url_activity_ecs',
- 'linux_anomalous_process_all_hosts_ecs',
- 'linux_anomalous_user_name_ecs',
- ],
- },
- {
- name: 'siem_winlogbeat_ecs',
- defaultIndexPattern: 'winlogbeat-*',
- jobs: [
- 'rare_process_by_host_windows_ecs',
- 'windows_anomalous_network_activity_ecs',
- 'windows_anomalous_path_activity_ecs',
- 'windows_anomalous_process_all_hosts_ecs',
- 'windows_anomalous_process_creation',
- 'windows_anomalous_script',
- 'windows_anomalous_service',
- 'windows_anomalous_user_name_ecs',
- ],
- },
-];
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx
index a7025c68c429e..26ebfeb91629b 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx
@@ -4,153 +4,41 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- mockConfigTemplates,
- mockEmbeddedJobIds,
- mockIndexPatternSavedObjects,
- mockInstalledJobIds,
- mockJobsSummaryResponse,
-} from './__mocks__/api';
-import {
- getConfigTemplatesToInstall,
- getIndexPatternTitleIdMapping,
- getIndexPatternTitles,
- getJobsToDisplay,
- getJobsToInstall,
- searchFilter,
- getStablePatternTitles,
-} from './helpers';
+import { mockSiemJobs } from './__mocks__/api';
+import { filterJobs, getStablePatternTitles, searchFilter } from './helpers';
jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({
hasMlAdminPermissions: () => true,
}));
describe('helpers', () => {
- describe('getJobsToInstall', () => {
- test('returns jobIds from all ConfigTemplates', () => {
- const jobsToInstall = getJobsToInstall(mockConfigTemplates);
- expect(jobsToInstall.length).toEqual(3);
- });
- });
-
- describe('getConfigTemplatesToInstall', () => {
- test('returns all configTemplates if no jobs are installed', () => {
- const configTemplatesToInstall = getConfigTemplatesToInstall(
- mockConfigTemplates,
- [],
- ['auditbeat-*', 'winlogbeat-*']
- );
- expect(configTemplatesToInstall.length).toEqual(2);
- });
-
- test('returns subset of configTemplates if index not available', () => {
- const configTemplatesToInstall = getConfigTemplatesToInstall(
- mockConfigTemplates,
- [],
- ['auditbeat-*', 'spongbeat-*']
- );
- expect(configTemplatesToInstall.length).toEqual(1);
- });
-
- test('returns all configTemplates if only partial jobs installed', () => {
- const configTemplatesToInstall = getConfigTemplatesToInstall(
- mockConfigTemplates,
- mockInstalledJobIds,
- ['auditbeat-*', 'winlogbeat-*']
- );
- expect(configTemplatesToInstall.length).toEqual(2);
- });
-
- test('returns no configTemplates if index is substring of indexPatterns', () => {
- const configTemplatesToInstall = getConfigTemplatesToInstall(
- mockConfigTemplates,
- mockInstalledJobIds,
- ['winlogbeat-**']
- );
- expect(configTemplatesToInstall.length).toEqual(0);
- });
- });
-
- describe('getJobsToDisplay', () => {
- test('returns empty array when null summaryData provided', () => {
- const jobsToDisplay = getJobsToDisplay(null, mockEmbeddedJobIds, false, false);
- expect(jobsToDisplay.length).toEqual(0);
- });
-
- test('returns all DisplayJobs', () => {
- const jobsToDisplay = getJobsToDisplay(
- mockJobsSummaryResponse,
- mockEmbeddedJobIds,
- false,
- false
- );
- expect(jobsToDisplay.length).toEqual(4);
- });
-
- test('returns DisplayJobs matching only embeddedJobs', () => {
- const jobsToDisplay = getJobsToDisplay(
- mockJobsSummaryResponse,
- mockEmbeddedJobIds,
- true,
- false
- );
- expect(jobsToDisplay.length).toEqual(3);
- });
-
- test('returns only custom DisplayJobs from jobsSummary', () => {
- const jobsToDisplay = getJobsToDisplay(
- mockJobsSummaryResponse,
- mockEmbeddedJobIds,
- false,
- true
- );
- expect(jobsToDisplay.length).toEqual(1);
+ describe('filterJobs', () => {
+ test('returns all jobs when no filter is suplied', () => {
+ const filteredJobs = filterJobs({
+ jobs: mockSiemJobs,
+ selectedGroups: [],
+ showCustomJobs: false,
+ showElasticJobs: false,
+ filterQuery: '',
+ });
+ expect(filteredJobs.length).toEqual(3);
});
});
describe('searchFilter', () => {
test('returns all jobs when nullfilterQuery is provided', () => {
- const jobsToDisplay = searchFilter(mockJobsSummaryResponse);
- expect(jobsToDisplay.length).toEqual(mockJobsSummaryResponse.length);
+ const jobsToDisplay = searchFilter(mockSiemJobs);
+ expect(jobsToDisplay.length).toEqual(mockSiemJobs.length);
});
test('returns correct DisplayJobs when filterQuery matches job.id', () => {
- const jobsToDisplay = searchFilter(mockJobsSummaryResponse, 'rare');
- expect(jobsToDisplay.length).toEqual(3);
+ const jobsToDisplay = searchFilter(mockSiemJobs, 'rare_process');
+ expect(jobsToDisplay.length).toEqual(2);
});
test('returns correct DisplayJobs when filterQuery matches job.description', () => {
- const jobsToDisplay = searchFilter(mockJobsSummaryResponse, 'high number');
- expect(jobsToDisplay.length).toEqual(1);
- });
- });
-
- describe('getIndexPatternTitles', () => {
- test('returns empty array when no index patterns are provided', () => {
- const indexPatternTitles = getIndexPatternTitles([]);
- expect(indexPatternTitles.length).toEqual(0);
- });
-
- test('returns titles when index patterns are provided', () => {
- const indexPatternTitles = getIndexPatternTitles(mockIndexPatternSavedObjects);
- expect(indexPatternTitles.length).toEqual(2);
- });
- });
-
- describe('getIndexPatternTitleIdMapping', () => {
- test('returns empty array when no index patterns are provided', () => {
- const indexPatternTitleIdMapping = getIndexPatternTitleIdMapping([]);
- expect(indexPatternTitleIdMapping.length).toEqual(0);
- });
-
- test('returns correct mapping when index patterns are provided', () => {
- const indexPatternTitleIdMapping = getIndexPatternTitleIdMapping(
- mockIndexPatternSavedObjects
- );
- expect(indexPatternTitleIdMapping).toEqual([
- { id: '2d1fe420-eeee-11e9-ad95-4b5e687c2aee', title: 'filebeat-*' },
- { id: '5463ec70-c7ba-ffff-ad95-4b5e687c2aee', title: 'auditbeat-*' },
- ]);
+ const jobsToDisplay = searchFilter(mockSiemJobs, 'Detect unusually');
+ expect(jobsToDisplay.length).toEqual(2);
});
});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx
index d0fc3ab074ebf..eaca7a9068c37 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx
@@ -4,56 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ConfigTemplate, IndexPatternSavedObject, Job } from './types';
+import { SiemJob } from './types';
/**
- * Returns all `jobIds` for each configTemplate provided
- *
- * @param templates ConfigTemplates as provided by ML Team (https://github.com/elastic/machine-learning-data/issues/194#issuecomment-505779406)
- */
-export const getJobsToInstall = (templates: ConfigTemplate[]): string[] =>
- templates.reduce((jobs: string[], template) => [...jobs, ...template.jobs], []);
-
-/**
- * Returns which ConfigTemplates that need to be installed based off of which Jobs are already installed and the configured indexPattern
- *
- * @param templates ConfigTemplates as provided by ML Team
- * @param installedJobIds list of installed JobIds
- * @param indexPatterns list of the user's currently configured IndexPatterns
- */
-export const getConfigTemplatesToInstall = (
- templates: ConfigTemplate[],
- installedJobIds: string[],
- indexPatterns: string[]
-): ConfigTemplate[] =>
- templates
- .filter(ct => !ct.jobs.every(ctJobId => installedJobIds.includes(ctJobId)))
- .filter(ct => indexPatterns.includes(ct.defaultIndexPattern));
-
-/**
- * Returns a filtered array of Jobs that based on filterGroup selection (Elastic vs Custom Jobs) and any user provided filterQuery
+ * Returns a filtered array of Jobs according to JobsTableFilters selections
*
* @param jobs to filter
- * @param embeddedJobIds jobIds as defined in the ConfigTemplates provided by the ML Team
+ * @param selectedGroups groups to filter on
* @param showCustomJobs whether or not to show all Custom Jobs (Non-embedded Jobs in SIEM Group)
* @param showElasticJobs whether or not to show Elastic Jobs (Embedded ConfigTemplate Jobs)
* @param filterQuery user-provided search string to filter for occurrence in job names/description
*/
-export const getJobsToDisplay = (
- jobs: Job[] | null,
- embeddedJobIds: string[],
- showCustomJobs: boolean,
- showElasticJobs: boolean,
- filterQuery?: string
-): Job[] =>
- jobs
- ? searchFilter(
- jobs
- .filter(job => (showCustomJobs ? embeddedJobIds.includes(job.id) : true))
- .filter(job => (showElasticJobs ? !embeddedJobIds.includes(job.id) : true)),
- filterQuery
- )
- : [];
+export const filterJobs = ({
+ jobs,
+ selectedGroups,
+ showCustomJobs,
+ showElasticJobs,
+ filterQuery,
+}: {
+ jobs: SiemJob[];
+ selectedGroups: string[];
+ showCustomJobs: boolean;
+ showElasticJobs: boolean;
+ filterQuery: string;
+}): SiemJob[] =>
+ searchFilter(
+ jobs
+ .filter(job => !showCustomJobs || (showCustomJobs && !job.isElasticJob))
+ .filter(job => !showElasticJobs || (showElasticJobs && job.isElasticJob))
+ .filter(
+ job => selectedGroups.length === 0 || selectedGroups.some(g => job.groups.includes(g))
+ ),
+ filterQuery
+ );
/**
* Returns filtered array of Jobs based on user-provided search string to filter for occurrence in job names/description
@@ -61,21 +44,13 @@ export const getJobsToDisplay = (
* @param jobs to filter
* @param filterQuery user-provided search string to filter for occurrence in job names/description
*/
-export const searchFilter = (jobs: Job[], filterQuery?: string): Job[] =>
+export const searchFilter = (jobs: SiemJob[], filterQuery?: string): SiemJob[] =>
jobs.filter(job =>
filterQuery == null
? true
: job.id.includes(filterQuery) || job.description.includes(filterQuery)
);
-/**
- * Returns a string array of Index Pattern Titles
- *
- * @param indexPatterns IndexPatternSavedObject[] as provided from the useIndexPatterns() hook
- */
-export const getIndexPatternTitles = (indexPatterns: IndexPatternSavedObject[]): string[] =>
- indexPatterns.reduce((acc: string[], v) => [...acc, v.attributes.title], []);
-
/**
* Given an array of titles this will always return the same string for usage within
* useEffect and other shallow compare areas.
@@ -83,19 +58,3 @@ export const getIndexPatternTitles = (indexPatterns: IndexPatternSavedObject[]):
* @param patterns string[] string array that will return a stable reference regardless of ordering or case sensitivity.
*/
export const getStablePatternTitles = (patterns: string[]) => patterns.sort().join();
-
-/**
- * Returns a mapping of indexPatternTitle to indexPatternId
- *
- * @param indexPatterns IndexPatternSavedObject[] as provided from the useIndexPatterns() hook
- */
-export const getIndexPatternTitleIdMapping = (
- indexPatterns: IndexPatternSavedObject[]
-): Array<{ title: string; id: string }> =>
- indexPatterns.reduce((acc: Array<{ title: string; id: string }>, v) => {
- if (v.attributes && v.attributes.title) {
- return [...acc, { title: v.attributes.title, id: v.id }];
- } else {
- return acc;
- }
- }, []);
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/translations.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/translations.ts
index 3982931a2b7ca..4740dbb59e352 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/translations.ts
@@ -6,20 +6,6 @@
import { i18n } from '@kbn/i18n';
-export const INDEX_PATTERN_FETCH_FAILURE = i18n.translate(
- 'xpack.siem.components.mlPopup.hooks.errors.indexPatternFetchFailureTitle',
- {
- defaultMessage: 'Index pattern fetch failure',
- }
-);
-
-export const JOB_SUMMARY_FETCH_FAILURE = i18n.translate(
- 'xpack.siem.components.mlPopup.hooks.errors.jobSummaryFetchFailureTitle',
- {
- defaultMessage: 'Job summary fetch failure',
- }
-);
-
export const SIEM_JOB_FETCH_FAILURE = i18n.translate(
'xpack.siem.components.mlPopup.hooks.errors.siemJobFetchFailureTitle',
{
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_job_summary_data.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_job_summary_data.test.tsx
deleted file mode 100644
index ad863369199a8..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_job_summary_data.test.tsx
+++ /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 { getSiemJobsFromJobsSummary } from './use_job_summary_data';
-import { mockJobsSummaryResponse } from '../__mocks__/api';
-
-describe('useJobSummaryData', () => {
- describe('getSiemJobsFromJobsSummary', () => {
- test('returns all jobs that are in the siem group', () => {
- const siemJobs = getSiemJobsFromJobsSummary(mockJobsSummaryResponse);
- expect(siemJobs.length).toEqual(3);
- });
- });
-});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_job_summary_data.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_job_summary_data.tsx
deleted file mode 100644
index 6ae18bc15ab9c..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_job_summary_data.tsx
+++ /dev/null
@@ -1,75 +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 { useContext, useEffect, useState } from 'react';
-
-import { jobsSummary } from '../api';
-import { Job } from '../types';
-import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions';
-import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider';
-import { useStateToaster } from '../../toasters';
-import { errorToToaster } from '../../ml/api/error_to_toaster';
-import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
-import { DEFAULT_KBN_VERSION } from '../../../../common/constants';
-
-import * as i18n from './translations';
-
-type Return = [boolean, Job[] | null];
-
-export const getSiemJobsFromJobsSummary = (data: Job[]) =>
- data.reduce((jobs: Job[], job: Job) => {
- return job.groups.includes('siem') ? [...jobs, job] : jobs;
- }, []);
-
-export const useJobSummaryData = (jobIds: string[] = [], refreshToggle = false): Return => {
- const [jobSummaryData, setJobSummaryData] = useState(null);
- const [loading, setLoading] = useState(true);
- const capabilities = useContext(MlCapabilitiesContext);
- const userPermissions = hasMlUserPermissions(capabilities);
- const [, dispatchToaster] = useStateToaster();
- const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
-
- useEffect(() => {
- let isSubscribed = true;
- const abortCtrl = new AbortController();
- setLoading(true);
-
- async function fetchSiemJobsFromJobsSummary() {
- if (userPermissions) {
- try {
- const data: Job[] = await jobsSummary(
- jobIds,
- {
- 'kbn-version': kbnVersion,
- },
- abortCtrl.signal
- );
-
- // TODO: API returns all jobs even though we specified jobIds -- jobsSummary call seems to match request in ML App?
- const siemJobs = getSiemJobsFromJobsSummary(data);
- if (isSubscribed) {
- setJobSummaryData(siemJobs);
- }
- } catch (error) {
- if (isSubscribed) {
- errorToToaster({ title: i18n.JOB_SUMMARY_FETCH_FAILURE, error, dispatchToaster });
- }
- }
- }
- if (isSubscribed) {
- setLoading(false);
- }
- }
-
- fetchSiemJobsFromJobsSummary();
- return () => {
- isSubscribed = false;
- abortCtrl.abort();
- };
- }, [refreshToggle, userPermissions]);
-
- return [loading, jobSummaryData];
-};
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.test.tsx
deleted file mode 100644
index e1f5a346d092e..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.test.tsx
+++ /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 { mockGroupsResponse } from '../__mocks__/api';
-import { getSiemJobIdsFromGroupsData } from './use_siem_jobs';
-
-describe('useSiemJobs', () => {
- describe('getSiemJobsFromGroupData', () => {
- test('returns all jobIds for siem group', () => {
- const siemJobIds = getSiemJobIdsFromGroupsData(mockGroupsResponse);
- expect(siemJobIds.length).toEqual(6);
- });
- });
-});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx
index f7f45d67d3468..89f995773e833 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx
@@ -4,33 +4,37 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { useState, useEffect, useContext } from 'react';
+import { useContext, useEffect, useState } from 'react';
-import { groupsData } from '../api';
-import { Group } from '.././types';
+import { checkRecognizer, getJobsSummary, getModules } from '../api';
+import { SiemJob } from '../types';
import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider';
import { useStateToaster } from '../../toasters';
import { errorToToaster } from '../../ml/api/error_to_toaster';
import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
-import { DEFAULT_KBN_VERSION } from '../../../../common/constants';
+import { DEFAULT_INDEX_KEY } from '../../../../common/constants';
import * as i18n from './translations';
+import { createSiemJobs } from './use_siem_jobs_helpers';
-type Return = [boolean, string[]];
-
-export const getSiemJobIdsFromGroupsData = (data: Group[]) =>
- data.reduce((jobIds: string[], group: Group) => {
- return group.id === 'siem' ? [...jobIds, ...group.jobIds] : jobIds;
- }, []);
+type Return = [boolean, SiemJob[]];
+/**
+ * Compiles a collection of SiemJobs, which are a list of all jobs relevant to the SIEM App. This
+ * includes all installed jobs in the `SIEM` ML group, and all jobs within ML Modules defined in
+ * ml_module (whether installed or not). Use the corresponding helper functions to filter the job
+ * list as necessary. E.g. installed jobs, running jobs, etc.
+ *
+ * @param refetchData
+ */
export const useSiemJobs = (refetchData: boolean): Return => {
- const [siemJobs, setSiemJobs] = useState([]);
+ const [siemJobs, setSiemJobs] = useState([]);
const [loading, setLoading] = useState(true);
const capabilities = useContext(MlCapabilitiesContext);
const userPermissions = hasMlUserPermissions(capabilities);
+ const [siemDefaultIndex] = useKibanaUiSetting(DEFAULT_INDEX_KEY);
const [, dispatchToaster] = useStateToaster();
- const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
useEffect(() => {
let isSubscribed = true;
@@ -40,16 +44,17 @@ export const useSiemJobs = (refetchData: boolean): Return => {
async function fetchSiemJobIdsFromGroupsData() {
if (userPermissions) {
try {
- const data = await groupsData(
- {
- 'kbn-version': kbnVersion,
- },
- abortCtrl.signal
- );
+ // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex
+ const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([
+ getJobsSummary(abortCtrl.signal),
+ getModules({ signal: abortCtrl.signal }),
+ checkRecognizer({ indexPatternName: siemDefaultIndex, signal: abortCtrl.signal }),
+ ]);
+
+ const compositeSiemJobs = createSiemJobs(jobSummaryData, modulesData, compatibleModules);
- const siemJobIds = getSiemJobIdsFromGroupsData(data);
if (isSubscribed) {
- setSiemJobs(siemJobIds);
+ setSiemJobs(compositeSiemJobs);
}
} catch (error) {
if (isSubscribed) {
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx
new file mode 100644
index 0000000000000..fc9f369a305aa
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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 {
+ composeModuleAndInstalledJobs,
+ createSiemJobs,
+ getAugmentedFields,
+ getInstalledJobs,
+ getModuleJobs,
+ moduleToSiemJob,
+} from './use_siem_jobs_helpers';
+import {
+ checkRecognizerSuccess,
+ mockGetModuleResponse,
+ mockJobsSummaryResponse,
+} from '../__mocks__/api';
+
+// TODO: Expand test coverage
+
+describe('useSiemJobsHelpers', () => {
+ describe('moduleToSiemJob', () => {
+ test('correctly converts module to SiemJob', () => {
+ const siemJob = moduleToSiemJob(
+ mockGetModuleResponse[0],
+ mockGetModuleResponse[0].jobs[0],
+ false
+ );
+ expect(siemJob).toEqual({
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ defaultIndexPattern: 'auditbeat-*',
+ description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)',
+ groups: ['auditbeat', 'process', 'siem'],
+ hasDatafeed: false,
+ id: 'rare_process_by_host_linux_ecs',
+ isCompatible: false,
+ isElasticJob: true,
+ isInstalled: false,
+ isSingleMetricViewerJob: false,
+ jobState: '',
+ memory_status: '',
+ moduleId: 'siem_auditbeat',
+ processed_record_count: 0,
+ });
+ });
+
+ describe('getAugmentedFields', () => {
+ test('return correct augmented fields for given matching compatible modules', () => {
+ const moduleJobs = getModuleJobs(mockGetModuleResponse, ['siem_auditbeat']);
+ const augmentedFields = getAugmentedFields('rare_process_by_host_linux_ecs', moduleJobs, [
+ 'siem_auditbeat',
+ ]);
+ expect(augmentedFields).toEqual({
+ defaultIndexPattern: 'auditbeat-*',
+ isCompatible: true,
+ isElasticJob: true,
+ moduleId: 'siem_auditbeat',
+ });
+ });
+ });
+
+ describe('getModuleJobs', () => {
+ test('returns all jobs within a module for a compatible moduleId', () => {
+ const moduleJobs = getModuleJobs(mockGetModuleResponse, ['siem_auditbeat']);
+ expect(moduleJobs.length).toEqual(3);
+ });
+ });
+
+ describe('getInstalledJobs', () => {
+ test('returns all jobs from jobSummary for a compatible moduleId', () => {
+ const moduleJobs = getModuleJobs(mockGetModuleResponse, ['siem_auditbeat']);
+ const installedJobs = getInstalledJobs(mockJobsSummaryResponse, moduleJobs, [
+ 'siem_auditbeat',
+ ]);
+ expect(installedJobs.length).toEqual(3);
+ });
+ });
+
+ describe('composeModuleAndInstalledJobs', () => {
+ test('returns correct number of jobs when composing separate module and installed jobs', () => {
+ const moduleJobs = getModuleJobs(mockGetModuleResponse, ['siem_auditbeat']);
+ const installedJobs = getInstalledJobs(mockJobsSummaryResponse, moduleJobs, [
+ 'siem_auditbeat',
+ ]);
+ const siemJobs = composeModuleAndInstalledJobs(installedJobs, moduleJobs);
+ expect(siemJobs.length).toEqual(6);
+ });
+ });
+
+ describe('createSiemJobs', () => {
+ test('returns correct number of jobs when creating jobs with successful responses', () => {
+ const siemJobs = createSiemJobs(
+ mockJobsSummaryResponse,
+ mockGetModuleResponse,
+ checkRecognizerSuccess
+ );
+ expect(siemJobs.length).toEqual(6);
+ });
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx
new file mode 100644
index 0000000000000..81b7914b81742
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx
@@ -0,0 +1,154 @@
+/*
+ * 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 {
+ AugmentedSiemJobFields,
+ JobSummary,
+ Module,
+ ModuleJob,
+ RecognizerModule,
+ SiemJob,
+} from '../types';
+import { mlModules } from '../ml_modules';
+
+/**
+ * Helper function for converting from ModuleJob -> SiemJob
+ * @param module
+ * @param moduleJob
+ * @param isCompatible
+ */
+export const moduleToSiemJob = (
+ module: Module,
+ moduleJob: ModuleJob,
+ isCompatible: boolean
+): SiemJob => {
+ return {
+ datafeedId: '',
+ datafeedIndices: [],
+ datafeedState: '',
+ hasDatafeed: false,
+ isSingleMetricViewerJob: false,
+ jobState: '',
+ memory_status: '',
+ processed_record_count: 0,
+ id: moduleJob.id,
+ description: moduleJob.config.description,
+ groups: [...moduleJob.config.groups].sort(),
+ defaultIndexPattern: module.defaultIndexPattern,
+ moduleId: module.id,
+ isCompatible,
+ isInstalled: false,
+ isElasticJob: true,
+ };
+};
+
+/**
+ * Returns fields necessary to augment a ModuleJob to a SiemJob
+ *
+ * @param jobId
+ * @param moduleJobs
+ * @param compatibleModuleIds
+ */
+export const getAugmentedFields = (
+ jobId: string,
+ moduleJobs: SiemJob[],
+ compatibleModuleIds: string[]
+): AugmentedSiemJobFields => {
+ const moduleJob = moduleJobs.find(mj => mj.id === jobId);
+ return moduleJob !== undefined
+ ? {
+ moduleId: moduleJob.moduleId,
+ defaultIndexPattern: moduleJob.defaultIndexPattern,
+ isCompatible: compatibleModuleIds.includes(moduleJob.moduleId),
+ isElasticJob: true,
+ }
+ : {
+ moduleId: '',
+ defaultIndexPattern: '',
+ isCompatible: true,
+ isElasticJob: false,
+ };
+};
+
+/**
+ * Process Modules[] from the `get_module` ML API into SiemJobs[] by filtering to SIEM specific
+ * modules and unpacking jobs from each module
+ *
+ * @param modulesData
+ * @param compatibleModuleIds
+ */
+export const getModuleJobs = (modulesData: Module[], compatibleModuleIds: string[]): SiemJob[] =>
+ modulesData
+ .filter(module => mlModules.includes(module.id))
+ .map(module => [
+ ...module.jobs.map(moduleJob =>
+ moduleToSiemJob(module, moduleJob, compatibleModuleIds.includes(module.id))
+ ),
+ ])
+ .flat();
+
+/**
+ * Process JobSummary[] from the `jobs_summary` ML API into SiemJobs[] by filtering to to SIEM jobs
+ * and augmenting with moduleId/defaultIndexPattern/isCompatible
+ *
+ * @param jobSummaryData
+ * @param moduleJobs
+ * @param compatibleModuleIds
+ */
+export const getInstalledJobs = (
+ jobSummaryData: JobSummary[],
+ moduleJobs: SiemJob[],
+ compatibleModuleIds: string[]
+): SiemJob[] =>
+ jobSummaryData
+ .filter(({ groups }) => groups.includes('siem'))
+ .map(jobSummary => ({
+ ...jobSummary,
+ ...getAugmentedFields(jobSummary.id, moduleJobs, compatibleModuleIds),
+ isInstalled: true,
+ }));
+
+/**
+ * Combines installed jobs + moduleSiemJobs that don't overlap and sorts by name asc
+ *
+ * @param installedJobs
+ * @param moduleSiemJobs
+ */
+export const composeModuleAndInstalledJobs = (
+ installedJobs: SiemJob[],
+ moduleSiemJobs: SiemJob[]
+): SiemJob[] => {
+ const installedJobsIds = installedJobs.map(installedJob => installedJob.id);
+
+ return [...installedJobs, ...moduleSiemJobs.filter(mj => !installedJobsIds.includes(mj.id))].sort(
+ (a, b) => a.id.localeCompare(b.id)
+ );
+};
+/**
+ * Creates a list of SiemJobs by composing JobSummary jobs (installed jobs) and Module
+ * jobs (pre-packaged SIEM jobs) into a single job object that can be used throughout the SIEM app
+ *
+ * @param jobSummaryData
+ * @param modulesData
+ * @param compatibleModules
+ */
+export const createSiemJobs = (
+ jobSummaryData: JobSummary[],
+ modulesData: Module[],
+ compatibleModules: RecognizerModule[]
+): SiemJob[] => {
+ // Create lookup of compatible modules
+ const compatibleModuleIds = compatibleModules.map(module => module.id);
+
+ // Process modulesData: Filter to SIEM specific modules, and unpack jobs from modules
+ const moduleSiemJobs = getModuleJobs(modulesData, compatibleModuleIds);
+
+ // Process jobSummaryData: Filter to SIEM jobs, and augment with moduleId/defaultIndexPattern/isCompatible
+ const installedJobs = getInstalledJobs(jobSummaryData, moduleSiemJobs, compatibleModuleIds);
+
+ // Combine installed jobs + moduleSiemJobs that don't overlap, and sort by name asc
+ return composeModuleAndInstalledJobs(installedJobs, moduleSiemJobs);
+};
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/filter_group.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/filter_group.test.tsx.snap
deleted file mode 100644
index aa8524366d2b2..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/filter_group.test.tsx.snap
+++ /dev/null
@@ -1,52 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`FilterGroup renders correctly against snapshot 1`] = `
-
-
-
-
-
-
-
- Elastic jobs
-
-
- Custom jobs
-
-
-
-
-`;
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap
index aeb4ce089883b..7f7c63504f317 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap
@@ -8,7 +8,7 @@ exports[`JobSwitch renders correctly against snapshot 1`] = `
grow={false}
>
{
- test('renders correctly against snapshot', () => {
- const wrapper = shallow(
-
- );
- expect(toJson(wrapper)).toMatchSnapshot();
- });
-
- test('when you click filter onChange is called and filter updated', () => {
- const mockSetShowAllJobs = jest.fn();
- const wrapper = mount(
-
- );
-
- wrapper
- .find('[data-test-subj="show-elastic-jobs-filter-button"]')
- .first()
- .simulate('click');
- wrapper.update();
-
- expect(mockSetShowAllJobs.mock.calls[0]).toEqual([true]);
- });
-});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filter_group.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filter_group.tsx
deleted file mode 100644
index 6a70fee696297..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filter_group.tsx
+++ /dev/null
@@ -1,71 +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 {
- EuiFilterButton,
- EuiFilterGroup,
- EuiFlexGroup,
- EuiFlexItem,
- // @ts-ignore no-exported-member
- EuiSearchBar,
-} from '@elastic/eui';
-
-import { EuiSearchBarQuery } from '../../open_timeline/types';
-import * as i18n from '../translations';
-
-interface FilterGroupProps {
- showCustomJobs: boolean;
- setShowCustomJobs: (showCustomJobs: boolean) => void;
- showElasticJobs: boolean;
- setShowElasticJobs: (showCustomJobs: boolean) => void;
- setFilterQuery: (filterQuery: string) => void;
-}
-
-export const FilterGroup = React.memo(
- ({ showCustomJobs, setShowCustomJobs, showElasticJobs, setShowElasticJobs, setFilterQuery }) => (
-
-
- setFilterQuery(query.queryText.trim())}
- />
-
-
-
-
- {
- setShowCustomJobs(!showCustomJobs);
- setShowElasticJobs(false);
- }}
- data-test-subj="show-custom-jobs-filter-button"
- withNext
- >
- {i18n.SHOW_ELASTIC_JOBS}
-
- {
- setShowElasticJobs(!showElasticJobs);
- setShowCustomJobs(false);
- }}
- data-test-subj="show-elastic-jobs-filter-button"
- >
- {i18n.SHOW_CUSTOM_JOBS}
-
-
-
-
- )
-);
-
-FilterGroup.displayName = 'FilterGroup';
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap
new file mode 100644
index 0000000000000..4c9a27b76060c
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GroupsFilterPopover renders correctly against snapshot 1`] = `
+
+ Groups
+
+ }
+ closePopover={[Function]}
+ display="inlineBlock"
+ hasArrow={true}
+ isOpen={false}
+ ownFocus={true}
+ panelPaddingSize="none"
+>
+
+ auditbeat (2)
+
+
+ process (3)
+
+
+ winlogbeat (1)
+
+
+`;
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap
new file mode 100644
index 0000000000000..fac91f75978f0
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap
@@ -0,0 +1,138 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`JobsTableFilters renders correctly against snapshot 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ Elastic jobs
+
+
+ Custom jobs
+
+
+
+
+`;
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx
new file mode 100644
index 0000000000000..cb8748ea5e997
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx
@@ -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.
+ */
+
+import { mount, shallow } from 'enzyme';
+import toJson from 'enzyme-to-json';
+import * as React from 'react';
+import { GroupsFilterPopover } from './groups_filter_popover';
+import { mockSiemJobs } from '../../__mocks__/api';
+import { SiemJob } from '../../types';
+import { cloneDeep } from 'lodash/fp';
+
+describe('GroupsFilterPopover', () => {
+ let siemJobs: SiemJob[];
+
+ beforeEach(() => {
+ siemJobs = cloneDeep(mockSiemJobs);
+ });
+
+ test('renders correctly against snapshot', () => {
+ const wrapper = shallow(
+
+ );
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+
+ test('when a filter is clicked, it becomes checked ', () => {
+ const mockOnSelectedGroupsChanged = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ wrapper
+ .find('[data-test-subj="groups-filter-popover-button"]')
+ .first()
+ .simulate('click');
+ wrapper.update();
+
+ wrapper
+ .find('EuiFilterSelectItem')
+ .first()
+ .simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper
+ .find('EuiFilterSelectItem')
+ .first()
+ .prop('checked')
+ ).toEqual('on');
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx
new file mode 100644
index 0000000000000..e39046ba013c7
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx
@@ -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 React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
+import {
+ EuiFilterButton,
+ EuiFilterSelectItem,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiPopover,
+ EuiSpacer,
+} from '@elastic/eui';
+import * as i18n from './translations';
+import { SiemJob } from '../../types';
+import { toggleSelectedGroup } from './toggle_selected_group';
+
+interface GroupsFilterPopoverProps {
+ siemJobs: SiemJob[];
+ onSelectedGroupsChanged: Dispatch>;
+}
+
+/**
+ * Popover for selecting which SiemJob groups to filter on. Component extracts unique groups and
+ * their counts from the provided SiemJobs. The 'siem' group is filtered out as all jobs will be
+ * siem jobs
+ *
+ * @param siemJobs jobs to fetch groups from to display for filtering
+ * @param onSelectedGroupsChanged change listener to be notified when group selection changes
+ */
+export const GroupsFilterPopover = React.memo(
+ ({ siemJobs, onSelectedGroupsChanged }) => {
+ const [isGroupPopoverOpen, setIsGroupPopoverOpen] = useState(false);
+ const [selectedGroups, setSelectedGroups] = useState([]);
+
+ const groups = siemJobs
+ .map(j => j.groups)
+ .flat()
+ .filter(g => g !== 'siem');
+ const uniqueGroups = Array.from(new Set(groups));
+
+ useEffect(() => {
+ onSelectedGroupsChanged(selectedGroups);
+ }, [selectedGroups.sort().join()]);
+
+ return (
+ setIsGroupPopoverOpen(!isGroupPopoverOpen)}
+ isSelected={isGroupPopoverOpen}
+ hasActiveFilters={selectedGroups.length > 0}
+ numActiveFilters={selectedGroups.length}
+ >
+ {i18n.GROUPS}
+
+ }
+ isOpen={isGroupPopoverOpen}
+ closePopover={() => setIsGroupPopoverOpen(!isGroupPopoverOpen)}
+ panelPaddingSize="none"
+ >
+ {uniqueGroups.map((group, index) => (
+ toggleSelectedGroup(group, selectedGroups, setSelectedGroups)}
+ >
+ {`${group} (${groups.filter(g => g === group).length})`}
+
+ ))}
+ {uniqueGroups.length === 0 && (
+
+
+
+
+ {i18n.NO_GROUPS_AVAILABLE}
+
+
+ )}
+
+ );
+ }
+);
+
+GroupsFilterPopover.displayName = 'GroupsFilterPopover';
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx
new file mode 100644
index 0000000000000..5838c3105de6d
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { mount, shallow } from 'enzyme';
+import toJson from 'enzyme-to-json';
+import * as React from 'react';
+import { JobsTableFilters } from './jobs_table_filters';
+import { SiemJob } from '../../types';
+import { cloneDeep } from 'lodash/fp';
+import { mockSiemJobs } from '../../__mocks__/api';
+
+describe('JobsTableFilters', () => {
+ let siemJobs: SiemJob[];
+
+ beforeEach(() => {
+ siemJobs = cloneDeep(mockSiemJobs);
+ });
+
+ test('renders correctly against snapshot', () => {
+ const wrapper = shallow( );
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+
+ test('when you click Elastic Jobs filter, state is updated and it is selected', () => {
+ const onFilterChanged = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ wrapper
+ .find('[data-test-subj="show-elastic-jobs-filter-button"]')
+ .first()
+ .simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper
+ .find('[data-test-subj="show-elastic-jobs-filter-button"]')
+ .first()
+ .prop('hasActiveFilters')
+ ).toEqual(true);
+ });
+
+ test('when you click Custom Jobs filter, state is updated and it is selected', () => {
+ const onFilterChanged = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ wrapper
+ .find('[data-test-subj="show-custom-jobs-filter-button"]')
+ .first()
+ .simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper
+ .find('[data-test-subj="show-custom-jobs-filter-button"]')
+ .first()
+ .prop('hasActiveFilters')
+ ).toEqual(true);
+ });
+
+ test('when you click Custom Jobs filter once, then Elastic Jobs filter, state is updated and selected changed', () => {
+ const onFilterChanged = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ wrapper
+ .find('[data-test-subj="show-custom-jobs-filter-button"]')
+ .first()
+ .simulate('click');
+ wrapper.update();
+
+ wrapper
+ .find('[data-test-subj="show-elastic-jobs-filter-button"]')
+ .first()
+ .simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper
+ .find('[data-test-subj="show-custom-jobs-filter-button"]')
+ .first()
+ .prop('hasActiveFilters')
+ ).toEqual(false);
+ expect(
+ wrapper
+ .find('[data-test-subj="show-elastic-jobs-filter-button"]')
+ .first()
+ .prop('hasActiveFilters')
+ ).toEqual(true);
+ });
+
+ test('when you click Custom Jobs filter twice, state is updated and it is revert', () => {
+ const onFilterChanged = jest.fn();
+ const wrapper = mount(
+
+ );
+
+ wrapper
+ .find('[data-test-subj="show-custom-jobs-filter-button"]')
+ .first()
+ .simulate('click');
+ wrapper.update();
+
+ wrapper
+ .find('[data-test-subj="show-custom-jobs-filter-button"]')
+ .first()
+ .simulate('click');
+ wrapper.update();
+
+ expect(
+ wrapper
+ .find('[data-test-subj="show-custom-jobs-filter-button"]')
+ .first()
+ .prop('hasActiveFilters')
+ ).toEqual(false);
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx
new file mode 100644
index 0000000000000..ba080757d34a8
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx
@@ -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 React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
+
+import {
+ EuiFilterButton,
+ EuiFilterGroup,
+ EuiFlexGroup,
+ EuiFlexItem,
+ // @ts-ignore no-exported-member
+ EuiSearchBar,
+} from '@elastic/eui';
+import { EuiSearchBarQuery } from '../../../open_timeline/types';
+import * as i18n from './translations';
+import { JobsFilters, SiemJob } from '../../types';
+import { GroupsFilterPopover } from './groups_filter_popover';
+
+interface JobsTableFiltersProps {
+ siemJobs: SiemJob[];
+ onFilterChanged: Dispatch>;
+}
+
+/**
+ * Collection of filters for filtering data within the JobsTable. Contains search bar, Elastic/Custom
+ * Jobs filter button toggle, and groups selection
+ *
+ * @param siemJobs jobs to fetch groups from to display for filtering
+ * @param onFilterChanged change listener to be notified on filter changes
+ */
+export const JobsTableFilters = React.memo(
+ ({ siemJobs, onFilterChanged }) => {
+ const [filterQuery, setFilterQuery] = useState('');
+ const [selectedGroups, setSelectedGroups] = useState([]);
+ const [showCustomJobs, setShowCustomJobs] = useState(false);
+ const [showElasticJobs, setShowElasticJobs] = useState(false);
+
+ // Propagate filter changes to parent
+ useEffect(() => {
+ onFilterChanged({ filterQuery, showCustomJobs, showElasticJobs, selectedGroups });
+ }, [filterQuery, selectedGroups.sort().join(), showCustomJobs, showElasticJobs]);
+
+ return (
+
+
+ setFilterQuery(query.queryText.trim())}
+ />
+
+
+
+
+
+
+
+
+
+
+ {
+ setShowElasticJobs(!showElasticJobs);
+ setShowCustomJobs(false);
+ }}
+ data-test-subj="show-elastic-jobs-filter-button"
+ withNext
+ >
+ {i18n.SHOW_ELASTIC_JOBS}
+
+ {
+ setShowCustomJobs(!showCustomJobs);
+ setShowElasticJobs(false);
+ }}
+ data-test-subj="show-custom-jobs-filter-button"
+ >
+ {i18n.SHOW_CUSTOM_JOBS}
+
+
+
+
+ );
+ }
+);
+
+JobsTableFilters.displayName = 'JobsTableFilters';
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx
new file mode 100644
index 0000000000000..353012732e1ae
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { toggleSelectedGroup } from './toggle_selected_group';
+
+describe('#toggleSelectedGroup', () => {
+ let setSelectedGroups: jest.Mock;
+ beforeEach(() => {
+ setSelectedGroups = jest.fn();
+ });
+
+ test('removes only job', () => {
+ toggleSelectedGroup('deadbeat', ['deadbeat'], setSelectedGroups);
+ expect(setSelectedGroups.mock.calls[0][0]).toEqual([]);
+ });
+
+ test('removes first job', () => {
+ toggleSelectedGroup('siem', ['siem', 'frankbeat', 'auditbeat'], setSelectedGroups);
+ expect(setSelectedGroups.mock.calls[0][0]).toEqual(['frankbeat', 'auditbeat']);
+ });
+
+ test('removes middle job', () => {
+ toggleSelectedGroup('frankbeat', ['siem', 'frankbeat', 'auditbeat'], setSelectedGroups);
+ expect(setSelectedGroups.mock.calls[0][0]).toEqual(['siem', 'auditbeat']);
+ });
+
+ test('removes last job', () => {
+ toggleSelectedGroup('auditbeat', ['siem', 'frankbeat', 'auditbeat'], setSelectedGroups);
+ expect(setSelectedGroups.mock.calls[0][0]).toEqual(['siem', 'frankbeat']);
+ });
+
+ test('adds job if element does not exist', () => {
+ toggleSelectedGroup('deadbeat', ['siem', 'frankbeat', 'auditbeat'], setSelectedGroups);
+ expect(setSelectedGroups.mock.calls[0][0]).toEqual([
+ 'siem',
+ 'frankbeat',
+ 'auditbeat',
+ 'deadbeat',
+ ]);
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx
new file mode 100644
index 0000000000000..f6f252a4d7b9f
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx
@@ -0,0 +1,22 @@
+/*
+ * 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 { Dispatch, SetStateAction } from 'react';
+
+export const toggleSelectedGroup = (
+ group: string,
+ selectedGroups: string[],
+ setSelectedGroups: Dispatch>
+): void => {
+ const selectedGroupIndex = selectedGroups.indexOf(group);
+ const updatedSelectedGroups = [...selectedGroups];
+ if (selectedGroupIndex >= 0) {
+ updatedSelectedGroups.splice(selectedGroupIndex, 1);
+ } else {
+ updatedSelectedGroups.push(group);
+ }
+ setSelectedGroups(updatedSelectedGroups);
+};
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts
new file mode 100644
index 0000000000000..b52a933407c2e
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const FILTER_PLACEHOLDER = i18n.translate(
+ 'xpack.siem.components.mlPopover.jobsTable.filters.searchFilterPlaceholder',
+ {
+ defaultMessage: 'e.g. rare_process_linux',
+ }
+);
+
+export const GROUPS = i18n.translate(
+ 'xpack.siem.components.mlPopover.jobsTable.filters.groupsLabel',
+ {
+ defaultMessage: 'Groups',
+ }
+);
+
+export const NO_GROUPS_AVAILABLE = i18n.translate(
+ 'xpack.siem.components.mlPopover.jobsTable.filters.noGroupsAvailableDescription',
+ {
+ defaultMessage: 'No Groups available',
+ }
+);
+
+export const SHOW_ELASTIC_JOBS = i18n.translate(
+ 'xpack.siem.components.mlPopover.jobsTable.filters.showAllJobsLabel',
+ {
+ defaultMessage: 'Elastic jobs',
+ }
+);
+
+export const SHOW_CUSTOM_JOBS = i18n.translate(
+ 'xpack.siem.components.mlPopover.jobsTable.filters.showSiemJobsLabel',
+ {
+ defaultMessage: 'Custom jobs',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx
index 844d346405e0e..2e869cce9ddf7 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx
@@ -9,17 +9,25 @@ import toJson from 'enzyme-to-json';
import * as React from 'react';
import { isChecked, isFailure, isJobLoading, JobSwitch } from './job_switch';
-import { mockOpenedJob } from '../__mocks__/api';
+import { cloneDeep } from 'lodash/fp';
+import { mockSiemJobs } from '../__mocks__/api';
+import { SiemJob } from '../types';
describe('JobSwitch', () => {
+ let siemJobs: SiemJob[];
let onJobStateChangeMock = jest.fn();
beforeEach(() => {
+ siemJobs = cloneDeep(mockSiemJobs);
onJobStateChangeMock = jest.fn();
});
test('renders correctly against snapshot', () => {
const wrapper = shallow(
-
+
);
expect(toJson(wrapper)).toMatchSnapshot();
});
@@ -27,8 +35,8 @@ describe('JobSwitch', () => {
test('should call onJobStateChange when the switch is clicked to be true/open', () => {
const wrapper = mount(
);
@@ -40,18 +48,18 @@ describe('JobSwitch', () => {
target: { checked: true },
});
- expect(onJobStateChangeMock.mock.calls[0]).toEqual([
- 'siem-api-rare_process_linux_ecs',
- 1562870521264,
- true,
- ]);
+ expect(onJobStateChangeMock.mock.calls[0][0].id).toEqual(
+ 'linux_anomalous_network_activity_ecs'
+ );
+ expect(onJobStateChangeMock.mock.calls[0][1]).toEqual(1571022859393);
+ expect(onJobStateChangeMock.mock.calls[0][2]).toEqual(true);
});
test('should have a switch when it is not in the loading state', () => {
const wrapper = mount(
);
@@ -61,8 +69,8 @@ describe('JobSwitch', () => {
test('should not have a switch when it is in the loading state', () => {
const wrapper = mount(
);
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx
index 1b06a7bcadf0c..b62478acaf197 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx
@@ -7,7 +7,7 @@
import styled from 'styled-components';
import React, { useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui';
-import { Job } from '../types';
+import { SiemJob } from '../types';
const StaticSwitch = styled(EuiSwitch)`
.euiSwitch__thumb,
@@ -19,9 +19,9 @@ const StaticSwitch = styled(EuiSwitch)`
StaticSwitch.displayName = 'StaticSwitch';
export interface JobSwitchProps {
- job: Job;
- isSummaryLoading: boolean;
- onJobStateChange: (jobName: string, latestTimestampMs: number, enable: boolean) => void;
+ job: SiemJob;
+ isSiemJobsLoading: boolean;
+ onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => void;
}
// Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js
@@ -42,13 +42,13 @@ export const isFailure = (jobState: string, datafeedState: string): boolean => {
};
export const JobSwitch = React.memo(
- ({ job, isSummaryLoading, onJobStateChange }) => {
+ ({ job, isSiemJobsLoading, onJobStateChange }) => {
const [isLoading, setIsLoading] = useState(false);
return (
- {isSummaryLoading || isLoading || isJobLoading(job.jobState, job.datafeedId) ? (
+ {isSiemJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedId) ? (
) : (
(
checked={isChecked(job.jobState, job.datafeedState)}
onChange={e => {
setIsLoading(true);
- onJobStateChange(job.id, job.latestTimestampMs || 0, e.target.checked);
+ onJobStateChange(job, job.latestTimestampMs || 0, e.target.checked);
}}
/>
)}
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx
index 932dad267e3fa..5bd1660d2db48 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx
@@ -8,51 +8,43 @@ import { shallow, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import * as React from 'react';
import { JobsTable } from './jobs_table';
-import { mockJobsSummaryResponse } from '../__mocks__/api';
+import { mockSiemJobs } from '../__mocks__/api';
import { cloneDeep } from 'lodash/fp';
+import { SiemJob } from '../types';
describe('JobsTable', () => {
+ let siemJobs: SiemJob[];
let onJobStateChangeMock = jest.fn();
beforeEach(() => {
+ siemJobs = cloneDeep(mockSiemJobs);
onJobStateChangeMock = jest.fn();
});
test('renders correctly against snapshot', () => {
const wrapper = shallow(
-
+
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('should render the hyperlink which points specifically to the job id', () => {
const wrapper = mount(
-
+
);
expect(
wrapper
.find('[data-test-subj="jobs-table-link"]')
.first()
.props().href
- ).toEqual('/test/base/path/app/ml#/jobs?mlManagement=(jobId:rc-rare-process-windows-5)');
+ ).toEqual(
+ '/test/base/path/app/ml#/jobs?mlManagement=(jobId:linux_anomalous_network_activity_ecs)'
+ );
});
test('should render the hyperlink with URI encodings which points specifically to the job id', () => {
- const cloneJobsSummaryResponse = cloneDeep(mockJobsSummaryResponse);
- cloneJobsSummaryResponse[0].id = 'job id with spaces';
+ siemJobs[0].id = 'job id with spaces';
const wrapper = mount(
-
+
);
expect(
wrapper
@@ -64,11 +56,7 @@ describe('JobsTable', () => {
test('should call onJobStateChange when the switch is clicked to be true/open', () => {
const wrapper = mount(
-
+
);
wrapper
.find('[data-test-subj="job-switch"] input')
@@ -76,31 +64,19 @@ describe('JobsTable', () => {
.simulate('change', {
target: { checked: true },
});
- expect(onJobStateChangeMock.mock.calls[0]).toEqual([
- 'rc-rare-process-windows-5',
- 1561402325194,
- true,
- ]);
+ expect(onJobStateChangeMock.mock.calls[0]).toEqual([siemJobs[0], 1571022859393, true]);
});
test('should have a switch when it is not in the loading state', () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="job-switch"]').exists()).toBe(true);
});
test('should not have a switch when it is in the loading state', () => {
const wrapper = mount(
-
+
);
expect(wrapper.find('[data-test-subj="job-switch"]').exists()).toBe(false);
});
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx
index 1cd429f92b6a9..b15c684b1bbbe 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx
@@ -9,17 +9,21 @@ import React, { useEffect, useState } from 'react';
import {
CENTER_ALIGNMENT,
+ EuiBadge,
EuiBasicTable,
EuiButton,
EuiEmptyPrompt,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
EuiLink,
EuiText,
} from '@elastic/eui';
import styled from 'styled-components';
-import * as i18n from '../translations';
+import * as i18n from './translations';
import { JobSwitch } from './job_switch';
-import { Job } from '../types';
+import { SiemJob } from '../types';
const JobNameWrapper = styled.div`
margin: 5px 0;
@@ -32,11 +36,11 @@ const truncateThreshold = 200;
const getJobsTableColumns = (
isLoading: boolean,
- onJobStateChange: (jobName: string, latestTimestampMs: number, enable: boolean) => void
+ onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => void
) => [
{
name: i18n.COLUMN_JOB_NAME,
- render: ({ id, description }: Job) => (
+ render: ({ id, description }: SiemJob) => (
),
},
+ {
+ name: i18n.COLUMN_GROUPS,
+ render: ({ groups }: SiemJob) => (
+
+ {groups.map(group => (
+
+ {group}
+
+ ))}
+
+ ),
+ width: '140px',
+ },
{
name: i18n.COLUMN_RUN_JOB,
- render: (job: Job) => (
-
- ),
+ render: (job: SiemJob) =>
+ job.isCompatible ? (
+
+ ) : (
+
+ ),
align: CENTER_ALIGNMENT,
width: '80px',
},
];
-const getPaginatedItems = (items: Job[], pageIndex: number, pageSize: number): Job[] =>
+const getPaginatedItems = (items: SiemJob[], pageIndex: number, pageSize: number): SiemJob[] =>
items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize);
export interface JobTableProps {
isLoading: boolean;
- jobs: Job[];
- onJobStateChange: (jobName: string, latestTimestampMs: number, enable: boolean) => void;
+ jobs: SiemJob[];
+ onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => void;
}
export const JobsTable = React.memo(({ isLoading, jobs, onJobStateChange }: JobTableProps) => {
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/translations.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/translations.ts
new file mode 100644
index 0000000000000..a508fda17228e
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/translations.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 COLUMN_JOB_NAME = i18n.translate(
+ 'xpack.siem.components.mlPopup.jobsTable.jobNameColumn',
+ {
+ defaultMessage: 'Job name',
+ }
+);
+
+export const COLUMN_GROUPS = i18n.translate('xpack.siem.components.mlPopup.jobsTable.tagsColumn', {
+ defaultMessage: 'Groups',
+});
+
+export const COLUMN_RUN_JOB = i18n.translate(
+ 'xpack.siem.components.mlPopup.jobsTable.runJobColumn',
+ {
+ defaultMessage: 'Run job',
+ }
+);
+
+export const NO_ITEMS_TEXT = i18n.translate(
+ 'xpack.siem.components.mlPopup.jobsTable.noItemsDescription',
+ {
+ defaultMessage: 'No SIEM Machine Learning jobs found',
+ }
+);
+
+export const CREATE_CUSTOM_JOB = i18n.translate(
+ 'xpack.siem.components.mlPopup.jobsTable.createCustomJobButtonLabel',
+ {
+ defaultMessage: 'Create custom job',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_modules.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_modules.tsx
new file mode 100644
index 0000000000000..99d805b8e9234
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_modules.tsx
@@ -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.
+ */
+
+/**
+ * ML Modules relevant to the SIEM App that should be used to display jobs within the Anomaly
+ * Detection UI. Added as part of: https://github.com/elastic/kibana/pull/39678/files
+ *
+ */
+export const mlModules: string[] = ['siem_auditbeat', 'siem_winlogbeat'];
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx
index 6815c23ebaa46..2c3bc469a51df 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx
@@ -4,53 +4,42 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiButton, EuiPopover, EuiPopoverTitle, EuiSpacer } from '@elastic/eui';
-import React, { useContext, useEffect, useReducer, useState } from 'react';
+import { EuiButton, EuiCallOut, EuiPopover, EuiPopoverTitle, EuiSpacer } from '@elastic/eui';
+import React, { useContext, useReducer, useState } from 'react';
import styled from 'styled-components';
import moment from 'moment';
-
-import { useJobSummaryData } from './hooks/use_job_summary_data';
import * as i18n from './translations';
-import { Job } from './types';
+import { JobsFilters, JobSummary, SiemJob } from './types';
import { hasMlAdminPermissions } from '../ml/permissions/has_ml_admin_permissions';
import { MlCapabilitiesContext } from '../ml/permissions/ml_capabilities_provider';
import { JobsTable } from './jobs_table/jobs_table';
import { setupMlJob, startDatafeeds, stopDatafeeds } from './api';
-import { useIndexPatterns } from './hooks/use_index_patterns';
import { UpgradeContents } from './upgrade_contents';
-import { FilterGroup } from './jobs_table/filter_group';
+import { JobsTableFilters } from './jobs_table/filters/jobs_table_filters';
import { ShowingCount } from './jobs_table/showing_count';
import { PopoverDescription } from './popover_description';
-import {
- getConfigTemplatesToInstall,
- getIndexPatternTitles,
- getJobsToDisplay,
- getJobsToInstall,
- getStablePatternTitles,
-} from './helpers';
-import { configTemplates } from './config_templates';
import { useStateToaster } from '../toasters';
import { errorToToaster } from '../ml/api/error_to_toaster';
-import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
-import { DEFAULT_KBN_VERSION } from '../../../common/constants';
import { METRIC_TYPE, TELEMETRY_EVENT, trackUiAction as track } from '../../lib/track_usage';
+import { useSiemJobs } from './hooks/use_siem_jobs';
+import { filterJobs } from './helpers';
const PopoverContentsDiv = styled.div`
- max-width: 550px;
+ max-width: 684px;
`;
PopoverContentsDiv.displayName = 'PopoverContentsDiv';
interface State {
isLoading: boolean;
- jobs: Job[];
+ jobs: JobSummary[];
refreshToggle: boolean;
}
type Action =
| { type: 'refresh' }
| { type: 'loading' }
- | { type: 'success'; results: Job[] }
+ | { type: 'success'; results: JobSummary[] }
| { type: 'failure' };
function mlPopoverReducer(state: State, action: Action): State {
@@ -92,26 +81,40 @@ const initialState: State = {
refreshToggle: true,
};
+const defaultFilterProps: JobsFilters = {
+ filterQuery: '',
+ showCustomJobs: false,
+ showElasticJobs: false,
+ selectedGroups: [],
+};
+
export const MlPopover = React.memo(() => {
const [{ refreshToggle }, dispatch] = useReducer(mlPopoverReducer, initialState);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
- const [showCustomJobs, setShowCustomJobs] = useState(false);
- const [showElasticJobs, setShowElasticJobs] = useState(false);
- const [isLoadingJobSummaryData, jobSummaryData] = useJobSummaryData([], refreshToggle);
- const [isCreatingJobs, setIsCreatingJobs] = useState(false);
- const [filterQuery, setFilterQuery] = useState('');
+ const [filterProperties, setFilterProperties] = useState(defaultFilterProps);
+ const [isLoadingSiemJobs, siemJobs] = useSiemJobs(refreshToggle);
const [, dispatchToaster] = useStateToaster();
- const [, configuredIndexPatterns] = useIndexPatterns(refreshToggle);
const capabilities = useContext(MlCapabilitiesContext);
- const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
- const headers = { 'kbn-version': kbnVersion };
-
- const configuredIndexPatternTitles = getIndexPatternTitles(configuredIndexPatterns);
// Enable/Disable Job & Datafeed -- passed to JobsTable for use as callback on JobSwitch
- const enableDatafeed = async (jobName: string, latestTimestampMs: number, enable: boolean) => {
- submitTelemetry(jobName, enable, embeddedJobIds);
+ const enableDatafeed = async (job: SiemJob, latestTimestampMs: number, enable: boolean) => {
+ submitTelemetry(job, enable);
+
+ if (!job.isInstalled) {
+ try {
+ await setupMlJob({
+ configTemplate: job.moduleId,
+ indexPatternName: job.defaultIndexPattern,
+ jobIdErrorFilter: [job.id],
+ groups: job.groups,
+ });
+ } catch (error) {
+ errorToToaster({ title: i18n.CREATE_JOB_FAILURE, error, dispatchToaster });
+ dispatch({ type: 'refresh' });
+ return;
+ }
+ }
// Max start time for job is no more than two weeks ago to ensure job performance
const maxStartTime = moment
@@ -122,14 +125,14 @@ export const MlPopover = React.memo(() => {
if (enable) {
const startTime = Math.max(latestTimestampMs, maxStartTime);
try {
- await startDatafeeds([`datafeed-${jobName}`], headers, startTime);
+ await startDatafeeds({ datafeedIds: [`datafeed-${job.id}`], start: startTime });
} catch (error) {
track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_ENABLE_FAILURE);
errorToToaster({ title: i18n.START_JOB_FAILURE, error, dispatchToaster });
}
} else {
try {
- await stopDatafeeds([`datafeed-${jobName}`], headers);
+ await stopDatafeeds({ datafeedIds: [`datafeed-${job.id}`] });
} catch (error) {
track(METRIC_TYPE.COUNT, TELEMETRY_EVENT.JOB_DISABLE_FAILURE);
errorToToaster({ title: i18n.STOP_JOB_FAILURE, error, dispatchToaster });
@@ -138,60 +141,12 @@ export const MlPopover = React.memo(() => {
dispatch({ type: 'refresh' });
};
- // All jobs from embedded configTemplates that should be installed
- const embeddedJobIds = getJobsToInstall(configTemplates);
+ const filteredJobs = filterJobs({
+ jobs: siemJobs,
+ ...filterProperties,
+ });
- // Jobs currently installed retrieved via ml jobs_summary api for 'siem' group
- const siemGroupJobIds = jobSummaryData != null ? jobSummaryData.map(job => job.id) : [];
- const installedJobIds = embeddedJobIds.filter(job => siemGroupJobIds.includes(job));
-
- // Config templates that still need to be installed and have a defaultIndexPattern that is configured
- const configTemplatesToInstall = getConfigTemplatesToInstall(
- configTemplates,
- installedJobIds,
- configuredIndexPatternTitles || []
- );
-
- // Filter installed job to show all 'siem' group jobs or just embedded
- const jobsToDisplay = getJobsToDisplay(
- jobSummaryData,
- embeddedJobIds,
- showCustomJobs,
- showElasticJobs,
- filterQuery
- );
-
- // Install Config Templates as effect of opening popover
- useEffect(() => {
- if (
- isPopoverOpen &&
- jobSummaryData != null &&
- configuredIndexPatternTitles.length > 0 &&
- configTemplatesToInstall.length > 0
- ) {
- const setupJobs = async () => {
- setIsCreatingJobs(true);
- try {
- await Promise.all(
- configTemplatesToInstall.map(configTemplate => {
- return setupMlJob({
- configTemplate: configTemplate.name,
- indexPatternName: configTemplate.defaultIndexPattern,
- groups: ['siem'],
- headers,
- });
- })
- );
- setIsCreatingJobs(false);
- dispatch({ type: 'refresh' });
- } catch (error) {
- errorToToaster({ title: i18n.CREATE_JOB_FAILURE, error, dispatchToaster });
- setIsCreatingJobs(false);
- }
- };
- setupJobs();
- }
- }, [jobSummaryData, getStablePatternTitles(configuredIndexPatternTitles)]);
+ const incompatibleJobCount = siemJobs.filter(j => !j.isCompatible).length;
if (!capabilities.isPlatinumOrTrialLicense) {
// If the user does not have platinum show upgrade UI
@@ -243,21 +198,30 @@ export const MlPopover = React.memo(() => {
-
+
-
+
-
+
+
+ {incompatibleJobCount > 0 && (
+ <>
+
+ {i18n.MODULE_NOT_COMPATIBLE_DESCRIPTION}
+
+
+
+ >
+ )}
@@ -269,11 +233,11 @@ export const MlPopover = React.memo(() => {
}
});
-const submitTelemetry = (jobName: string, enabled: boolean, embeddedJobIds: string[]) => {
+const submitTelemetry = (job: SiemJob, enabled: boolean) => {
// Report type of job enabled/disabled
track(
METRIC_TYPE.COUNT,
- embeddedJobIds.includes(jobName)
+ job.isElasticJob
? enabled
? TELEMETRY_EVENT.SIEM_JOB_ENABLED
: TELEMETRY_EVENT.SIEM_JOB_DISABLED
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx
index 0c91773c48556..8f90877feb72f 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx
@@ -9,7 +9,7 @@ import toJson from 'enzyme-to-json';
import * as React from 'react';
import { PopoverDescription } from './popover_description';
-describe('FilterGroup', () => {
+describe('JobsTableFilters', () => {
test('renders correctly against snapshot', () => {
const wrapper = shallow( );
expect(toJson(wrapper)).toMatchSnapshot();
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/translations.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/translations.ts
index 0ea9960edc16a..3fc9f3a484b89 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/translations.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/translations.ts
@@ -32,46 +32,18 @@ export const LICENSE_BUTTON = i18n.translate('xpack.siem.components.mlPopup.lice
defaultMessage: 'Manage license',
});
-export const FILTER_PLACEHOLDER = i18n.translate(
- 'xpack.siem.components.mlPopup.filterPlaceholder',
+export const MODULE_NOT_COMPATIBLE_TITLE = (incompatibleJobCount: number) =>
+ i18n.translate('xpack.siem.components.mlPopup.moduleNotCompatibleTitle', {
+ values: { incompatibleJobCount },
+ defaultMessage:
+ '{incompatibleJobCount} {incompatibleJobCount, plural, =1 {job} other {jobs}} are currently unavailable',
+ });
+
+export const MODULE_NOT_COMPATIBLE_DESCRIPTION = i18n.translate(
+ 'xpack.siem.components.mlPopup.moduleNotCompatibleDescription',
{
- defaultMessage: 'e.g. rare_process_linux',
- }
-);
-
-export const SHOW_ELASTIC_JOBS = i18n.translate('xpack.siem.components.mlPopup.showAllJobsLabel', {
- defaultMessage: 'Elastic jobs',
-});
-
-export const SHOW_CUSTOM_JOBS = i18n.translate('xpack.siem.components.mlPopup.showSiemJobsLabel', {
- defaultMessage: 'Custom jobs',
-});
-
-export const COLUMN_JOB_NAME = i18n.translate(
- 'xpack.siem.components.mlPopup.jobsTable.jobNameColumn',
- {
- defaultMessage: 'Job name',
- }
-);
-
-export const COLUMN_RUN_JOB = i18n.translate(
- 'xpack.siem.components.mlPopup.jobsTable.runJobColumn',
- {
- defaultMessage: 'Run job',
- }
-);
-
-export const NO_ITEMS_TEXT = i18n.translate(
- 'xpack.siem.components.mlPopup.jobsTable.noItemsDescription',
- {
- defaultMessage: 'No SIEM Machine Learning jobs found',
- }
-);
-
-export const CREATE_CUSTOM_JOB = i18n.translate(
- 'xpack.siem.components.mlPopup.jobsTable.createCustomJobButtonLabel',
- {
- defaultMessage: 'Create custom job',
+ defaultMessage:
+ 'You may be missing the required index patterns. Learn more in our documentation.',
}
);
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts
index 5e8f6aa40b5a9..203f4f646b62b 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts
@@ -12,26 +12,104 @@ export interface Group {
calendarIds: string[];
}
+export interface CheckRecognizerProps {
+ indexPatternName: string;
+ headers?: Record;
+ signal: AbortSignal;
+}
+
+export interface RecognizerModule {
+ id: string;
+ title: string;
+ query: Record;
+ description: string;
+ logo: {
+ icon: string;
+ };
+}
+
+export interface GetModulesProps {
+ moduleId?: string;
+ headers?: Record;
+ signal: AbortSignal;
+}
+
+export interface Module {
+ id: string;
+ title: string;
+ description: string;
+ type: string;
+ logoFile: string;
+ defaultIndexPattern: string;
+ query: Record;
+ jobs: ModuleJob[];
+ datafeeds: ModuleDatafeed[];
+ kibana: object;
+}
+
+/**
+ * Representation of an ML Job as returned from `the ml/modules/get_module` API
+ */
+export interface ModuleJob {
+ id: string;
+ config: {
+ groups: string[];
+ description: string;
+ analysis_config: {
+ bucket_span: string;
+ summary_count_field_name?: string;
+ detectors: Detector[];
+ influencers: string[];
+ };
+ analysis_limits: {
+ model_memory_limit: string;
+ };
+ data_description: {
+ time_field: string;
+ time_format?: string;
+ };
+ model_plot_config?: {
+ enabled: boolean;
+ };
+ custom_settings: {
+ created_by: string;
+ custom_urls: CustomURL[];
+ };
+ job_type: string;
+ };
+}
+
+// TODO: Speak to ML team about why the get_module API will sometimes return indexes and other times indices
+// See mockGetModuleResponse for examples
+export interface ModuleDatafeed {
+ id: string;
+ config: {
+ job_id: string;
+ indexes?: string[];
+ indices?: string[];
+ query: Record;
+ };
+}
+
export interface MlSetupArgs {
configTemplate: string;
indexPatternName: string;
+ jobIdErrorFilter: string[];
groups: string[];
prefix?: string;
- headers: Record;
+ headers?: Record;
}
-export interface ConfigTemplate {
- name: string;
- defaultIndexPattern: string;
- jobs: string[];
-}
-
-export interface Job {
+/**
+ * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API
+ */
+export interface JobSummary {
datafeedId: string;
datafeedIndices: string[];
datafeedState: string;
description: string;
earliestTimestampMs?: number;
+ latestResultsTimestampMs?: number;
groups: string[];
hasDatafeed: boolean;
id: string;
@@ -43,6 +121,37 @@ export interface Job {
processed_record_count: number;
}
+export interface Detector {
+ detector_description: string;
+ function: string;
+ by_field_name: string;
+ partition_field_name?: string;
+}
+
+export interface CustomURL {
+ url_name: string;
+ url_value: string;
+}
+
+/**
+ * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary
+ * that includes necessary metadata like moduleName, defaultIndexPattern, etc.
+ */
+export interface SiemJob extends JobSummary {
+ moduleId: string;
+ defaultIndexPattern: string;
+ isCompatible: boolean;
+ isInstalled: boolean;
+ isElasticJob: boolean;
+}
+
+export interface AugmentedSiemJobFields {
+ moduleId: string;
+ defaultIndexPattern: string;
+ isCompatible: boolean;
+ isElasticJob: boolean;
+}
+
export interface SetupMlResponseJob {
id: string;
success: boolean;
@@ -65,9 +174,16 @@ export interface SetupMlResponse {
export interface StartDatafeedResponse {
[key: string]: {
started: boolean;
+ error?: string;
};
}
+export interface ErrorResponse {
+ statusCode?: number;
+ error?: string;
+ message?: string;
+}
+
export interface StopDatafeedResponse {
[key: string]: {
stopped: boolean;
@@ -90,9 +206,9 @@ export interface IndexPatternSavedObject {
version: string;
}
-export interface IndexPatternResponse {
- page: number;
- per_page: number;
- saved_objects: IndexPatternSavedObject[];
- total: number;
+export interface JobsFilters {
+ filterQuery: string;
+ showCustomJobs: boolean;
+ showElasticJobs: boolean;
+ selectedGroups: string[];
}
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx
index c866b0fd190f4..13d48c0e62b6d 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx
@@ -9,7 +9,7 @@ import toJson from 'enzyme-to-json';
import * as React from 'react';
import { UpgradeContents } from './upgrade_contents';
-describe('FilterGroup', () => {
+describe('JobsTableFilters', () => {
test('renders correctly against snapshot', () => {
const wrapper = shallow( );
expect(toJson(wrapper)).toMatchSnapshot();
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/__snapshots__/index.test.tsx.snap
deleted file mode 100644
index 7c9dfb87400a2..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/__snapshots__/index.test.tsx.snap
+++ /dev/null
@@ -1,162 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Domains Table Component Rendering it renders the default Domains table 1`] = `
-
-`;
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/columns.tsx
deleted file mode 100644
index cf5da3fbebba6..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/columns.tsx
+++ /dev/null
@@ -1,200 +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 { EuiIcon, EuiToolTip } from '@elastic/eui';
-import numeral from '@elastic/numeral';
-import { getOr, isEmpty } from 'lodash/fp';
-import moment from 'moment';
-import React from 'react';
-import { StaticIndexPattern } from 'ui/index_patterns';
-
-import {
- DomainsEdges,
- DomainsItem,
- DomainsNetworkField,
- FlowDirection,
- FlowTarget,
-} from '../../../../graphql/types';
-import { assertUnreachable } from '../../../../lib/helpers';
-import { escapeQueryValue } from '../../../../lib/keury';
-import { networkModel } from '../../../../store';
-import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper';
-import { escapeDataProviderId } from '../../../drag_and_drop/helpers';
-import { defaultToEmptyTag, getEmptyTagValue } from '../../../empty_value';
-import { PreferenceFormattedDate } from '../../../formatted_date';
-import { Columns } from '../../../paginated_table';
-import { LocalizedDateTooltip } from '../../../localized_date_tooltip';
-import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider';
-import { PreferenceFormattedBytes } from '../../../formatted_bytes';
-import { Provider } from '../../../timeline/data_providers/provider';
-import { AddToKql } from '../../add_to_kql';
-
-import * as i18n from './translations';
-
-export type DomainsColumns = [
- Columns,
- Columns,
- Columns,
- Columns,
- Columns,
- Columns
-];
-
-export const getDomainsColumns = (
- indexPattern: StaticIndexPattern,
- ip: string,
- flowDirection: FlowDirection,
- flowTarget: FlowTarget,
- type: networkModel.NetworkType,
- tableId: string
-): DomainsColumns => [
- {
- field: `node.${flowTarget}.domainName`,
- name: i18n.DOMAIN_NAME,
- truncateText: false,
- hideForMobile: false,
- sortable: true,
- render: domainName => {
- const domainNameAttr = `${flowTarget}.domain`;
- if (domainName != null) {
- const id = escapeDataProviderId(
- `${tableId}-table-${flowTarget}-${flowDirection}-domain-${domainName}`
- );
- return (
-
- snapshot.isDragging ? (
-
-
-
- ) : (
- <>{domainName}>
- )
- }
- />
- );
- } else {
- return getEmptyTagValue();
- }
- },
- },
- {
- field: 'node.network.direction',
- name: i18n.DIRECTION,
- truncateText: false,
- hideForMobile: false,
- render: directions =>
- isEmpty(directions)
- ? getEmptyTagValue()
- : directions &&
- directions.map((direction, index) => (
-
- <>
- {defaultToEmptyTag(direction)}
- {index < directions.length - 1 ? '\u00A0' : null}
- >
-
- )),
- },
- {
- field: 'node.network.bytes',
- name: i18n.BYTES,
- truncateText: false,
- hideForMobile: false,
- sortable: true,
- render: bytes => {
- if (bytes != null) {
- return ;
- } else {
- return getEmptyTagValue();
- }
- },
- },
- {
- field: 'node.network.packets',
- name: i18n.PACKETS,
- truncateText: false,
- hideForMobile: false,
- sortable: true,
- render: packets => {
- if (packets != null) {
- return numeral(packets).format('0,000');
- } else {
- return getEmptyTagValue();
- }
- },
- },
- {
- field: `node.${flowTarget}.uniqueIpCount`,
- name: getFlowTargetTitle(flowTarget),
- truncateText: false,
- hideForMobile: false,
- sortable: true,
- render: uniqueIpCount => {
- if (uniqueIpCount != null) {
- return numeral(uniqueIpCount).format('0,000');
- } else {
- return getEmptyTagValue();
- }
- },
- },
- {
- name: (
-
- <>
- {i18n.LAST_SEEN}{' '}
-
- >
-
- ),
- truncateText: false,
- hideForMobile: false,
- render: ({ node }) => {
- const lastSeenAttr = `${flowTarget}.lastSeen`;
- const lastSeen = getOr(null, lastSeenAttr, node);
- if (lastSeen != null) {
- return (
-
-
-
- );
- }
- return getEmptyTagValue();
- },
- },
-];
-
-const getFlowTargetTitle = (flowTarget: FlowTarget): string => {
- switch (flowTarget) {
- case FlowTarget.client:
- return i18n.UNIQUE_CLIENTS;
- case FlowTarget.server:
- return i18n.UNIQUE_SERVERS;
- case FlowTarget.source:
- return i18n.UNIQUE_DESTINATIONS;
- case FlowTarget.destination:
- return i18n.UNIQUE_SOURCES;
- }
- assertUnreachable(flowTarget);
-};
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/index.test.tsx
deleted file mode 100644
index 45b5998f4d45c..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/index.test.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { mount, shallow } from 'enzyme';
-import toJson from 'enzyme-to-json';
-import { getOr } from 'lodash/fp';
-import * as React from 'react';
-import { MockedProvider } from 'react-apollo/test-utils';
-import { Provider as ReduxStoreProvider } from 'react-redux';
-
-import { FlowTarget } from '../../../../graphql/types';
-import {
- apolloClientObservable,
- mockIndexPattern,
- mockGlobalState,
- TestProviders,
-} from '../../../../mock';
-import { createStore, networkModel, State } from '../../../../store';
-
-import { DomainsTable } from '.';
-import { mockDomainsData } from './mock';
-
-jest.mock('../../../../lib/settings/use_kibana_ui_setting');
-
-describe('Domains Table Component', () => {
- const loadPage = jest.fn();
- const ip = '10.10.10.10';
- const state: State = mockGlobalState;
-
- let store = createStore(state, apolloClientObservable);
-
- beforeEach(() => {
- store = createStore(state, apolloClientObservable);
- });
-
- describe('Rendering', () => {
- test('it renders the default Domains table', () => {
- const wrapper = shallow(
-
-
-
- );
-
- expect(toJson(wrapper)).toMatchSnapshot();
- });
- });
-
- describe('Sorting on Table', () => {
- test('when you click on the column header, you should show the sorting icon', () => {
- const wrapper = mount(
-
-
-
-
-
- );
- expect(store.getState().network.details.queries!.domains.domainsSortField).toEqual({
- direction: 'desc',
- field: 'bytes',
- });
-
- wrapper
- .find('.euiTable thead tr th button')
- .at(1)
- .simulate('click');
-
- wrapper.update();
-
- expect(store.getState().network.details.queries!.domains.domainsSortField).toEqual({
- direction: 'asc',
- field: 'bytes',
- });
- expect(
- wrapper
- .find('.euiTable thead tr th button')
- .first()
- .text()
- ).toEqual('DomainClick to sort in ascending order');
- expect(
- wrapper
- .find('.euiTable thead tr th button')
- .at(1)
- .text()
- ).toEqual('BytesClick to sort in descending order');
- expect(
- wrapper
- .find('.euiTable thead tr th button')
- .at(1)
- .find('svg')
- ).toBeTruthy();
- });
- });
-});
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/index.tsx
deleted file mode 100644
index 52cd8ec70f326..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/index.tsx
+++ /dev/null
@@ -1,235 +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 { isEqual } from 'lodash/fp';
-import React from 'react';
-import { connect } from 'react-redux';
-import { ActionCreator } from 'redux';
-import { StaticIndexPattern } from 'ui/index_patterns';
-
-import { networkActions } from '../../../../store/actions';
-import {
- Direction,
- DomainsEdges,
- DomainsFields,
- DomainsSortField,
- FlowDirection,
- FlowTarget,
-} from '../../../../graphql/types';
-import { networkModel, networkSelectors, State } from '../../../../store';
-import { FlowDirectionSelect } from '../../../flow_controls/flow_direction_select';
-import { Criteria, ItemsPerRow, PaginatedTable, SortingBasicTable } from '../../../paginated_table';
-
-import { getDomainsColumns } from './columns';
-import * as i18n from './translations';
-const tableType = networkModel.IpDetailsTableType.domains;
-
-interface OwnProps {
- data: DomainsEdges[];
- flowTarget: FlowTarget;
- fakeTotalCount: number;
- id: string;
- isInspect: boolean;
- indexPattern: StaticIndexPattern;
- ip: string;
- loading: boolean;
- loadPage: (newActivePage: number) => void;
- showMorePagesIndicator: boolean;
- totalCount: number;
- type: networkModel.NetworkType;
-}
-
-interface DomainsTableReduxProps {
- activePage: number;
- domainsSortField: DomainsSortField;
- flowDirection: FlowDirection;
- limit: number;
-}
-
-interface DomainsTableDispatchProps {
- updateDomainsDirection: ActionCreator<{
- flowDirection: FlowDirection;
- networkType: networkModel.NetworkType;
- }>;
- updateDomainsLimit: ActionCreator<{
- limit: number;
- networkType: networkModel.NetworkType;
- }>;
- updateDomainsSort: ActionCreator<{
- domainsSort: DomainsSortField;
- networkType: networkModel.NetworkType;
- }>;
- updateTableActivePage: ActionCreator<{
- activePage: number;
- tableType: networkModel.IpDetailsTableType;
- }>;
-}
-
-type DomainsTableProps = OwnProps & DomainsTableReduxProps & DomainsTableDispatchProps;
-
-const rowItems: ItemsPerRow[] = [
- {
- text: i18n.ROWS_5,
- numberOfRow: 5,
- },
- {
- text: i18n.ROWS_10,
- numberOfRow: 10,
- },
-];
-
-export const DomainsTableId = 'domains-table';
-
-class DomainsTableComponent extends React.PureComponent {
- public render() {
- const {
- activePage,
- data,
- domainsSortField,
- fakeTotalCount,
- flowDirection,
- flowTarget,
- id,
- indexPattern,
- ip,
- isInspect,
- limit,
- loading,
- loadPage,
- showMorePagesIndicator,
- totalCount,
- type,
- updateDomainsLimit,
- updateTableActivePage,
- } = this.props;
-
- return (
-
- }
- headerTitle={i18n.DOMAINS}
- headerUnit={i18n.UNIT(totalCount)}
- id={id}
- isInspect={isInspect}
- itemsPerRow={rowItems}
- limit={limit}
- loading={loading}
- loadPage={newActivePage => loadPage(newActivePage)}
- onChange={this.onChange}
- pageOfItems={data}
- sorting={getSortField(domainsSortField, flowTarget)}
- totalCount={fakeTotalCount}
- updateActivePage={newPage =>
- updateTableActivePage({
- activePage: newPage,
- tableType,
- })
- }
- updateLimitPagination={newLimit =>
- updateDomainsLimit({ limit: newLimit, networkType: type })
- }
- />
- );
- }
-
- private onChange = (criteria: Criteria) => {
- if (criteria.sort != null) {
- const splitField = criteria.sort.field.split('.');
- const newDomainsSort: DomainsSortField = {
- field: getSortFromString(splitField[splitField.length - 1]),
- direction: criteria.sort.direction,
- };
- if (!isEqual(newDomainsSort, this.props.domainsSortField)) {
- this.props.updateDomainsSort({
- domainsSortField: newDomainsSort,
- networkType: this.props.type,
- });
- }
- }
- };
-
- private onChangeDomainsDirection = (flowDirection: FlowDirection) =>
- this.props.updateDomainsDirection({ flowDirection, networkType: this.props.type });
-}
-
-const makeMapStateToProps = () => {
- const getDomainsSelector = networkSelectors.domainsSelector();
- const mapStateToProps = (state: State) => ({
- ...getDomainsSelector(state),
- });
- return mapStateToProps;
-};
-
-export const DomainsTable = connect(
- makeMapStateToProps,
- {
- updateDomainsLimit: networkActions.updateDomainsLimit,
- updateDomainsDirection: networkActions.updateDomainsFlowDirection,
- updateDomainsSort: networkActions.updateDomainsSort,
- updateTableActivePage: networkActions.updateIpDetailsTableActivePage,
- }
-)(DomainsTableComponent);
-
-const getSortField = (sortField: DomainsSortField, flowTarget: FlowTarget): SortingBasicTable => {
- switch (sortField.field) {
- case DomainsFields.domainName:
- return {
- field: `node.${flowTarget}.${sortField.field}`,
- direction: sortField.direction,
- };
- case DomainsFields.bytes:
- return {
- field: `node.network.${sortField.field}`,
- direction: sortField.direction,
- };
- case DomainsFields.packets:
- return {
- field: `node.network.${sortField.field}`,
- direction: sortField.direction,
- };
- case DomainsFields.uniqueIpCount:
- return {
- field: `node.${flowTarget}.${sortField.field}`,
- direction: sortField.direction,
- };
- default:
- return {
- field: 'node.network.bytes',
- direction: Direction.desc,
- };
- }
-};
-
-const getSortFromString = (sortField: string): DomainsFields => {
- switch (sortField) {
- case DomainsFields.domainName.valueOf():
- return DomainsFields.domainName;
- case DomainsFields.bytes.valueOf():
- return DomainsFields.bytes;
- case DomainsFields.packets.valueOf():
- return DomainsFields.packets;
- case DomainsFields.uniqueIpCount.valueOf():
- return DomainsFields.uniqueIpCount;
- default:
- return DomainsFields.bytes;
- }
-};
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/mock.ts b/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/mock.ts
deleted file mode 100644
index 624db09f03943..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/mock.ts
+++ /dev/null
@@ -1,75 +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 { DomainsData } from '../../../../graphql/types';
-
-export const mockDomainsData: DomainsData = {
- totalCount: 2,
- edges: [
- {
- node: {
- source: null,
- destination: {
- uniqueIpCount: 96,
- domainName: 'samsungtv-kitchen.iot.sr.local.example.com',
- firstSeen: null,
- lastSeen: null,
- },
- network: {
- bytes: 1054651765,
- direction: [],
- packets: 707990,
- },
- },
- cursor: {
- value: 'samsungtv-kitchen.iot.sr.local.example.com',
- },
- },
- {
- node: {
- source: null,
- destination: {
- uniqueIpCount: 6,
- domainName: '.row.exmaple.com',
- firstSeen: null,
- lastSeen: null,
- },
- network: {
- bytes: 0,
- direction: [],
- packets: 0,
- },
- },
- cursor: {
- value: 'row.exmaple.com',
- },
- },
- {
- node: {
- source: null,
- destination: {
- uniqueIpCount: 1,
- domainName: '10.10.10.10',
- firstSeen: null,
- lastSeen: null,
- },
- network: {
- bytes: 0,
- direction: [],
- packets: 0,
- },
- },
- cursor: {
- value: '10.10.10.10',
- },
- },
- ],
- pageInfo: {
- activePage: 1,
- fakeTotalCount: 50,
- showMorePagesIndicator: true,
- },
-};
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/translations.ts b/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/translations.ts
deleted file mode 100644
index b702eb0578337..0000000000000
--- a/x-pack/legacy/plugins/siem/public/components/page/network/domains_table/translations.ts
+++ /dev/null
@@ -1,99 +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 { i18n } from '@kbn/i18n';
-
-export const DOMAINS = i18n.translate('xpack.siem.network.ipDetails.domainsTable.domainsTitle', {
- defaultMessage: 'Domains',
-});
-
-export const UNIT = (totalCount: number) =>
- i18n.translate('xpack.siem.network.ipDetails.domainsTable.unit', {
- values: { totalCount },
- defaultMessage: `{totalCount, plural, =1 {domain} other {domains}}`,
- });
-
-// Columns
-export const DOMAIN_NAME = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.domainNameTitle',
- {
- defaultMessage: 'Domain',
- }
-);
-
-export const DIRECTION = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.directionTitle',
- {
- defaultMessage: 'Direction',
- }
-);
-
-export const BYTES = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.bytesTitle',
- {
- defaultMessage: 'Bytes',
- }
-);
-
-export const PACKETS = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.packetsTitle',
- {
- defaultMessage: 'Packets',
- }
-);
-
-export const UNIQUE_DESTINATIONS = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.uniqueDestinationsTitle',
- {
- defaultMessage: 'Unique destinations',
- }
-);
-
-export const UNIQUE_SOURCES = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.uniqueSourcesTitle',
- {
- defaultMessage: 'Unique sources',
- }
-);
-
-export const UNIQUE_CLIENTS = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.uniqueClientsTitle',
- {
- defaultMessage: 'Unique servers',
- }
-);
-
-export const UNIQUE_SERVERS = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.uniqueServersTitle',
- {
- defaultMessage: 'Unique clients',
- }
-);
-
-export const LAST_SEEN = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.lastSeenTitle',
- {
- defaultMessage: 'Last seen',
- }
-);
-
-export const FIRST_LAST_SEEN_TOOLTIP = i18n.translate(
- 'xpack.siem.network.ipDetails.domainsTable.columns.firstLastSeenToolTip',
- {
- defaultMessage: 'Relative to the selected date range',
- }
-);
-
-// Row Select
-export const ROWS_5 = i18n.translate('xpack.siem.network.ipDetails.domainsTable.rows', {
- values: { numRows: 5 },
- defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}',
-});
-
-export const ROWS_10 = i18n.translate('xpack.siem.network.ipDetails.domainsTable.rows', {
- values: { numRows: 10 },
- defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}',
-});
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap
index bcd7e6a0461a0..498c620312a3a 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap
@@ -1,6 +1,200 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`NetworkTopNFlow Table Component rendering it renders the default NetworkTopNFlow table 1`] = `
+exports[`NetworkTopNFlow Table Component rendering it renders the default NetworkTopNFlow table on the IP Details page 1`] = `
+
+`;
+
+exports[`NetworkTopNFlow Table Component rendering it renders the default NetworkTopNFlow table on the Network page 1`] = `
];
+export type NetworkTopNFlowColumnsIpDetails = [
+ Columns,
+ Columns,
+ Columns,
+ Columns,
+ Columns,
+ Columns
+];
+
export const getNetworkTopNFlowColumns = (
indexPattern: StaticIndexPattern,
- flowTarget: FlowTargetNew,
+ flowTarget: FlowTargetSourceDest,
type: networkModel.NetworkType,
tableId: string
): NetworkTopNFlowColumns => [
@@ -211,7 +220,7 @@ export const getNetworkTopNFlowColumns = (
{
align: 'right',
field: `node.${flowTarget}.${getOppositeField(flowTarget)}_ips`,
- name: flowTarget === FlowTargetNew.source ? i18n.DESTINATION_IPS : i18n.SOURCE_IPS,
+ name: flowTarget === FlowTargetSourceDest.source ? i18n.DESTINATION_IPS : i18n.SOURCE_IPS,
sortable: true,
render: ips => {
if (ips != null) {
@@ -223,5 +232,24 @@ export const getNetworkTopNFlowColumns = (
},
];
-const getOppositeField = (flowTarget: FlowTargetNew): FlowTargetNew =>
- flowTarget === FlowTargetNew.source ? FlowTargetNew.destination : FlowTargetNew.source;
+export const getNFlowColumnsCurated = (
+ indexPattern: StaticIndexPattern,
+ flowTarget: FlowTargetSourceDest,
+ type: networkModel.NetworkType,
+ tableId: string
+): NetworkTopNFlowColumns | NetworkTopNFlowColumnsIpDetails => {
+ const columns = getNetworkTopNFlowColumns(indexPattern, flowTarget, type, tableId);
+
+ // Columns to exclude from host details pages
+ if (type === networkModel.NetworkType.details) {
+ columns.pop();
+ return columns;
+ }
+
+ return columns;
+};
+
+const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest =>
+ flowTarget === FlowTargetSourceDest.source
+ ? FlowTargetSourceDest.destination
+ : FlowTargetSourceDest.source;
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx
index d1d3a6a8c462c..aad28521b1c45 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx
@@ -11,7 +11,7 @@ import * as React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { Provider as ReduxStoreProvider } from 'react-redux';
-import { FlowTargetNew } from '../../../../graphql/types';
+import { FlowTargetSourceDest } from '../../../../graphql/types';
import {
apolloClientObservable,
mockGlobalState,
@@ -34,13 +34,13 @@ describe('NetworkTopNFlow Table Component', () => {
});
describe('rendering', () => {
- test('it renders the default NetworkTopNFlow table', () => {
+ test('it renders the default NetworkTopNFlow table on the Network page', () => {
const wrapper = shallow(
{
expect(toJson(wrapper)).toMatchSnapshot();
});
+
+ test('it renders the default NetworkTopNFlow table on the IP Details page', () => {
+ const wrapper = shallow(
+
+
+
+ );
+
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
});
describe('Sorting on Table', () => {
@@ -69,7 +95,7 @@ describe('NetworkTopNFlow Table Component', () => {
void;
@@ -46,7 +47,12 @@ interface NetworkTopNFlowTableReduxProps {
}
interface NetworkTopNFlowTableDispatchProps {
- updateTableActivePage: ActionCreator<{
+ setIpDetailsTablesActivePageToZero: ActionCreator;
+ updateIpDetailsTableActivePage: ActionCreator<{
+ activePage: number;
+ tableType: networkModel.IpDetailsTableType;
+ }>;
+ updateNetworkPageTableActivePage: ActionCreator<{
activePage: number;
tableType: networkModel.NetworkTableType;
}>;
@@ -87,18 +93,26 @@ const NetworkTopNFlowTableComponent = React.memo(
flowTargeted,
id,
indexPattern,
+ ip,
isInspect,
limit,
loading,
loadPage,
+ setIpDetailsTablesActivePageToZero,
showMorePagesIndicator,
topNFlowSort,
totalCount,
type,
+ updateIpDetailsTableActivePage,
+ updateNetworkPageTableActivePage,
updateTopNFlowLimit,
updateTopNFlowSort,
- updateTableActivePage,
}) => {
+ useEffect(() => {
+ if (ip && activePage !== 0) {
+ setIpDetailsTablesActivePageToZero(null);
+ }
+ }, [ip]);
const onChange = (criteria: Criteria, tableType: networkModel.TopNTableType) => {
if (criteria.sort != null) {
const splitField = criteria.sort.field.split('.');
@@ -120,14 +134,23 @@ const NetworkTopNFlowTableComponent = React.memo(
};
let tableType: networkModel.TopNTableType;
- let headerTitle: string;
-
- if (flowTargeted === FlowTargetNew.source) {
- headerTitle = i18n.SOURCE_IP;
- tableType = networkModel.NetworkTableType.topNFlowSource;
+ const headerTitle: string =
+ flowTargeted === FlowTargetSourceDest.source ? i18n.SOURCE_IP : i18n.DESTINATION_IP;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let updateTableActivePage: any;
+ if (type === networkModel.NetworkType.page) {
+ tableType =
+ flowTargeted === FlowTargetSourceDest.source
+ ? networkModel.NetworkTableType.topNFlowSource
+ : networkModel.NetworkTableType.topNFlowDestination;
+ updateTableActivePage = updateNetworkPageTableActivePage;
} else {
- headerTitle = i18n.DESTINATION_IP;
- tableType = networkModel.NetworkTableType.topNFlowDestination;
+ tableType =
+ flowTargeted === FlowTargetSourceDest.source
+ ? networkModel.IpDetailsTableType.topNFlowSource
+ : networkModel.IpDetailsTableType.topNFlowDestination;
+ updateTableActivePage = updateIpDetailsTableActivePage;
}
const field =
@@ -139,12 +162,7 @@ const NetworkTopNFlowTableComponent = React.memo(
return (
(
NetworkTopNFlowTableComponent.displayName = 'NetworkTopNFlowTableComponent';
const mapStateToProps = (state: State, ownProps: OwnProps) =>
- networkSelectors.topNFlowSelector(ownProps.flowTargeted);
+ networkSelectors.topNFlowSelector(ownProps.flowTargeted, ownProps.type);
export const NetworkTopNFlowTable = connect(
mapStateToProps,
{
+ setIpDetailsTablesActivePageToZero: networkActions.setIpDetailsTablesActivePageToZero,
updateTopNFlowLimit: networkActions.updateTopNFlowLimit,
updateTopNFlowSort: networkActions.updateTopNFlowSort,
- updateTableActivePage: networkActions.updateNetworkPageTableActivePage,
+ updateNetworkPageTableActivePage: networkActions.updateNetworkPageTableActivePage,
+ updateIpDetailsTableActivePage: networkActions.updateIpDetailsTableActivePage,
}
)(NetworkTopNFlowTableComponent);
diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts
index 9e7cb9e8b4b6d..9ef63bf6d3167 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts
+++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { NetworkTopNFlowData, FlowTarget } from '../../../../graphql/types';
+import { NetworkTopNFlowData, FlowTargetSourceDest } from '../../../../graphql/types';
export const mockData: { NetworkTopNFlow: NetworkTopNFlowData } = {
NetworkTopNFlow: {
@@ -30,7 +30,7 @@ export const mockData: { NetworkTopNFlow: NetworkTopNFlowData } = {
region_iso_code: ['US-CA'],
region_name: ['California'],
},
- flowTarget: FlowTarget.source,
+ flowTarget: FlowTargetSourceDest.source,
},
},
destination: null,
@@ -63,7 +63,7 @@ export const mockData: { NetworkTopNFlow: NetworkTopNFlowData } = {
region_iso_code: ['MY-10'],
region_name: ['Selangor'],
},
- flowTarget: FlowTarget.source,
+ flowTarget: FlowTargetSourceDest.source,
},
},
destination: null,
diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx
index 257ee03c944bf..1529648b7133e 100644
--- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx
@@ -21,10 +21,12 @@ import styled, { css } from 'styled-components';
import { Direction } from '../../graphql/types';
import { AuthTableColumns } from '../page/hosts/authentications_table';
-import { DomainsColumns } from '../page/network/domains_table/columns';
import { HostsTableColumns } from '../page/hosts/hosts_table';
import { NetworkDnsColumns } from '../page/network/network_dns_table/columns';
-import { NetworkTopNFlowColumns } from '../page/network/network_top_n_flow_table/columns';
+import {
+ NetworkTopNFlowColumns,
+ NetworkTopNFlowColumnsIpDetails,
+} from '../page/network/network_top_n_flow_table/columns';
import { TlsColumns } from '../page/network/tls_table/columns';
import { UncommonProcessTableColumns } from '../page/hosts/uncommon_process_table';
import { UsersColumns } from '../page/network/users_table/columns';
@@ -63,12 +65,11 @@ declare type HostsTableColumnsTest = [
declare type BasicTableColumns =
| AuthTableColumns
- | DomainsColumns
- | DomainsColumns
| HostsTableColumns
| HostsTableColumnsTest
| NetworkDnsColumns
| NetworkTopNFlowColumns
+ | NetworkTopNFlowColumnsIpDetails
| TlsColumns
| UncommonProcessTableColumns
| UsersColumns;
diff --git a/x-pack/legacy/plugins/siem/public/containers/domains/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/domains/index.gql_query.ts
deleted file mode 100644
index 8266a83bcad5e..0000000000000
--- a/x-pack/legacy/plugins/siem/public/containers/domains/index.gql_query.ts
+++ /dev/null
@@ -1,71 +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 domainsQuery = gql`
- query GetDomainsQuery(
- $sourceId: ID!
- $filterQuery: String
- $flowDirection: FlowDirection!
- $flowTarget: FlowTarget!
- $ip: String!
- $pagination: PaginationInputPaginated!
- $sort: DomainsSortField!
- $timerange: TimerangeInput!
- $defaultIndex: [String!]!
- $inspect: Boolean!
- ) {
- source(id: $sourceId) {
- id
- Domains(
- filterQuery: $filterQuery
- flowDirection: $flowDirection
- flowTarget: $flowTarget
- ip: $ip
- pagination: $pagination
- sort: $sort
- timerange: $timerange
- defaultIndex: $defaultIndex
- ) {
- totalCount
- edges {
- node {
- source {
- uniqueIpCount
- domainName
- firstSeen
- lastSeen
- }
- destination {
- uniqueIpCount
- domainName
- firstSeen
- lastSeen
- }
- network {
- bytes
- direction
- packets
- }
- }
- cursor {
- value
- }
- }
- pageInfo {
- activePage
- fakeTotalCount
- showMorePagesIndicator
- }
- inspect @include(if: $inspect) {
- dsl
- response
- }
- }
- }
- }
-`;
diff --git a/x-pack/legacy/plugins/siem/public/containers/domains/index.tsx b/x-pack/legacy/plugins/siem/public/containers/domains/index.tsx
deleted file mode 100644
index 8aca3bc2086ac..0000000000000
--- a/x-pack/legacy/plugins/siem/public/containers/domains/index.tsx
+++ /dev/null
@@ -1,175 +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 { getOr } from 'lodash/fp';
-import React from 'react';
-import { Query } from 'react-apollo';
-import { connect } from 'react-redux';
-
-import chrome from 'ui/chrome';
-import { DEFAULT_INDEX_KEY } from '../../../common/constants';
-import {
- DomainsEdges,
- DomainsSortField,
- GetDomainsQuery,
- FlowDirection,
- FlowTarget,
- PageInfoPaginated,
-} from '../../graphql/types';
-import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store';
-import { createFilter, getDefaultFetchPolicy } from '../helpers';
-import { generateTablePaginationOptions } from '../../components/paginated_table/helpers';
-import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated';
-import { domainsQuery } from './index.gql_query';
-
-const ID = 'domainsQuery';
-
-export interface DomainsArgs {
- domains: DomainsEdges[];
- id: string;
- inspect: inputsModel.InspectQuery;
- isInspected: boolean;
- loading: boolean;
- loadPage: (newActivePage: number) => void;
- pageInfo: PageInfoPaginated;
- refetch: inputsModel.Refetch;
- totalCount: number;
-}
-
-export interface OwnProps extends QueryTemplatePaginatedProps {
- children: (args: DomainsArgs) => React.ReactNode;
- flowTarget: FlowTarget;
- ip: string;
- type: networkModel.NetworkType;
-}
-
-export interface DomainsComponentReduxProps {
- activePage: number;
- domainsSortField: DomainsSortField;
- flowDirection: FlowDirection;
- isInspected: boolean;
- limit: number;
-}
-
-type DomainsProps = OwnProps & DomainsComponentReduxProps;
-
-class DomainsComponentQuery extends QueryTemplatePaginated<
- DomainsProps,
- GetDomainsQuery.Query,
- GetDomainsQuery.Variables
-> {
- public render() {
- const {
- activePage,
- children,
- domainsSortField,
- endDate,
- filterQuery,
- flowDirection,
- flowTarget,
- id = ID,
- ip,
- isInspected,
- limit,
- skip,
- sourceId,
- startDate,
- } = this.props;
- const variables: GetDomainsQuery.Variables = {
- defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY),
- filterQuery: createFilter(filterQuery),
- flowDirection,
- flowTarget,
- inspect: isInspected,
- ip,
- pagination: generateTablePaginationOptions(activePage, limit),
- sort: domainsSortField,
- sourceId,
- timerange: {
- interval: '12h',
- from: startDate!,
- to: endDate!,
- },
- };
- return (
-
- query={domainsQuery}
- fetchPolicy={getDefaultFetchPolicy()}
- notifyOnNetworkStatusChange
- skip={skip}
- variables={{
- defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY),
- filterQuery: createFilter(filterQuery),
- flowDirection,
- flowTarget,
- inspect: isInspected,
- ip,
- pagination: generateTablePaginationOptions(activePage, limit),
- sort: domainsSortField,
- sourceId,
- timerange: {
- interval: '12h',
- from: startDate!,
- to: endDate!,
- },
- }}
- >
- {({ data, loading, fetchMore, networkStatus, refetch }) => {
- const domains = getOr([], `source.Domains.edges`, data);
- this.setFetchMore(fetchMore);
- this.setFetchMoreOptions((newActivePage: number) => ({
- variables: {
- pagination: generateTablePaginationOptions(newActivePage, limit),
- },
- updateQuery: (prev, { fetchMoreResult }) => {
- if (!fetchMoreResult) {
- return prev;
- }
- return {
- ...fetchMoreResult,
- source: {
- ...fetchMoreResult.source,
- Domains: {
- ...fetchMoreResult.source.Domains,
- edges: [...fetchMoreResult.source.Domains.edges],
- },
- },
- };
- },
- }));
- const isLoading = this.isItAValidLoading(loading, variables, networkStatus);
- return children({
- domains,
- id,
- inspect: getOr(null, 'source.Domains.inspect', data),
- isInspected,
- loading: isLoading,
- loadPage: this.wrappedLoadMore,
- pageInfo: getOr({}, 'source.Domains.pageInfo', data),
- refetch: this.memoizedRefetchQuery(variables, limit, refetch),
- totalCount: getOr(-1, 'source.Domains.totalCount', data),
- });
- }}
-
- );
- }
-}
-
-const makeMapStateToProps = () => {
- const getDomainsSelector = networkSelectors.domainsSelector();
- const getQuery = inputsSelectors.globalQueryByIdSelector();
- const mapStateToProps = (state: State, { id = ID }: OwnProps) => {
- const { isInspected } = getQuery(state, id);
- return {
- ...getDomainsSelector(state),
- isInspected,
- };
- };
-
- return mapStateToProps;
-};
-
-export const DomainsQuery = connect(makeMapStateToProps)(DomainsComponentQuery);
diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts
index 3b15cea33ef31..81a94bc94652b 100644
--- a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts
@@ -9,10 +9,11 @@ import gql from 'graphql-tag';
export const networkTopNFlowQuery = gql`
query GetNetworkTopNFlowQuery(
$sourceId: ID!
+ $ip: String
$filterQuery: String
$pagination: PaginationInputPaginated!
$sort: NetworkTopNFlowSortField!
- $flowTarget: FlowTargetNew!
+ $flowTarget: FlowTargetSourceDest!
$timerange: TimerangeInput!
$defaultIndex: [String!]!
$inspect: Boolean!
@@ -22,6 +23,7 @@ export const networkTopNFlowQuery = gql`
NetworkTopNFlow(
filterQuery: $filterQuery
flowTarget: $flowTarget
+ ip: $ip
pagination: $pagination
sort: $sort
timerange: $timerange
diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx
index 6b7864d48ed08..eba9c5640fb58 100644
--- a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx
@@ -12,7 +12,7 @@ import { connect } from 'react-redux';
import chrome from 'ui/chrome';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
import {
- FlowTargetNew,
+ FlowTargetSourceDest,
GetNetworkTopNFlowQuery,
NetworkTopNFlowEdges,
NetworkTopNFlowSortField,
@@ -28,6 +28,7 @@ const ID = 'networkTopNFlowQuery';
export interface NetworkTopNFlowArgs {
id: string;
+ ip?: string;
inspect: inputsModel.InspectQuery;
isInspected: boolean;
loading: boolean;
@@ -40,7 +41,8 @@ export interface NetworkTopNFlowArgs {
export interface OwnProps extends QueryTemplatePaginatedProps {
children: (args: NetworkTopNFlowArgs) => React.ReactNode;
- flowTarget: FlowTargetNew;
+ flowTarget: FlowTargetSourceDest;
+ ip?: string;
type: networkModel.NetworkType;
}
@@ -66,6 +68,7 @@ class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated<
flowTarget,
filterQuery,
id = `${ID}-${flowTarget}`,
+ ip,
isInspected,
limit,
skip,
@@ -78,6 +81,7 @@ class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated<
filterQuery: createFilter(filterQuery),
flowTarget,
inspect: isInspected,
+ ip,
pagination: generateTablePaginationOptions(activePage, limit),
sort: topNFlowSort,
sourceId,
@@ -136,8 +140,11 @@ class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated<
}
}
-const mapStateToProps = (state: State, { flowTarget, id = `${ID}-${flowTarget}` }: OwnProps) => {
- const getNetworkTopNFlowSelector = networkSelectors.topNFlowSelector(flowTarget);
+const mapStateToProps = (
+ state: State,
+ { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps
+) => {
+ const getNetworkTopNFlowSelector = networkSelectors.topNFlowSelector(flowTarget, type);
const getQuery = inputsSelectors.globalQueryByIdSelector();
const { isInspected } = getQuery(state, id);
return {
diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json
index 9173bbe5295f7..b8bdf27bed7fa 100644
--- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json
+++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json
@@ -1193,113 +1193,6 @@
"isDeprecated": false,
"deprecationReason": null
},
- {
- "name": "Domains",
- "description": "",
- "args": [
- {
- "name": "filterQuery",
- "description": "",
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "defaultValue": null
- },
- {
- "name": "id",
- "description": "",
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "defaultValue": null
- },
- {
- "name": "ip",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "pagination",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "INPUT_OBJECT",
- "name": "PaginationInputPaginated",
- "ofType": null
- }
- },
- "defaultValue": null
- },
- {
- "name": "sort",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "INPUT_OBJECT", "name": "DomainsSortField", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "flowDirection",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "ENUM", "name": "FlowDirection", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "flowTarget",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "ENUM", "name": "FlowTarget", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "timerange",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "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": "NON_NULL",
- "name": null,
- "ofType": { "kind": "OBJECT", "name": "DomainsData", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
{
"name": "Tls",
"description": "",
@@ -1665,13 +1558,19 @@
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"defaultValue": null
},
+ {
+ "name": "ip",
+ "description": "",
+ "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "defaultValue": null
+ },
{
"name": "flowTarget",
"description": "",
"type": {
"kind": "NON_NULL",
"name": null,
- "ofType": { "kind": "ENUM", "name": "FlowTargetNew", "ofType": null }
+ "ofType": { "kind": "ENUM", "name": "FlowTargetSourceDest", "ofType": null }
},
"defaultValue": null
},
@@ -6384,7 +6283,7 @@
},
{
"kind": "INPUT_OBJECT",
- "name": "DomainsSortField",
+ "name": "TlsSortField",
"description": "",
"fields": null,
"inputFields": [
@@ -6394,7 +6293,7 @@
"type": {
"kind": "NON_NULL",
"name": null,
- "ofType": { "kind": "ENUM", "name": "DomainsFields", "ofType": null }
+ "ofType": { "kind": "ENUM", "name": "TlsFields", "ofType": null }
},
"defaultValue": null
},
@@ -6415,60 +6314,13 @@
},
{
"kind": "ENUM",
- "name": "DomainsFields",
- "description": "",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": [
- {
- "name": "domainName",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "direction",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- { "name": "bytes", "description": "", "isDeprecated": false, "deprecationReason": null },
- {
- "name": "packets",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "uniqueIpCount",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "possibleTypes": null
- },
- {
- "kind": "ENUM",
- "name": "FlowDirection",
+ "name": "TlsFields",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
- {
- "name": "uniDirectional",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "biDirectional",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- }
+ { "name": "_id", "description": "", "isDeprecated": false, "deprecationReason": null }
],
"possibleTypes": null
},
@@ -6494,7 +6346,7 @@
},
{
"kind": "OBJECT",
- "name": "DomainsData",
+ "name": "TlsData",
"description": "",
"fields": [
{
@@ -6510,7 +6362,7 @@
"ofType": {
"kind": "NON_NULL",
"name": null,
- "ofType": { "kind": "OBJECT", "name": "DomainsEdges", "ofType": null }
+ "ofType": { "kind": "OBJECT", "name": "TlsEdges", "ofType": null }
}
}
},
@@ -6557,7 +6409,7 @@
},
{
"kind": "OBJECT",
- "name": "DomainsEdges",
+ "name": "TlsEdges",
"description": "",
"fields": [
{
@@ -6567,7 +6419,7 @@
"type": {
"kind": "NON_NULL",
"name": null,
- "ofType": { "kind": "OBJECT", "name": "DomainsNode", "ofType": null }
+ "ofType": { "kind": "OBJECT", "name": "TlsNode", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
@@ -6592,7 +6444,7 @@
},
{
"kind": "OBJECT",
- "name": "DomainsNode",
+ "name": "TlsNode",
"description": "",
"fields": [
{
@@ -6612,42 +6464,82 @@
"deprecationReason": null
},
{
- "name": "source",
+ "name": "alternativeNames",
"description": "",
"args": [],
- "type": { "kind": "OBJECT", "name": "DomainsItem", "ofType": null },
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ },
"isDeprecated": false,
"deprecationReason": null
},
{
- "name": "destination",
+ "name": "notAfter",
"description": "",
"args": [],
- "type": { "kind": "OBJECT", "name": "DomainsItem", "ofType": null },
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ },
"isDeprecated": false,
"deprecationReason": null
},
{
- "name": "client",
+ "name": "commonNames",
"description": "",
"args": [],
- "type": { "kind": "OBJECT", "name": "DomainsItem", "ofType": null },
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ },
"isDeprecated": false,
"deprecationReason": null
},
{
- "name": "server",
+ "name": "ja3",
"description": "",
"args": [],
- "type": { "kind": "OBJECT", "name": "DomainsItem", "ofType": null },
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ },
"isDeprecated": false,
"deprecationReason": null
},
{
- "name": "network",
+ "name": "issuerNames",
"description": "",
"args": [],
- "type": { "kind": "OBJECT", "name": "DomainsNetworkField", "ofType": null },
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
+ }
+ },
"isDeprecated": false,
"deprecationReason": null
}
@@ -6658,419 +6550,23 @@
"possibleTypes": null
},
{
- "kind": "OBJECT",
- "name": "DomainsItem",
+ "kind": "INPUT_OBJECT",
+ "name": "UsersSortField",
"description": "",
- "fields": [
- {
- "name": "uniqueIpCount",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "domainName",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
+ "fields": null,
+ "inputFields": [
{
- "name": "firstSeen",
+ "name": "field",
"description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Date", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "ENUM", "name": "UsersFields", "ofType": null }
+ },
+ "defaultValue": null
},
{
- "name": "lastSeen",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Date", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "DomainsNetworkField",
- "description": "",
- "fields": [
- {
- "name": "bytes",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "packets",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Float", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "transport",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "direction",
- "description": "",
- "args": [],
- "type": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "ENUM", "name": "NetworkDirectionEcs", "ofType": null }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "ENUM",
- "name": "NetworkDirectionEcs",
- "description": "",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": [
- {
- "name": "inbound",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "outbound",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "internal",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "external",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "incoming",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "outgoing",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "listening",
- "description": "",
- "isDeprecated": false,
- "deprecationReason": null
- },
- { "name": "unknown", "description": "", "isDeprecated": false, "deprecationReason": null }
- ],
- "possibleTypes": null
- },
- {
- "kind": "INPUT_OBJECT",
- "name": "TlsSortField",
- "description": "",
- "fields": null,
- "inputFields": [
- {
- "name": "field",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "ENUM", "name": "TlsFields", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "direction",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null }
- },
- "defaultValue": null
- }
- ],
- "interfaces": null,
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "ENUM",
- "name": "TlsFields",
- "description": "",
- "fields": null,
- "inputFields": null,
- "interfaces": null,
- "enumValues": [
- { "name": "_id", "description": "", "isDeprecated": false, "deprecationReason": null }
- ],
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "TlsData",
- "description": "",
- "fields": [
- {
- "name": "edges",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "OBJECT", "name": "TlsEdges", "ofType": null }
- }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "totalCount",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "pageInfo",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "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": "TlsEdges",
- "description": "",
- "fields": [
- {
- "name": "node",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "OBJECT", "name": "TlsNode", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "cursor",
- "description": "",
- "args": [],
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "OBJECT",
- "name": "TlsNode",
- "description": "",
- "fields": [
- {
- "name": "_id",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "timestamp",
- "description": "",
- "args": [],
- "type": { "kind": "SCALAR", "name": "Date", "ofType": null },
- "isDeprecated": false,
- "deprecationReason": null
- },
- {
- "name": "alternativeNames",
- "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": "notAfter",
- "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": "commonNames",
- "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": "ja3",
- "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": "issuerNames",
- "description": "",
- "args": [],
- "type": {
- "kind": "LIST",
- "name": null,
- "ofType": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
- }
- },
- "isDeprecated": false,
- "deprecationReason": null
- }
- ],
- "inputFields": null,
- "interfaces": [],
- "enumValues": null,
- "possibleTypes": null
- },
- {
- "kind": "INPUT_OBJECT",
- "name": "UsersSortField",
- "description": "",
- "fields": null,
- "inputFields": [
- {
- "name": "field",
- "description": "",
- "type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": { "kind": "ENUM", "name": "UsersFields", "ofType": null }
- },
- "defaultValue": null
- },
- {
- "name": "direction",
+ "name": "direction",
"description": "",
"type": {
"kind": "NON_NULL",
@@ -7690,7 +7186,7 @@
},
{
"kind": "ENUM",
- "name": "FlowTargetNew",
+ "name": "FlowTargetSourceDest",
"description": "",
"fields": null,
"inputFields": null,
@@ -8025,7 +7521,7 @@
"name": "flowTarget",
"description": "",
"args": [],
- "type": { "kind": "ENUM", "name": "FlowTarget", "ofType": null },
+ "type": { "kind": "ENUM", "name": "FlowTargetSourceDest", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
@@ -11283,6 +10779,83 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "ENUM",
+ "name": "NetworkDirectionEcs",
+ "description": "",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "inbound",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "outbound",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "internal",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "external",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "incoming",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "outgoing",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "listening",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ { "name": "unknown", "description": "", "isDeprecated": false, "deprecationReason": null }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "FlowDirection",
+ "description": "",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "uniDirectional",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "biDirectional",
+ "description": "",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
{
"kind": "INPUT_OBJECT",
"name": "FavoriteTimelineInput",
diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts
index 2d82dcb7b30f3..7514259caa2a7 100644
--- a/x-pack/legacy/plugins/siem/public/graphql/types.ts
+++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts
@@ -67,12 +67,6 @@ export interface HostsSortField {
direction: Direction;
}
-export interface DomainsSortField {
- field: DomainsFields;
-
- direction: Direction;
-}
-
export interface TlsSortField {
field: TlsFields;
@@ -245,17 +239,8 @@ export enum HostsFields {
lastSeen = 'lastSeen',
}
-export enum DomainsFields {
- domainName = 'domainName',
- direction = 'direction',
- bytes = 'bytes',
- packets = 'packets',
- uniqueIpCount = 'uniqueIpCount',
-}
-
-export enum FlowDirection {
- uniDirectional = 'uniDirectional',
- biDirectional = 'biDirectional',
+export enum TlsFields {
+ _id = '_id',
}
export enum FlowTarget {
@@ -265,27 +250,12 @@ export enum FlowTarget {
source = 'source',
}
-export enum NetworkDirectionEcs {
- inbound = 'inbound',
- outbound = 'outbound',
- internal = 'internal',
- external = 'external',
- incoming = 'incoming',
- outgoing = 'outgoing',
- listening = 'listening',
- unknown = 'unknown',
-}
-
-export enum TlsFields {
- _id = '_id',
-}
-
export enum UsersFields {
name = 'name',
count = 'count',
}
-export enum FlowTargetNew {
+export enum FlowTargetSourceDest {
destination = 'destination',
source = 'source',
}
@@ -313,6 +283,22 @@ export enum SortFieldTimeline {
created = 'created',
}
+export enum NetworkDirectionEcs {
+ inbound = 'inbound',
+ outbound = 'outbound',
+ internal = 'internal',
+ external = 'external',
+ incoming = 'incoming',
+ outgoing = 'outgoing',
+ listening = 'listening',
+ unknown = 'unknown',
+}
+
+export enum FlowDirection {
+ uniDirectional = 'uniDirectional',
+ biDirectional = 'biDirectional',
+}
+
export type ToStringArray = string[];
export type Date = string;
@@ -431,8 +417,6 @@ export interface Source {
IpOverview?: Maybe;
- Domains: DomainsData;
-
Tls: TlsData;
Users: UsersData;
@@ -1328,58 +1312,6 @@ export interface AutonomousSystemOrganization {
name?: Maybe;
}
-export interface DomainsData {
- edges: DomainsEdges[];
-
- totalCount: number;
-
- pageInfo: PageInfoPaginated;
-
- inspect?: Maybe;
-}
-
-export interface DomainsEdges {
- node: DomainsNode;
-
- cursor: CursorType;
-}
-
-export interface DomainsNode {
- _id?: Maybe;
-
- timestamp?: Maybe;
-
- source?: Maybe;
-
- destination?: Maybe;
-
- client?: Maybe;
-
- server?: Maybe;
-
- network?: Maybe;
-}
-
-export interface DomainsItem {
- uniqueIpCount?: Maybe;
-
- domainName?: Maybe;
-
- firstSeen?: Maybe;
-
- lastSeen?: Maybe;
-}
-
-export interface DomainsNetworkField {
- bytes?: Maybe;
-
- packets?: Maybe;
-
- transport?: Maybe;
-
- direction?: Maybe;
-}
-
export interface TlsData {
edges: TlsEdges[];
@@ -1573,7 +1505,7 @@ export interface AutonomousSystemItem {
export interface GeoItem {
geo?: Maybe;
- flowTarget?: Maybe;
+ flowTarget?: Maybe;
}
export interface TopNFlowItemDestination {
@@ -2068,25 +2000,6 @@ export interface IpOverviewSourceArgs {
defaultIndex: string[];
}
-export interface DomainsSourceArgs {
- filterQuery?: Maybe;
-
- id?: Maybe;
-
- ip: string;
-
- pagination: PaginationInputPaginated;
-
- sort: DomainsSortField;
-
- flowDirection: FlowDirection;
-
- flowTarget: FlowTarget;
-
- timerange: TimerangeInput;
-
- defaultIndex: string[];
-}
export interface TlsSourceArgs {
filterQuery?: Maybe;
@@ -2153,7 +2066,9 @@ export interface NetworkTopNFlowSourceArgs {
filterQuery?: Maybe;
- flowTarget: FlowTargetNew;
+ ip?: Maybe;
+
+ flowTarget: FlowTargetSourceDest;
pagination: PaginationInputPaginated;
@@ -2396,123 +2311,6 @@ export namespace GetAuthenticationsQuery {
};
}
-export namespace GetDomainsQuery {
- export type Variables = {
- sourceId: string;
- filterQuery?: Maybe;
- flowDirection: FlowDirection;
- flowTarget: FlowTarget;
- ip: string;
- pagination: PaginationInputPaginated;
- sort: DomainsSortField;
- timerange: TimerangeInput;
- defaultIndex: string[];
- inspect: boolean;
- };
-
- export type Query = {
- __typename?: 'Query';
-
- source: Source;
- };
-
- export type Source = {
- __typename?: 'Source';
-
- id: string;
-
- Domains: Domains;
- };
-
- export type Domains = {
- __typename?: 'DomainsData';
-
- totalCount: number;
-
- edges: Edges[];
-
- pageInfo: PageInfo;
-
- inspect: Maybe;
- };
-
- export type Edges = {
- __typename?: 'DomainsEdges';
-
- node: Node;
-
- cursor: Cursor;
- };
-
- export type Node = {
- __typename?: 'DomainsNode';
-
- source: Maybe<_Source>;
-
- destination: Maybe;
-
- network: Maybe;
- };
-
- export type _Source = {
- __typename?: 'DomainsItem';
-
- uniqueIpCount: Maybe;
-
- domainName: Maybe;
-
- firstSeen: Maybe;
-
- lastSeen: Maybe;
- };
-
- export type Destination = {
- __typename?: 'DomainsItem';
-
- uniqueIpCount: Maybe;
-
- domainName: Maybe;
-
- firstSeen: Maybe;
-
- lastSeen: Maybe;
- };
-
- export type Network = {
- __typename?: 'DomainsNetworkField';
-
- bytes: Maybe;
-
- direction: Maybe;
-
- packets: Maybe;
- };
-
- export type Cursor = {
- __typename?: 'CursorType';
-
- value: Maybe;
- };
-
- export type PageInfo = {
- __typename?: 'PageInfoPaginated';
-
- activePage: number;
-
- fakeTotalCount: number;
-
- showMorePagesIndicator: boolean;
- };
-
- export type Inspect = {
- __typename?: 'Inspect';
-
- dsl: string[];
-
- response: string[];
- };
-}
-
export namespace GetEventsOverTimeQuery {
export type Variables = {
sourceId: string;
@@ -3276,10 +3074,11 @@ export namespace GetNetworkDnsQuery {
export namespace GetNetworkTopNFlowQuery {
export type Variables = {
sourceId: string;
+ ip?: Maybe;
filterQuery?: Maybe;
pagination: PaginationInputPaginated;
sort: NetworkTopNFlowSortField;
- flowTarget: FlowTargetNew;
+ flowTarget: FlowTargetSourceDest;
timerange: TimerangeInput;
defaultIndex: string[];
inspect: boolean;
@@ -3358,7 +3157,7 @@ export namespace GetNetworkTopNFlowQuery {
geo: Maybe;
- flowTarget: Maybe;
+ flowTarget: Maybe;
};
export type Geo = {
@@ -3406,7 +3205,7 @@ export namespace GetNetworkTopNFlowQuery {
geo: Maybe<_Geo>;
- flowTarget: Maybe;
+ flowTarget: Maybe;
};
export type _Geo = {
diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx
new file mode 100644
index 0000000000000..13f53cd34feb6
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IndexPatternSavedObject } from '../../types';
+
+export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [
+ {
+ type: 'index-pattern',
+ id: '2d1fe420-eeee-11e9-ad95-4b5e687c2aee',
+ attributes: {
+ title: 'filebeat-*',
+ },
+ updated_at: '2019-08-26T04:30:09.111Z',
+ version: 'WzE4LLwxXQ==',
+ },
+ {
+ type: 'index-pattern',
+ id: '5463ec70-c7ba-ffff-ad95-4b5e687c2aee',
+ attributes: {
+ title: 'auditbeat-*',
+ },
+ updated_at: '2019-08-26T04:31:12.934Z',
+ version: 'WzELLywxXQ==',
+ },
+];
diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts
new file mode 100644
index 0000000000000..95825b7d4abda
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 fetchMock from 'fetch-mock';
+import { throwIfNotOk } from './api';
+
+describe('api', () => {
+ afterEach(() => {
+ fetchMock.reset();
+ });
+
+ describe('#throwIfNotOk', () => {
+ test('does a throw if it is given response that is not ok and the body is not parsable', async () => {
+ fetchMock.mock('http://example.com', 500);
+ const response = await fetch('http://example.com');
+ await expect(throwIfNotOk(response)).rejects.toThrow('Network Error: Internal Server Error');
+ });
+
+ test('does a throw and returns a body if it is parsable', async () => {
+ fetchMock.mock('http://example.com', {
+ status: 500,
+ body: {
+ statusCode: 500,
+ message: 'I am a custom message',
+ },
+ });
+ const response = await fetch('http://example.com');
+ await expect(throwIfNotOk(response)).rejects.toThrow('I am a custom message');
+ });
+
+ test('does NOT do a throw if it is given response is not ok', async () => {
+ fetchMock.mock('http://example.com', 200);
+ const response = await fetch('http://example.com');
+ await expect(throwIfNotOk(response)).resolves.toEqual(undefined);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx
new file mode 100644
index 0000000000000..b0493733a735b
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import chrome from 'ui/chrome';
+
+import { useKibanaUiSetting } from '../../lib/settings/use_kibana_ui_setting';
+import { DEFAULT_KBN_VERSION } from '../../../common/constants';
+import * as i18n from '../translations';
+import { parseJsonFromBody, ToasterErrors } from '../../components/ml/api/throw_if_not_ok';
+import { IndexPatternResponse, IndexPatternSavedObject } from '../types';
+
+const emptyIndexPattern: IndexPatternSavedObject[] = [];
+
+/**
+ * Fetches Configured Index Patterns from the Kibana saved objects API
+ *
+ * TODO: Refactor to context provider: https://github.com/elastic/siem-team/issues/448
+ *
+ * @param headers
+ * @param signal
+ */
+export const getIndexPatterns = async (
+ signal: AbortSignal,
+ headers?: Record
+): Promise => {
+ const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
+ const response = await fetch(
+ `${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`,
+ {
+ method: 'GET',
+ credentials: 'same-origin',
+ headers: {
+ 'content-type': 'application/json',
+ 'kbn-xsrf': kbnVersion,
+ 'kbn-version': kbnVersion,
+ 'kbn-system-api': 'true',
+ ...headers,
+ },
+ signal,
+ }
+ );
+ await throwIfNotOk(response);
+ const results: IndexPatternResponse = await response.json();
+
+ if (results.saved_objects && Array.isArray(results.saved_objects)) {
+ return results.saved_objects;
+ } else {
+ return emptyIndexPattern;
+ }
+};
+
+export const throwIfNotOk = async (response: Response): Promise => {
+ if (!response.ok) {
+ const body = await parseJsonFromBody(response);
+ if (body != null && body.message) {
+ if (body.statusCode != null) {
+ throw new ToasterErrors([body.message, `${i18n.STATUS_CODE} ${body.statusCode}`]);
+ } else {
+ throw new ToasterErrors([body.message]);
+ }
+ } else {
+ throw new ToasterErrors([`${i18n.NETWORK_ERROR} ${response.statusText}`]);
+ }
+ }
+};
diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/helpers.test.tsx
new file mode 100644
index 0000000000000..b02db810f72e1
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/hooks/api/helpers.test.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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 { getIndexPatternTitleIdMapping, getIndexPatternTitles } from './helpers';
+import { mockIndexPatternSavedObjects } from './__mock__/api';
+
+describe('helpers', () => {
+ describe('getIndexPatternTitles', () => {
+ test('returns empty array when no index patterns are provided', () => {
+ const indexPatternTitles = getIndexPatternTitles([]);
+ expect(indexPatternTitles.length).toEqual(0);
+ });
+
+ test('returns titles when index patterns are provided', () => {
+ const indexPatternTitles = getIndexPatternTitles(mockIndexPatternSavedObjects);
+ expect(indexPatternTitles).toEqual(['filebeat-*', 'auditbeat-*']);
+ });
+ });
+
+ describe('getIndexPatternTitleIdMapping', () => {
+ test('returns empty array when no index patterns are provided', () => {
+ const indexPatternTitleIdMapping = getIndexPatternTitleIdMapping([]);
+ expect(indexPatternTitleIdMapping.length).toEqual(0);
+ });
+
+ test('returns correct mapping when index patterns are provided', () => {
+ const indexPatternTitleIdMapping = getIndexPatternTitleIdMapping(
+ mockIndexPatternSavedObjects
+ );
+ expect(indexPatternTitleIdMapping).toEqual([
+ { id: '2d1fe420-eeee-11e9-ad95-4b5e687c2aee', title: 'filebeat-*' },
+ { id: '5463ec70-c7ba-ffff-ad95-4b5e687c2aee', title: 'auditbeat-*' },
+ ]);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/helpers.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/helpers.tsx
new file mode 100644
index 0000000000000..bcf3f1f3d3c08
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/hooks/api/helpers.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { IndexPatternSavedObject } from '../types';
+
+/**
+ * Returns a string array of Index Pattern Titles
+ *
+ * @param indexPatterns IndexPatternSavedObject[] as provided from the useIndexPatterns() hook
+ */
+export const getIndexPatternTitles = (indexPatterns: IndexPatternSavedObject[]): string[] =>
+ indexPatterns.reduce((acc, v) => [...acc, v.attributes.title], []);
+
+/**
+ * Returns a mapping of indexPatternTitle to indexPatternId
+ *
+ * @param indexPatterns IndexPatternSavedObject[] as provided from the useIndexPatterns() hook
+ */
+export const getIndexPatternTitleIdMapping = (
+ indexPatterns: IndexPatternSavedObject[]
+): Array<{ title: string; id: string }> =>
+ indexPatterns.reduce>((acc, v) => {
+ if (v.attributes && v.attributes.title) {
+ return [...acc, { title: v.attributes.title, id: v.id }];
+ } else {
+ return acc;
+ }
+ }, []);
diff --git a/x-pack/legacy/plugins/siem/public/hooks/translations.ts b/x-pack/legacy/plugins/siem/public/hooks/translations.ts
new file mode 100644
index 0000000000000..ba3ec40df466a
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/hooks/translations.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 { i18n } from '@kbn/i18n';
+
+export const STATUS_CODE = i18n.translate(
+ 'xpack.siem.components.ml.api.errors.statusCodeFailureTitle',
+ {
+ defaultMessage: 'Status Code:',
+ }
+);
+
+export const NETWORK_ERROR = i18n.translate(
+ 'xpack.siem.components.ml.api.errors.networkErrorFailureTitle',
+ {
+ defaultMessage: 'Network Error:',
+ }
+);
+
+export const INDEX_PATTERN_FETCH_FAILURE = i18n.translate(
+ 'xpack.siem.components.mlPopup.hooks.errors.indexPatternFetchFailureTitle',
+ {
+ defaultMessage: 'Index pattern fetch failure',
+ }
+);
diff --git a/x-pack/legacy/plugins/siem/public/hooks/types.ts b/x-pack/legacy/plugins/siem/public/hooks/types.ts
new file mode 100644
index 0000000000000..4d66d8e191235
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/hooks/types.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface IndexPatternSavedObject {
+ attributes: {
+ title: string;
+ };
+ id: string;
+ type: string;
+ updated_at: string;
+ version: string;
+}
+
+export interface IndexPatternResponse {
+ page: number;
+ per_page: number;
+ saved_objects: IndexPatternSavedObject[];
+ total: number;
+}
diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx
similarity index 65%
rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_index_patterns.tsx
rename to x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx
index 3f5f6f9c4e958..b7e899cc50f75 100644
--- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_index_patterns.tsx
+++ b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx
@@ -6,24 +6,19 @@
import { useEffect, useState } from 'react';
-import { getIndexPatterns } from '../api';
-import { useStateToaster } from '../../toasters';
-import { errorToToaster } from '../../ml/api/error_to_toaster';
-import { useKibanaUiSetting } from '../../../lib/settings/use_kibana_ui_setting';
-import { DEFAULT_KBN_VERSION } from '../../../../common/constants';
+import { useStateToaster } from '../components/toasters';
+import { errorToToaster } from '../components/ml/api/error_to_toaster';
import * as i18n from './translations';
-import { IndexPatternSavedObject } from '../types';
+import { IndexPatternSavedObject } from '../components/ml_popover/types';
+import { getIndexPatterns } from './api/api';
type Return = [boolean, IndexPatternSavedObject[]];
-// TODO: Used by more than just ML now -- refactor to shared component https://github.com/elastic/siem-team/issues/448
-
export const useIndexPatterns = (refreshToggle = false): Return => {
const [indexPatterns, setIndexPatterns] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [, dispatchToaster] = useStateToaster();
- const [kbnVersion] = useKibanaUiSetting(DEFAULT_KBN_VERSION);
useEffect(() => {
let isSubscribed = true;
@@ -32,12 +27,7 @@ export const useIndexPatterns = (refreshToggle = false): Return => {
async function fetchIndexPatterns() {
try {
- const data = await getIndexPatterns(
- {
- 'kbn-version': kbnVersion,
- },
- abortCtrl.signal
- );
+ const data = await getIndexPatterns(abortCtrl.signal);
if (isSubscribed) {
setIndexPatterns(data);
diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts
index 83fa30c97145f..02f098cb7b9a9 100644
--- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts
+++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts
@@ -7,8 +7,6 @@
import { DEFAULT_TIMELINE_WIDTH } from '../components/timeline/body/helpers';
import {
Direction,
- DomainsFields,
- FlowDirection,
FlowTarget,
HostsFields,
NetworkDnsFields,
@@ -16,7 +14,7 @@ import {
TlsFields,
UsersFields,
} from '../graphql/types';
-import { State } from '../store';
+import { networkModel, State } from '../store';
import { defaultHeaders } from './header';
import {
@@ -71,17 +69,17 @@ export const mockGlobalState: State = {
network: {
page: {
queries: {
- topNFlowSource: {
+ [networkModel.NetworkTableType.topNFlowSource]: {
activePage: 0,
limit: 10,
topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc },
},
- topNFlowDestination: {
+ [networkModel.NetworkTableType.topNFlowDestination]: {
activePage: 0,
limit: 10,
topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc },
},
- dns: {
+ [networkModel.NetworkTableType.dns]: {
activePage: 0,
limit: 10,
dnsSortField: { field: NetworkDnsFields.queryCount, direction: Direction.desc },
@@ -96,18 +94,22 @@ export const mockGlobalState: State = {
filterQueryDraft: null,
flowTarget: FlowTarget.source,
queries: {
- domains: {
+ [networkModel.IpDetailsTableType.topNFlowSource]: {
activePage: 0,
limit: 10,
- flowDirection: FlowDirection.uniDirectional,
- domainsSortField: { field: DomainsFields.bytes, direction: Direction.desc },
+ topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc },
+ },
+ [networkModel.IpDetailsTableType.topNFlowDestination]: {
+ activePage: 0,
+ limit: 10,
+ topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc },
},
- tls: {
+ [networkModel.IpDetailsTableType.tls]: {
activePage: 0,
limit: 10,
tlsSortField: { field: TlsFields._id, direction: Direction.desc },
},
- users: {
+ [networkModel.IpDetailsTableType.users]: {
activePage: 0,
limit: 10,
usersSortField: { field: UsersFields.name, direction: Direction.asc },
diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx
index d815b37496bf1..80fcce20a4982 100644
--- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
+import { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
@@ -17,23 +17,23 @@ import { HeaderPage } from '../../components/header_page';
import { LastEventTime } from '../../components/last_event_time';
import { getNetworkUrl } from '../../components/link_to/redirect_to_network';
import { manageQuery } from '../../components/page/manage_query';
-import { DomainsTable } from '../../components/page/network/domains_table';
import { FlowTargetSelectConnected } from '../../components/page/network/flow_target_select_connected';
import { IpOverview } from '../../components/page/network/ip_overview';
import { UsersTable } from '../../components/page/network/users_table';
import { TlsTable } from '../../components/page/network/tls_table';
-import { DomainsQuery } from '../../containers/domains';
import { IpOverviewQuery } from '../../containers/ip_overview';
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
import { TlsQuery } from '../../containers/tls';
import { UsersQuery } from '../../containers/users';
-import { LastEventIndexKey } from '../../graphql/types';
+import { FlowTargetSourceDest, LastEventIndexKey } from '../../graphql/types';
import { decodeIpv6 } from '../../lib/helpers';
import { networkModel, networkSelectors, State } from '../../store';
import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../store/inputs/actions';
import { NetworkKql } from './kql';
import { NetworkEmptyPage } from './network_empty_page';
+import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow';
+import { NetworkTopNFlowTable } from '../../components/page/network/network_top_n_flow_table';
import * as i18n from './translations';
import { IPDetailsComponentProps } from './types';
import { AnomalyTableProvider } from '../../components/ml/anomaly/anomaly_table_provider';
@@ -41,11 +41,12 @@ import { scoreIntervalToDateTime } from '../../components/ml/score/score_interva
import { AnomaliesNetworkTable } from '../../components/ml/tables/anomalies_network_table';
import { networkToCriteria } from '../../components/ml/criteria/network_to_criteria';
import { SpyRoute } from '../../utils/route/spy_routes';
+import { ConditionalFlexGroup } from '../../pages/network/navigation/conditional_flex_group';
-const DomainsTableManage = manageQuery(DomainsTable);
const TlsTableManage = manageQuery(TlsTable);
const UsersTableManage = manageQuery(UsersTable);
const IpOverviewManage = manageQuery(IpOverview);
+const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable);
export const IPDetailsComponent = pure(
({
@@ -127,46 +128,92 @@ export const IPDetailsComponent = pure(
-
- {({
- id,
- inspect,
- isInspected,
- domains,
- totalCount,
- pageInfo,
- loading,
- loadPage,
- refetch,
- }) => (
-
+
+
- )}
-
+ >
+ {({
+ id,
+ inspect,
+ isInspected,
+ loading,
+ loadPage,
+ networkTopNFlow,
+ pageInfo,
+ refetch,
+ totalCount,
+ }) => (
+
+ )}
+
+
+
+
+
+ {({
+ id,
+ inspect,
+ isInspected,
+ loading,
+ loadPage,
+ networkTopNFlow,
+ pageInfo,
+ refetch,
+ totalCount,
+ }) => (
+
+ )}
+
+
+
diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx
index 08d9252357b37..0ed652804cc8e 100644
--- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx
@@ -8,7 +8,7 @@ import React, { useCallback } from 'react';
import { Route, Switch } from 'react-router-dom';
import { EuiFlexItem } from '@elastic/eui';
-import { FlowTargetNew } from '../../../graphql/types';
+import { FlowTargetSourceDest } from '../../../graphql/types';
import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime';
import { IPsQueryTabBody } from './ips_query_tab_body';
@@ -70,11 +70,11 @@ export const NetworkRoutes = ({
render={() => (
-
+
-
+
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts
index 838aaea38608f..bcdc0ef7aa790 100644
--- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts
@@ -7,7 +7,7 @@
import { StaticIndexPattern } from 'ui/index_patterns';
import { NavTab } from '../../../components/navigation/types';
-import { FlowTargetNew } from '../../../graphql/types';
+import { FlowTargetSourceDest } from '../../../graphql/types';
import { networkModel } from '../../../store';
import { ESTermQuery } from '../../../../common/typed_json';
import { NarrowDateRange } from '../../../components/ml/types';
@@ -25,7 +25,7 @@ export type DnsQueryTabBodyProps = QueryTabBodyProps & GlobalTimeArgs;
export type IPsQueryTabBodyProps = QueryTabBodyProps &
GlobalTimeArgs & {
indexPattern: StaticIndexPattern;
- flowTarget: FlowTargetNew;
+ flowTarget: FlowTargetSourceDest;
};
export type AnomaliesQueryTabBodyProps = QueryTabBodyProps &
diff --git a/x-pack/legacy/plugins/siem/public/store/network/actions.ts b/x-pack/legacy/plugins/siem/public/store/network/actions.ts
index 7408037b9f835..c2c71a4643e6a 100644
--- a/x-pack/legacy/plugins/siem/public/store/network/actions.ts
+++ b/x-pack/legacy/plugins/siem/public/store/network/actions.ts
@@ -7,8 +7,6 @@
import actionCreatorFactory from 'typescript-fsa';
import {
- DomainsSortField,
- FlowDirection,
FlowTarget,
NetworkDnsSortField,
NetworkTopNFlowSortField,
@@ -17,59 +15,61 @@ import {
} from '../../graphql/types';
import { KueryFilterQuery, networkModel, SerializedFilterQuery } from '../model';
-import { IpDetailsTableType, NetworkTableType, NetworkType } from './model';
-
const actionCreator = actionCreatorFactory('x-pack/siem/local/network');
export const updateNetworkPageTableActivePage = actionCreator<{
activePage: number;
- tableType: NetworkTableType;
+ tableType: networkModel.NetworkTableType;
}>('UPDATE_NETWORK_PAGE_TABLE_ACTIVE_PAGE');
-export const setNetworkTablesActivePageToZero = actionCreator(
- 'SET_NETWORK_TABLES_ACTIVE_PAGE_TO_ZERO'
-);
-
export const updateIpDetailsTableActivePage = actionCreator<{
activePage: number;
- tableType: IpDetailsTableType;
+ tableType: networkModel.IpDetailsTableType;
}>('UPDATE_NETWORK_DETAILS_TABLE_ACTIVE_PAGE');
+export const setIpDetailsTablesActivePageToZero = actionCreator(
+ 'SET_IP_DETAILS_TABLES_ACTIVE_PAGE_TO_ZERO'
+);
+
+export const setNetworkTablesActivePageToZero = actionCreator(
+ 'SET_NETWORK_TABLES_ACTIVE_PAGE_TO_ZERO'
+);
+
export const updateDnsLimit = actionCreator<{
limit: number;
- networkType: NetworkType;
+ networkType: networkModel.NetworkType;
}>('UPDATE_DNS_LIMIT');
export const updateDnsSort = actionCreator<{
dnsSortField: NetworkDnsSortField;
- networkType: NetworkType;
+ networkType: networkModel.NetworkType;
}>('UPDATE_DNS_SORT');
export const updateIsPtrIncluded = actionCreator<{
isPtrIncluded: boolean;
- networkType: NetworkType;
+ networkType: networkModel.NetworkType;
}>('UPDATE_DNS_IS_PTR_INCLUDED');
export const updateTopNFlowLimit = actionCreator<{
limit: number;
- networkType: NetworkType;
+ networkType: networkModel.NetworkType;
tableType: networkModel.TopNTableType;
}>('UPDATE_TOP_N_FLOW_LIMIT');
export const updateTopNFlowSort = actionCreator<{
topNFlowSort: NetworkTopNFlowSortField;
- networkType: NetworkType;
- tableType: networkModel.NetworkTableType;
+ networkType: networkModel.NetworkType;
+ tableType: networkModel.TopNTableType;
}>('UPDATE_TOP_N_FLOW_SORT');
export const setNetworkFilterQueryDraft = actionCreator<{
filterQueryDraft: KueryFilterQuery;
- networkType: NetworkType;
+ networkType: networkModel.NetworkType;
}>('SET_NETWORK_FILTER_QUERY_DRAFT');
export const applyNetworkFilterQuery = actionCreator<{
filterQuery: SerializedFilterQuery;
- networkType: NetworkType;
+ networkType: networkModel.NetworkType;
}>('APPLY_NETWORK_FILTER_QUERY');
// IP Details Actions
@@ -77,19 +77,6 @@ export const updateIpDetailsFlowTarget = actionCreator<{
flowTarget: FlowTarget;
}>('UPDATE_IP_DETAILS_TARGET');
-// Domains Table Actions
-export const updateDomainsLimit = actionCreator<{
- limit: number;
-}>('UPDATE_DOMAINS_LIMIT');
-
-export const updateDomainsFlowDirection = actionCreator<{
- flowDirection: FlowDirection;
-}>('UPDATE_DOMAINS_DIRECTION');
-
-export const updateDomainsSort = actionCreator<{
- domainsSortField: DomainsSortField;
-}>('UPDATE_DOMAINS_SORT');
-
// TLS Table Actions
export const updateTlsSort = actionCreator<{
tlsSortField: TlsSortField;
diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts b/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts
index 91b918cb45436..13c98eb800916 100644
--- a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts
+++ b/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts
@@ -8,8 +8,6 @@ import {
Direction,
NetworkTopNFlowFields,
NetworkDnsFields,
- FlowDirection,
- DomainsFields,
TlsFields,
UsersFields,
FlowTarget,
@@ -52,12 +50,19 @@ export const mockNetworkState: NetworkModel = {
},
details: {
queries: {
- [IpDetailsTableType.domains]: {
- activePage: 8,
- flowDirection: FlowDirection.uniDirectional,
+ [IpDetailsTableType.topNFlowSource]: {
+ activePage: 7,
limit: DEFAULT_TABLE_LIMIT,
- domainsSortField: {
- field: DomainsFields.bytes,
+ topNFlowSort: {
+ field: NetworkTopNFlowFields.bytes_out,
+ direction: Direction.desc,
+ },
+ },
+ [IpDetailsTableType.topNFlowDestination]: {
+ activePage: 3,
+ limit: DEFAULT_TABLE_LIMIT,
+ topNFlowSort: {
+ field: NetworkTopNFlowFields.bytes_out,
direction: Direction.desc,
},
},
@@ -88,17 +93,17 @@ describe('Network redux store', () => {
describe('#setNetworkQueriesActivePageToZero', () => {
test('set activePage to zero for all queries in hosts page ', () => {
expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.page)).toEqual({
- topNFlowSource: {
+ [NetworkTableType.topNFlowSource]: {
activePage: 0,
limit: 10,
topNFlowSort: { field: 'bytes_out', direction: 'desc' },
},
- topNFlowDestination: {
+ [NetworkTableType.topNFlowDestination]: {
activePage: 0,
limit: 10,
topNFlowSort: { field: 'bytes_out', direction: 'desc' },
},
- dns: {
+ [NetworkTableType.dns]: {
activePage: 0,
limit: 10,
dnsSortField: { field: 'uniqueDomains', direction: 'desc' },
@@ -109,14 +114,26 @@ describe('Network redux store', () => {
test('set activePage to zero for all queries in host details ', () => {
expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.details)).toEqual({
- domains: {
+ [IpDetailsTableType.topNFlowSource]: {
+ activePage: 0,
+ limit: 10,
+ topNFlowSort: { field: 'bytes_out', direction: 'desc' },
+ },
+ [IpDetailsTableType.topNFlowDestination]: {
+ activePage: 0,
+ limit: 10,
+ topNFlowSort: { field: 'bytes_out', direction: 'desc' },
+ },
+ [IpDetailsTableType.tls]: {
+ activePage: 0,
+ limit: 10,
+ tlsSortField: { field: '_id', direction: 'desc' },
+ },
+ [IpDetailsTableType.users]: {
activePage: 0,
- flowDirection: 'uniDirectional',
limit: 10,
- domainsSortField: { field: 'bytes', direction: 'desc' },
+ usersSortField: { field: 'name', direction: 'asc' },
},
- tls: { activePage: 0, limit: 10, tlsSortField: { field: '_id', direction: 'desc' } },
- users: { activePage: 0, limit: 10, usersSortField: { field: 'name', direction: 'asc' } },
});
});
});
diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.ts b/x-pack/legacy/plugins/siem/public/store/network/helpers.ts
index 2d19ad4f44213..b9876457625fb 100644
--- a/x-pack/legacy/plugins/siem/public/store/network/helpers.ts
+++ b/x-pack/legacy/plugins/siem/public/store/network/helpers.ts
@@ -34,8 +34,12 @@ export const setNetworkDetailsQueriesActivePageToZero = (
state: NetworkModel
): IpOverviewQueries => ({
...state.details.queries,
- [IpDetailsTableType.domains]: {
- ...state.details.queries[IpDetailsTableType.domains],
+ [IpDetailsTableType.topNFlowSource]: {
+ ...state.details.queries[IpDetailsTableType.topNFlowSource],
+ activePage: DEFAULT_TABLE_ACTIVE_PAGE,
+ },
+ [IpDetailsTableType.topNFlowDestination]: {
+ ...state.details.queries[IpDetailsTableType.topNFlowDestination],
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
},
[IpDetailsTableType.tls]: {
diff --git a/x-pack/legacy/plugins/siem/public/store/network/model.ts b/x-pack/legacy/plugins/siem/public/store/network/model.ts
index 8fefb6099da6b..541a2fe1a02e3 100644
--- a/x-pack/legacy/plugins/siem/public/store/network/model.ts
+++ b/x-pack/legacy/plugins/siem/public/store/network/model.ts
@@ -5,15 +5,13 @@
*/
import {
- DomainsSortField,
- FlowDirection,
FlowTarget,
NetworkDnsSortField,
NetworkTopNFlowSortField,
TlsSortField,
UsersSortField,
} from '../../graphql/types';
-import { KueryFilterQuery, networkModel, SerializedFilterQuery } from '../model';
+import { KueryFilterQuery, SerializedFilterQuery } from '../model';
export enum NetworkType {
page = 'page',
@@ -27,11 +25,14 @@ export enum NetworkTableType {
}
export type TopNTableType =
- | networkModel.NetworkTableType.topNFlowDestination
- | networkModel.NetworkTableType.topNFlowSource;
+ | NetworkTableType.topNFlowDestination
+ | NetworkTableType.topNFlowSource
+ | IpDetailsTableType.topNFlowDestination
+ | IpDetailsTableType.topNFlowSource;
export enum IpDetailsTableType {
- domains = 'domains',
+ topNFlowSource = 'topNFlowSourceIp',
+ topNFlowDestination = 'topNFlowDestinationIp',
tls = 'tls',
users = 'users',
}
@@ -64,10 +65,6 @@ export interface NetworkPageModel {
}
// IP Details Models
-export interface DomainsQuery extends BasicQueryPaginated {
- flowDirection: FlowDirection;
- domainsSortField: DomainsSortField;
-}
export interface TlsQuery extends BasicQueryPaginated {
tlsSortField: TlsSortField;
@@ -78,7 +75,8 @@ export interface UsersQuery extends BasicQueryPaginated {
}
export interface IpOverviewQueries {
- [IpDetailsTableType.domains]: DomainsQuery;
+ [IpDetailsTableType.topNFlowSource]: TopNFlowQuery;
+ [IpDetailsTableType.topNFlowDestination]: TopNFlowQuery;
[IpDetailsTableType.tls]: TlsQuery;
[IpDetailsTableType.users]: UsersQuery;
}
diff --git a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts
index 84f0772ee4ec6..df7d496714530 100644
--- a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts
+++ b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts
@@ -8,8 +8,6 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers';
import {
Direction,
- DomainsFields,
- FlowDirection,
FlowTarget,
NetworkDnsFields,
NetworkTopNFlowFields,
@@ -20,29 +18,27 @@ import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../constants';
import {
applyNetworkFilterQuery,
+ setIpDetailsTablesActivePageToZero,
setNetworkFilterQueryDraft,
+ setNetworkTablesActivePageToZero,
updateDnsLimit,
updateDnsSort,
- updateDomainsFlowDirection,
- updateDomainsLimit,
- updateTlsLimit,
- updateDomainsSort,
updateIpDetailsFlowTarget,
- updateIsPtrIncluded,
updateIpDetailsTableActivePage,
+ updateIsPtrIncluded,
updateNetworkPageTableActivePage,
+ updateTlsLimit,
+ updateTlsSort,
updateTopNFlowLimit,
updateTopNFlowSort,
- updateTlsSort,
updateUsersLimit,
updateUsersSort,
- setNetworkTablesActivePageToZero,
} from './actions';
import { IpDetailsTableType, NetworkModel, NetworkTableType, NetworkType } from './model';
import {
- setNetworkQueriesActivePageToZero,
- setNetworkPageQueriesActivePageToZero,
setNetworkDetailsQueriesActivePageToZero,
+ setNetworkPageQueriesActivePageToZero,
+ setNetworkQueriesActivePageToZero,
} from './helpers';
export type NetworkState = NetworkModel;
@@ -81,12 +77,19 @@ export const initialNetworkState: NetworkState = {
},
details: {
queries: {
- [IpDetailsTableType.domains]: {
+ [IpDetailsTableType.topNFlowSource]: {
activePage: DEFAULT_TABLE_ACTIVE_PAGE,
- flowDirection: FlowDirection.uniDirectional,
limit: DEFAULT_TABLE_LIMIT,
- domainsSortField: {
- field: DomainsFields.bytes,
+ topNFlowSort: {
+ field: NetworkTopNFlowFields.bytes_out,
+ direction: Direction.desc,
+ },
+ },
+ [IpDetailsTableType.topNFlowDestination]: {
+ activePage: DEFAULT_TABLE_ACTIVE_PAGE,
+ limit: DEFAULT_TABLE_LIMIT,
+ topNFlowSort: {
+ field: NetworkTopNFlowFields.bytes_out,
direction: Direction.desc,
},
},
@@ -125,6 +128,13 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
queries: setNetworkDetailsQueriesActivePageToZero(state),
},
}))
+ .case(setIpDetailsTablesActivePageToZero, state => ({
+ ...state,
+ details: {
+ ...state.details,
+ queries: setNetworkDetailsQueriesActivePageToZero(state),
+ },
+ }))
.case(updateIpDetailsTableActivePage, (state, { activePage, tableType }) => ({
...state,
[NetworkType.details]: {
@@ -190,32 +200,84 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
},
},
}))
- .case(updateTopNFlowLimit, (state, { limit, networkType, tableType }) => ({
- ...state,
- [networkType]: {
- ...state[networkType],
- queries: {
- ...state[networkType].queries,
- [tableType]: {
- ...state[NetworkType.page].queries[tableType],
- limit,
+ .case(updateTopNFlowLimit, (state, { limit, networkType, tableType }) => {
+ if (
+ networkType === NetworkType.page &&
+ (tableType === NetworkTableType.topNFlowSource ||
+ tableType === NetworkTableType.topNFlowDestination)
+ ) {
+ return {
+ ...state,
+ [networkType]: {
+ ...state[networkType],
+ queries: {
+ ...state[networkType].queries,
+ [tableType]: {
+ ...state[networkType].queries[tableType],
+ limit,
+ },
+ },
},
- },
- },
- }))
- .case(updateTopNFlowSort, (state, { topNFlowSort, networkType, tableType }) => ({
- ...state,
- [networkType]: {
- ...state[networkType],
- queries: {
- ...state[networkType].queries,
- [tableType]: {
- ...state[NetworkType.page].queries[tableType],
- topNFlowSort,
+ };
+ } else if (
+ tableType === IpDetailsTableType.topNFlowDestination ||
+ tableType === IpDetailsTableType.topNFlowSource
+ ) {
+ return {
+ ...state,
+ [NetworkType.details]: {
+ ...state[NetworkType.details],
+ queries: {
+ ...state[NetworkType.details].queries,
+ [tableType]: {
+ ...state[NetworkType.details].queries[tableType],
+ limit,
+ },
+ },
},
- },
- },
- }))
+ };
+ }
+ return state;
+ })
+ .case(updateTopNFlowSort, (state, { topNFlowSort, networkType, tableType }) => {
+ if (
+ networkType === NetworkType.page &&
+ (tableType === NetworkTableType.topNFlowSource ||
+ tableType === NetworkTableType.topNFlowDestination)
+ ) {
+ return {
+ ...state,
+ [networkType]: {
+ ...state[networkType],
+ queries: {
+ ...state[networkType].queries,
+ [tableType]: {
+ ...state[networkType].queries[tableType],
+ topNFlowSort,
+ },
+ },
+ },
+ };
+ } else if (
+ tableType === IpDetailsTableType.topNFlowDestination ||
+ tableType === IpDetailsTableType.topNFlowSource
+ ) {
+ return {
+ ...state,
+ [NetworkType.details]: {
+ ...state[NetworkType.details],
+ queries: {
+ ...state[NetworkType.details].queries,
+ [tableType]: {
+ ...state[NetworkType.details].queries[tableType],
+ topNFlowSort,
+ },
+ },
+ },
+ };
+ }
+ return state;
+ })
.case(setNetworkFilterQueryDraft, (state, { filterQueryDraft, networkType }) => ({
...state,
[networkType]: {
@@ -239,19 +301,6 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
flowTarget,
},
}))
- .case(updateDomainsLimit, (state, { limit }) => ({
- ...state,
- [NetworkType.details]: {
- ...state[NetworkType.details],
- queries: {
- ...state[NetworkType.details].queries,
- [IpDetailsTableType.domains]: {
- ...state[NetworkType.details].queries.domains,
- limit,
- },
- },
- },
- }))
.case(updateTlsLimit, (state, { limit }) => ({
...state,
[NetworkType.details]: {
@@ -265,32 +314,6 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
},
},
}))
- .case(updateDomainsFlowDirection, (state, { flowDirection }) => ({
- ...state,
- [NetworkType.details]: {
- ...state[NetworkType.details],
- queries: {
- ...state[NetworkType.details].queries,
- [IpDetailsTableType.domains]: {
- ...state[NetworkType.details].queries.domains,
- flowDirection,
- },
- },
- },
- }))
- .case(updateDomainsSort, (state, { domainsSortField }) => ({
- ...state,
- [NetworkType.details]: {
- ...state[NetworkType.details],
- queries: {
- ...state[NetworkType.details].queries,
- [IpDetailsTableType.domains]: {
- ...state[NetworkType.details].queries.domains,
- domainsSortField,
- },
- },
- },
- }))
.case(updateTlsSort, (state, { tlsSortField }) => ({
...state,
[NetworkType.details]: {
diff --git a/x-pack/legacy/plugins/siem/public/store/network/selectors.ts b/x-pack/legacy/plugins/siem/public/store/network/selectors.ts
index 3a11949b3990b..c2d1fa2988e1d 100644
--- a/x-pack/legacy/plugins/siem/public/store/network/selectors.ts
+++ b/x-pack/legacy/plugins/siem/public/store/network/selectors.ts
@@ -10,8 +10,8 @@ import { createSelector } from 'reselect';
import { isFromKueryExpressionValid } from '../../lib/keury';
import { State } from '../reducer';
-import { NetworkDetailsModel, NetworkPageModel, NetworkType } from './model';
-import { FlowTargetNew } from '../../graphql/types';
+import { IpDetailsTableType, NetworkDetailsModel, NetworkPageModel, NetworkType } from './model';
+import { FlowTargetSourceDest } from '../../graphql/types';
const selectNetworkPage = (state: State): NetworkPageModel => state.network.page;
@@ -31,14 +31,24 @@ export enum NetworkTableType {
topNFlowSource = 'topNFlowSource',
topNFlowDestination = 'topNFlowDestination',
}
-export const topNFlowSelector = (flowTarget: FlowTargetNew) =>
- createSelector(
- selectNetworkPage,
+export const topNFlowSelector = (flowTarget: FlowTargetSourceDest, networkType: NetworkType) => {
+ if (networkType === NetworkType.page) {
+ return createSelector(
+ selectNetworkPage,
+ network =>
+ flowTarget === FlowTargetSourceDest.source
+ ? network.queries[NetworkTableType.topNFlowSource]
+ : network.queries[NetworkTableType.topNFlowDestination]
+ );
+ }
+ return createSelector(
+ selectNetworkDetails,
network =>
- flowTarget === FlowTargetNew.source
- ? network.queries[NetworkTableType.topNFlowSource]
- : network.queries[NetworkTableType.topNFlowDestination]
+ flowTarget === FlowTargetSourceDest.source
+ ? network.queries[IpDetailsTableType.topNFlowSource]
+ : network.queries[IpDetailsTableType.topNFlowDestination]
);
+};
// Filter Query Selectors
export const networkFilterQueryAsJson = () =>
@@ -79,12 +89,6 @@ export const ipDetailsFlowTargetSelector = () =>
network => network.flowTarget
);
-export const domainsSelector = () =>
- createSelector(
- selectNetworkDetails,
- network => network.queries.domains
- );
-
export const tlsSelector = () =>
createSelector(
selectNetworkDetails,
diff --git a/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts
index 102cc4d0ae08c..fb65883de12d8 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts
@@ -6,12 +6,7 @@
import { SourceResolvers } from '../../graphql/types';
import { AppResolverOf, ChildResolverOf } from '../../lib/framework';
-import {
- DomainsRequestOptions,
- IpDetails,
- TlsRequestOptions,
- UsersRequestOptions,
-} from '../../lib/ip_details';
+import { IpDetails, TlsRequestOptions, UsersRequestOptions } from '../../lib/ip_details';
import { createOptions, createOptionsPaginated } from '../../utils/build_query/create_options';
import { QuerySourceResolver } from '../sources/resolvers';
@@ -20,11 +15,6 @@ export type QueryIpOverviewResolver = ChildResolverOf<
QuerySourceResolver
>;
-export type QueryDomainsResolver = ChildResolverOf<
- AppResolverOf,
- QuerySourceResolver
->;
-
export type QueryTlsResolver = ChildResolverOf<
AppResolverOf,
QuerySourceResolver
@@ -44,7 +34,6 @@ export const createIpDetailsResolvers = (
): {
Source: {
IpOverview: QueryIpOverviewResolver;
- Domains: QueryDomainsResolver;
Tls: QueryTlsResolver;
Users: QueryUsersResolver;
};
@@ -54,16 +43,6 @@ export const createIpDetailsResolvers = (
const options = { ...createOptions(source, args, info), ip: args.ip };
return libs.ipDetails.getIpOverview(req, options);
},
- async Domains(source, args, { req }, info) {
- const options: DomainsRequestOptions = {
- ...createOptionsPaginated(source, args, info),
- ip: args.ip,
- domainsSortField: args.sort,
- flowTarget: args.flowTarget,
- flowDirection: args.flowDirection,
- };
- return libs.ipDetails.getDomains(req, options);
- },
async Tls(source, args, { req }, info) {
const options: TlsRequestOptions = {
...createOptionsPaginated(source, args, info),
diff --git a/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts
index 1566c54c7d56b..3acbed3977d73 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts
@@ -42,71 +42,6 @@ const ipOverviewSchema = gql`
}
`;
-const domainsSchema = gql`
- enum DomainsFields {
- domainName
- direction
- bytes
- packets
- uniqueIpCount
- }
-
- input DomainsSortField {
- field: DomainsFields!
- direction: Direction!
- }
-
- type DomainsNetworkField {
- bytes: Float
- packets: Float
- transport: String
- direction: [NetworkDirectionEcs!]
- }
-
- type DomainsItem {
- uniqueIpCount: Float
- domainName: String
- firstSeen: Date
- lastSeen: Date
- }
-
- type DomainsNode {
- _id: String
- timestamp: Date
- source: DomainsItem
- destination: DomainsItem
- client: DomainsItem
- server: DomainsItem
- network: DomainsNetworkField
- }
-
- type DomainsEdges {
- node: DomainsNode!
- cursor: CursorType!
- }
-
- type DomainsData {
- edges: [DomainsEdges!]!
- totalCount: Float!
- pageInfo: PageInfoPaginated!
- inspect: Inspect
- }
-
- extend type Source {
- Domains(
- filterQuery: String
- id: String
- ip: String!
- pagination: PaginationInputPaginated!
- sort: DomainsSortField!
- flowDirection: FlowDirection!
- flowTarget: FlowTarget!
- timerange: TimerangeInput!
- defaultIndex: [String!]!
- ): DomainsData!
- }
-`;
-
const tlsSchema = gql`
enum TlsFields {
_id
@@ -199,4 +134,4 @@ const usersSchema = gql`
}
`;
-export const ipDetailsSchemas = [ipOverviewSchema, domainsSchema, tlsSchema, usersSchema];
+export const ipDetailsSchemas = [ipOverviewSchema, tlsSchema, usersSchema];
diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts
index eb358395bd5b6..23a8d4694ccae 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts
@@ -38,6 +38,7 @@ export const createNetworkResolvers = (
...createOptionsPaginated(source, args, info),
flowTarget: args.flowTarget,
networkTopNFlowSort: args.sort,
+ ip: args.ip,
};
return libs.network.getNetworkTopNFlow(req, options);
},
diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts
index 52be207fcd871..acd19b6efc0ed 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts
@@ -25,7 +25,7 @@ export const networkSchema = gql`
type GeoItem {
geo: GeoEcsFields
- flowTarget: FlowTarget
+ flowTarget: FlowTargetSourceDest
}
type AutonomousSystemItem {
@@ -122,7 +122,8 @@ export const networkSchema = gql`
NetworkTopNFlow(
id: String
filterQuery: String
- flowTarget: FlowTargetNew!
+ ip: String
+ flowTarget: FlowTargetSourceDest!
pagination: PaginationInputPaginated!
sort: NetworkTopNFlowSortField!
timerange: TimerangeInput!
diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts
index ae17c9ef067b6..8505d3efc4341 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/types.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts
@@ -69,12 +69,6 @@ export interface HostsSortField {
direction: Direction;
}
-export interface DomainsSortField {
- field: DomainsFields;
-
- direction: Direction;
-}
-
export interface TlsSortField {
field: TlsFields;
@@ -247,17 +241,8 @@ export enum HostsFields {
lastSeen = 'lastSeen',
}
-export enum DomainsFields {
- domainName = 'domainName',
- direction = 'direction',
- bytes = 'bytes',
- packets = 'packets',
- uniqueIpCount = 'uniqueIpCount',
-}
-
-export enum FlowDirection {
- uniDirectional = 'uniDirectional',
- biDirectional = 'biDirectional',
+export enum TlsFields {
+ _id = '_id',
}
export enum FlowTarget {
@@ -267,27 +252,12 @@ export enum FlowTarget {
source = 'source',
}
-export enum NetworkDirectionEcs {
- inbound = 'inbound',
- outbound = 'outbound',
- internal = 'internal',
- external = 'external',
- incoming = 'incoming',
- outgoing = 'outgoing',
- listening = 'listening',
- unknown = 'unknown',
-}
-
-export enum TlsFields {
- _id = '_id',
-}
-
export enum UsersFields {
name = 'name',
count = 'count',
}
-export enum FlowTargetNew {
+export enum FlowTargetSourceDest {
destination = 'destination',
source = 'source',
}
@@ -315,6 +285,22 @@ export enum SortFieldTimeline {
created = 'created',
}
+export enum NetworkDirectionEcs {
+ inbound = 'inbound',
+ outbound = 'outbound',
+ internal = 'internal',
+ external = 'external',
+ incoming = 'incoming',
+ outgoing = 'outgoing',
+ listening = 'listening',
+ unknown = 'unknown',
+}
+
+export enum FlowDirection {
+ uniDirectional = 'uniDirectional',
+ biDirectional = 'biDirectional',
+}
+
export type ToStringArray = string[] | string;
export type Date = string;
@@ -433,8 +419,6 @@ export interface Source {
IpOverview?: Maybe;
- Domains: DomainsData;
-
Tls: TlsData;
Users: UsersData;
@@ -1330,58 +1314,6 @@ export interface AutonomousSystemOrganization {
name?: Maybe;
}
-export interface DomainsData {
- edges: DomainsEdges[];
-
- totalCount: number;
-
- pageInfo: PageInfoPaginated;
-
- inspect?: Maybe;
-}
-
-export interface DomainsEdges {
- node: DomainsNode;
-
- cursor: CursorType;
-}
-
-export interface DomainsNode {
- _id?: Maybe;
-
- timestamp?: Maybe;
-
- source?: Maybe;
-
- destination?: Maybe;
-
- client?: Maybe;
-
- server?: Maybe;
-
- network?: Maybe;
-}
-
-export interface DomainsItem {
- uniqueIpCount?: Maybe;
-
- domainName?: Maybe;
-
- firstSeen?: Maybe;
-
- lastSeen?: Maybe;
-}
-
-export interface DomainsNetworkField {
- bytes?: Maybe;
-
- packets?: Maybe;
-
- transport?: Maybe;
-
- direction?: Maybe;
-}
-
export interface TlsData {
edges: TlsEdges[];
@@ -1575,7 +1507,7 @@ export interface AutonomousSystemItem {
export interface GeoItem {
geo?: Maybe;
- flowTarget?: Maybe;
+ flowTarget?: Maybe;
}
export interface TopNFlowItemDestination {
@@ -2070,25 +2002,6 @@ export interface IpOverviewSourceArgs {
defaultIndex: string[];
}
-export interface DomainsSourceArgs {
- filterQuery?: Maybe;
-
- id?: Maybe;
-
- ip: string;
-
- pagination: PaginationInputPaginated;
-
- sort: DomainsSortField;
-
- flowDirection: FlowDirection;
-
- flowTarget: FlowTarget;
-
- timerange: TimerangeInput;
-
- defaultIndex: string[];
-}
export interface TlsSourceArgs {
filterQuery?: Maybe;
@@ -2155,7 +2068,9 @@ export interface NetworkTopNFlowSourceArgs {
filterQuery?: Maybe;
- flowTarget: FlowTargetNew;
+ ip?: Maybe;
+
+ flowTarget: FlowTargetSourceDest;
pagination: PaginationInputPaginated;
@@ -2618,8 +2533,6 @@ export namespace SourceResolvers {
IpOverview?: IpOverviewResolver, TypeParent, TContext>;
- Domains?: DomainsResolver;
-
Tls?: TlsResolver;
Users?: UsersResolver;
@@ -2796,32 +2709,6 @@ export namespace SourceResolvers {
defaultIndex: string[];
}
- export type DomainsResolver = Resolver<
- R,
- Parent,
- TContext,
- DomainsArgs
- >;
- export interface DomainsArgs {
- filterQuery?: Maybe;
-
- id?: Maybe;
-
- ip: string;
-
- pagination: PaginationInputPaginated;
-
- sort: DomainsSortField;
-
- flowDirection: FlowDirection;
-
- flowTarget: FlowTarget;
-
- timerange: TimerangeInput;
-
- defaultIndex: string[];
- }
-
export type TlsResolver = Resolver<
R,
Parent,
@@ -2925,7 +2812,9 @@ export namespace SourceResolvers {
filterQuery?: Maybe;
- flowTarget: FlowTargetNew;
+ ip?: Maybe;
+
+ flowTarget: FlowTargetSourceDest;
pagination: PaginationInputPaginated;
@@ -5892,178 +5781,6 @@ export namespace AutonomousSystemOrganizationResolvers {
> = Resolver;
}
-export namespace DomainsDataResolvers {
- export interface Resolvers {
- edges?: EdgesResolver;
-
- totalCount?: TotalCountResolver;
-
- pageInfo?: PageInfoResolver