From 1cfc9356bd36c54b8005089decc36970c11c101d Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Thu, 2 Jul 2020 13:17:33 +0200
Subject: [PATCH 01/18] add getVisibleTypes API to SO type registry (#70559)
* add getVisibleTypes API
* doc nit
* fix mocking in tests
---
...ver.savedobjecttyperegistry.getalltypes.md | 4 ++-
...savedobjecttyperegistry.getvisibletypes.md | 19 ++++++++++++
...gin-core-server.savedobjecttyperegistry.md | 3 +-
.../saved_objects_type_registry.mock.ts | 2 ++
.../saved_objects_type_registry.test.ts | 29 ++++++++++++++++++-
.../saved_objects_type_registry.ts | 13 ++++++++-
src/core/server/server.api.md | 1 +
.../saved_objects/saved_objects_mixin.js | 2 +-
x-pack/plugins/features/server/plugin.test.ts | 8 +----
x-pack/plugins/features/server/plugin.ts | 5 +---
10 files changed, 70 insertions(+), 16 deletions(-)
create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md
index 1e0e89767c4e6..c839dd16d9a47 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md
@@ -4,7 +4,9 @@
## SavedObjectTypeRegistry.getAllTypes() method
-Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered.
+Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered, including the hidden ones.
+
+To only get the visible types (which is the most common use case), use `getVisibleTypes` instead.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md
new file mode 100644
index 0000000000000..a773c6a0a674f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md
@@ -0,0 +1,19 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [getVisibleTypes](./kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md)
+
+## SavedObjectTypeRegistry.getVisibleTypes() method
+
+Returns all visible [types](./kibana-plugin-core-server.savedobjectstype.md).
+
+A visible type is a type that doesn't explicitly define `hidden=true` during registration.
+
+Signature:
+
+```typescript
+getVisibleTypes(): SavedObjectsType[];
+```
+Returns:
+
+`SavedObjectsType[]`
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
index 69a94e4ad8c88..55ad7ca137de0 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md
@@ -16,10 +16,11 @@ export declare class SavedObjectTypeRegistry
| Method | Modifiers | Description |
| --- | --- | --- |
-| [getAllTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered. |
+| [getAllTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered, including the hidden ones.To only get the visible types (which is the most common use case), use getVisibleTypes
instead. |
| [getImportableAndExportableTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered that are importable/exportable. |
| [getIndex(type)](./kibana-plugin-core-server.savedobjecttyperegistry.getindex.md) | | Returns the indexPattern
property for given type, or undefined
if the type is not registered. |
| [getType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.gettype.md) | | Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition for given type name. |
+| [getVisibleTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md) | | Returns all visible [types](./kibana-plugin-core-server.savedobjectstype.md).A visible type is a type that doesn't explicitly define hidden=true
during registration. |
| [isHidden(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md) | | Returns the hidden
property for given type, or false
if the type is not registered. |
| [isImportableAndExportable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the management.importableAndExportable
property for given type, or false
if the type is not registered or does not define a management section. |
| [isMultiNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) | | Returns whether the type is multi-namespace (shareable); resolves to false
if the type is not registered |
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
index 5636dcadb444e..44490228490cc 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts
@@ -25,6 +25,7 @@ const createRegistryMock = (): jest.Mocked<
const mock = {
registerType: jest.fn(),
getType: jest.fn(),
+ getVisibleTypes: jest.fn(),
getAllTypes: jest.fn(),
getImportableAndExportableTypes: jest.fn(),
isNamespaceAgnostic: jest.fn(),
@@ -35,6 +36,7 @@ const createRegistryMock = (): jest.Mocked<
isImportableAndExportable: jest.fn(),
};
+ mock.getVisibleTypes.mockReturnValue([]);
mock.getAllTypes.mockReturnValue([]);
mock.getImportableAndExportableTypes.mockReturnValue([]);
mock.getIndex.mockReturnValue('.kibana-test');
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts
index e0f4d6fa28e50..25c94324c8f01 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts
@@ -99,10 +99,37 @@ describe('SavedObjectTypeRegistry', () => {
});
});
+ describe('#getVisibleTypes', () => {
+ it('returns only visible registered types', () => {
+ const typeA = createType({ name: 'typeA', hidden: false });
+ const typeB = createType({ name: 'typeB', hidden: true });
+ const typeC = createType({ name: 'typeC', hidden: false });
+ registry.registerType(typeA);
+ registry.registerType(typeB);
+ registry.registerType(typeC);
+
+ const registered = registry.getVisibleTypes();
+ expect(registered.length).toEqual(2);
+ expect(registered).toContainEqual(typeA);
+ expect(registered).toContainEqual(typeC);
+ });
+
+ it('does not mutate the registered types when altering the list', () => {
+ registry.registerType(createType({ name: 'typeA', hidden: false }));
+ registry.registerType(createType({ name: 'typeB', hidden: true }));
+ registry.registerType(createType({ name: 'typeC', hidden: false }));
+
+ const types = registry.getVisibleTypes();
+ types.splice(0, 2);
+
+ expect(registry.getVisibleTypes().length).toEqual(2);
+ });
+ });
+
describe('#getAllTypes', () => {
it('returns all registered types', () => {
const typeA = createType({ name: 'typeA' });
- const typeB = createType({ name: 'typeB' });
+ const typeB = createType({ name: 'typeB', hidden: true });
const typeC = createType({ name: 'typeC' });
registry.registerType(typeA);
registry.registerType(typeB);
diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts
index 99262d7a31e21..d0035294226ea 100644
--- a/src/core/server/saved_objects/saved_objects_type_registry.ts
+++ b/src/core/server/saved_objects/saved_objects_type_registry.ts
@@ -54,7 +54,18 @@ export class SavedObjectTypeRegistry {
}
/**
- * Return all {@link SavedObjectsType | types} currently registered.
+ * Returns all visible {@link SavedObjectsType | types}.
+ *
+ * A visible type is a type that doesn't explicitly define `hidden=true` during registration.
+ */
+ public getVisibleTypes() {
+ return [...this.types.values()].filter((type) => !this.isHidden(type.name));
+ }
+
+ /**
+ * Return all {@link SavedObjectsType | types} currently registered, including the hidden ones.
+ *
+ * To only get the visible types (which is the most common use case), use `getVisibleTypes` instead.
*/
public getAllTypes() {
return [...this.types.values()];
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 9cc5a8a386b0b..1cabaa57e519c 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -2468,6 +2468,7 @@ export class SavedObjectTypeRegistry {
getImportableAndExportableTypes(): SavedObjectsType[];
getIndex(type: string): string | undefined;
getType(type: string): SavedObjectsType | undefined;
+ getVisibleTypes(): SavedObjectsType[];
isHidden(type: string): boolean;
isImportableAndExportable(type: string): boolean;
isMultiNamespace(type: string): boolean;
diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js
index 63839b9d0f1d7..185c8807ae8b5 100644
--- a/src/legacy/server/saved_objects/saved_objects_mixin.js
+++ b/src/legacy/server/saved_objects/saved_objects_mixin.js
@@ -34,8 +34,8 @@ export function savedObjectsMixin(kbnServer, server) {
const typeRegistry = kbnServer.newPlatform.start.core.savedObjects.getTypeRegistry();
const mappings = migrator.getActiveMappings();
const allTypes = typeRegistry.getAllTypes().map((t) => t.name);
+ const visibleTypes = typeRegistry.getVisibleTypes().map((t) => t.name);
const schema = new SavedObjectsSchema(convertTypesToLegacySchema(typeRegistry.getAllTypes()));
- const visibleTypes = allTypes.filter((type) => !schema.isHiddenType(type));
server.decorate('server', 'kibanaMigrator', migrator);
diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts
index 79fd012337b00..3d85c2e9eb751 100644
--- a/x-pack/plugins/features/server/plugin.test.ts
+++ b/x-pack/plugins/features/server/plugin.test.ts
@@ -10,19 +10,13 @@ const initContext = coreMock.createPluginInitializerContext();
const coreSetup = coreMock.createSetup();
const coreStart = coreMock.createStart();
const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock();
-typeRegistry.getAllTypes.mockReturnValue([
+typeRegistry.getVisibleTypes.mockReturnValue([
{
name: 'foo',
hidden: false,
mappings: { properties: {} },
namespaceType: 'single' as 'single',
},
- {
- name: 'bar',
- hidden: true,
- mappings: { properties: {} },
- namespaceType: 'agnostic' as 'agnostic',
- },
]);
coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry);
diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts
index 149c1acfb5086..5783b20eae648 100644
--- a/x-pack/plugins/features/server/plugin.ts
+++ b/x-pack/plugins/features/server/plugin.ts
@@ -80,10 +80,7 @@ export class Plugin {
private registerOssFeatures(savedObjects: SavedObjectsServiceStart) {
const registry = savedObjects.getTypeRegistry();
- const savedObjectTypes = registry
- .getAllTypes()
- .filter((t) => !t.hidden)
- .map((t) => t.name);
+ const savedObjectTypes = registry.getVisibleTypes().map((t) => t.name);
this.logger.debug(
`Registering OSS features with SO types: ${savedObjectTypes.join(', ')}. "includeTimelion": ${
From 7d63cafd5d15ce5e85ba551472f6565f361b39d8 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Thu, 2 Jul 2020 12:31:51 +0100
Subject: [PATCH 02/18] chore(NA): disable alerts_detection_rules cypress
suites (#70577)
---
.../cypress/integration/alerts_detection_rules_custom.spec.ts | 3 ++-
.../cypress/integration/alerts_detection_rules_export.spec.ts | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
index 9e9732a403f8f..2a1a2d2c8e194 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts
@@ -64,7 +64,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login';
import { ALERTS_URL } from '../urls/navigation';
-describe('Detection rules, custom', () => {
+// // Skipped as was causing failures on master
+describe.skip('Detection rules, custom', () => {
before(() => {
esArchiverLoad('custom_rule_with_timeline');
});
diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
index 25fc1fc3a7c11..06e9228de4f49 100644
--- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts
@@ -17,7 +17,8 @@ import { ALERTS_URL } from '../urls/navigation';
const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson';
-describe('Export rules', () => {
+// Skipped as was causing failures on master
+describe.skip('Export rules', () => {
before(() => {
esArchiverLoad('custom_rules');
cy.server();
From 7b74094e0fa33bd4803dbe105b2a4ea3843ef170 Mon Sep 17 00:00:00 2001
From: Eric Davis
Date: Thu, 2 Jul 2020 07:55:52 -0400
Subject: [PATCH 03/18] Update docs for api authentication usage (#66819)
Co-authored-by: Kaarina Tungseth
---
docs/api/using-api.asciidoc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc
index aba65f2e921c2..e58d9c39ee8c4 100644
--- a/docs/api/using-api.asciidoc
+++ b/docs/api/using-api.asciidoc
@@ -10,7 +10,7 @@ NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to inte
[float]
[[api-authentication]]
=== Authentication
-{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console.
+{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, which is where the username and password are stored in order to be passed as part of the call.
[float]
[[api-calls]]
From c081caa6341143eb094a9ef00963eaada7d1c72c Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Thu, 2 Jul 2020 08:47:37 -0400
Subject: [PATCH 04/18] [Security_Solution][Endpoint] Leveraging msearch and
ancestry array for resolver (#70134)
* Refactor generator for ancestry support
* Adding optional ancestry array
* Refactor the pagination since the totals are not used anymore
* Updating the queries to not use aggregations for determining the totals
* Refactoring the children helper to handle pagination without totals
* Pinning the seed for the resolver tree generator service
* Splitting the fetcher into multiple classes for msearch
* Updating tests and api for ancestry array and msearch
* Adding more comments and fixing type errors
* Fixing resolver test import
* Fixing tests and type errors
* Fixing type errors and tests
* Removing useAncestry field
* Fixing test
* Removing useAncestry field from tests
* An empty array will be returned because that's how ES will do it too
---
.../common/endpoint/models/event.ts | 18 +
.../common/endpoint/schema/resolver.ts | 2 -
.../common/endpoint/types.ts | 25 +-
.../endpoint/routes/resolver/children.ts | 4 +-
.../routes/resolver/queries/alerts.ts | 19 +-
.../endpoint/routes/resolver/queries/base.ts | 29 +-
.../routes/resolver/queries/children.test.ts | 2 +-
.../routes/resolver/queries/children.ts | 30 +-
.../routes/resolver/queries/events.ts | 19 +-
.../routes/resolver/queries/lifecycle.ts | 2 +-
.../routes/resolver/queries/multi_searcher.ts | 18 +-
.../endpoint/routes/resolver/queries/stats.ts | 6 +-
.../server/endpoint/routes/resolver/tree.ts | 3 +-
.../resolver/utils/alerts_query_handler.ts | 83 +++++
.../resolver/utils/ancestry_query_handler.ts | 130 +++++++
.../resolver/utils/children_helper.test.ts | 238 ++++++++----
.../routes/resolver/utils/children_helper.ts | 161 ++++++--
.../utils/children_lifecycle_query_handler.ts | 71 ++++
.../utils/children_start_query_handler.ts | 109 ++++++
.../resolver/utils/events_query_handler.ts | 81 ++++
.../endpoint/routes/resolver/utils/fetch.ts | 350 +++++++++---------
.../resolver/utils/lifecycle_query_handler.ts | 72 ++++
.../endpoint/routes/resolver/utils/node.ts | 15 +-
.../routes/resolver/utils/pagination.test.ts | 36 +-
.../routes/resolver/utils/pagination.ts | 90 +----
.../api_integration/apis/endpoint/resolver.ts | 72 ++--
.../test/api_integration/services/resolver.ts | 4 +-
27 files changed, 1204 insertions(+), 485 deletions(-)
create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_start_query_handler.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts
create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
index 98f4b4336a1c8..86cccff957211 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts
@@ -60,6 +60,24 @@ export function ancestryArray(event: ResolverEvent): string[] | undefined {
return event.process.Ext.ancestry;
}
+export function getAncestryAsArray(event: ResolverEvent | undefined): string[] {
+ if (!event) {
+ return [];
+ }
+
+ const ancestors = ancestryArray(event);
+ if (ancestors) {
+ return ancestors;
+ }
+
+ const parentID = parentEntityId(event);
+ if (parentID) {
+ return [parentID];
+ }
+
+ return [];
+}
+
/**
* @param event The event to get the category for
*/
diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
index 398e2710b3253..42cbc2327fc28 100644
--- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts
@@ -13,7 +13,6 @@ export const validateTree = {
params: schema.object({ id: schema.string() }),
query: schema.object({
children: schema.number({ defaultValue: 10, min: 0, max: 100 }),
- generations: schema.number({ defaultValue: 3, min: 0, max: 3 }),
ancestors: schema.number({ defaultValue: 3, min: 0, max: 5 }),
events: schema.number({ defaultValue: 100, min: 0, max: 1000 }),
alerts: schema.number({ defaultValue: 100, min: 0, max: 1000 }),
@@ -66,7 +65,6 @@ export const validateChildren = {
params: schema.object({ id: schema.string() }),
query: schema.object({
children: schema.number({ defaultValue: 10, min: 1, max: 100 }),
- generations: schema.number({ defaultValue: 3, min: 1, max: 3 }),
afterChild: schema.maybe(schema.string()),
legacyEndpointID: schema.maybe(schema.string()),
}),
diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts
index 4efe89b2429ad..42b1337a91464 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types.ts
@@ -77,12 +77,18 @@ export interface ResolverNodeStats {
*/
export interface ResolverChildNode extends ResolverLifecycleNode {
/**
- * A child node's pagination cursor can be null for a couple reasons:
- * 1. At the time of querying it could have no children in ES, in which case it will be marked as
- * null because we know it does not have children during this query.
- * 2. If the max level was reached we do not know if this node has children or not so we'll mark it as null
+ * nextChild can have 3 different states:
+ *
+ * undefined: This indicates that you should not use this node for additional queries. It does not mean that node does
+ * not have any more direct children. The node could have more direct children but to determine that, use the
+ * ResolverChildren node's nextChild.
+ *
+ * null: Indicates that we have received all the children of the node. There may be more descendants though.
+ *
+ * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants
+ * using this node's entity_id
*/
- nextChild: string | null;
+ nextChild?: string | null;
}
/**
@@ -92,7 +98,14 @@ export interface ResolverChildNode extends ResolverLifecycleNode {
export interface ResolverChildren {
childNodes: ResolverChildNode[];
/**
- * This is the children cursor for the origin of a tree.
+ * nextChild can have 2 different states:
+ *
+ * null: Indicates that we have received all the descendants that can be retrieved using this node. To retrieve more
+ * nodes in the tree use a cursor provided in one of the returned children. If no other cursor exists then the tree
+ * is complete.
+ *
+ * string: Indicates this node has more descendants that can be retrieved, pass this cursor in while using this node's
+ * entity_id for the request.
*/
nextChild: string | null;
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts
index 74448a324a4ec..9b8cd9fd3edab 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/children.ts
@@ -18,14 +18,14 @@ export function handleChildren(
return async (context, req, res) => {
const {
params: { id },
- query: { children, generations, afterChild, legacyEndpointID: endpointID },
+ query: { children, afterChild, legacyEndpointID: endpointID },
} = req;
try {
const client = context.core.elasticsearch.legacy.client;
const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID);
return res.ok({
- body: await fetcher.children(children, generations, afterChild),
+ body: await fetcher.children(children, afterChild),
});
} catch (err) {
log.warn(err);
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts
index 95bc612c58a1b..feb4a404b2359 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts
@@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
-import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
+import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
/**
* Builds a query for retrieving alerts for a node.
*/
-export class AlertsQuery extends ResolverQuery {
+export class AlertsQuery extends ResolverQuery {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
@@ -38,11 +38,7 @@ export class AlertsQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(
- uniquePIDs.length,
- 'endgame.serial_event_id',
- 'endgame.unique_pid'
- ),
+ ...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}
@@ -60,14 +56,11 @@ export class AlertsQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.entity_id'),
+ ...this.pagination.buildQueryFields('event.id'),
};
}
- formatResponse(response: SearchResponse): PaginatedResults {
- return {
- results: ResolverQuery.getResults(response),
- totals: PaginationBuilder.getTotals(response.aggregations),
- };
+ formatResponse(response: SearchResponse): ResolverEvent[] {
+ return this.getResults(response);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts
index 35f8cad01e672..1b6a8f2f83387 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts
@@ -14,10 +14,12 @@ import { MSearchQuery } from './multi_searcher';
/**
* ResolverQuery provides the base structure for queries to retrieve events when building a resolver graph.
*
- * @param T the structured return type of a resolver query. This represents the type that is returned when translating
- * Elasticsearch's SearchResponse response.
+ * @param T the structured return type of a resolver query. This represents the final return type of the query after handling
+ * any aggregations.
+ * @param R the is the type after transforming ES's response. Making this definable let's us set whether it is a resolver event
+ * or something else.
*/
-export abstract class ResolverQuery implements MSearchQuery {
+export abstract class ResolverQuery implements MSearchQuery {
/**
*
* @param indexPattern the index pattern to use in the query for finding indices with documents in ES.
@@ -50,7 +52,7 @@ export abstract class ResolverQuery implements MSearchQuery {
};
}
- protected static getResults(response: SearchResponse): ResolverEvent[] {
+ protected getResults(response: SearchResponse): R[] {
return response.hits.hits.map((hit) => hit._source);
}
@@ -68,19 +70,26 @@ export abstract class ResolverQuery implements MSearchQuery {
}
/**
- * Searches ES for the specified ids.
+ * Searches ES for the specified ids and format the response.
*
* @param client a client for searching ES
* @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid)
*/
- async search(client: ILegacyScopedClusterClient, ids: string | string[]): Promise {
- const res: SearchResponse = await client.callAsCurrentUser(
- 'search',
- this.buildSearch(ids)
- );
+ async searchAndFormat(client: ILegacyScopedClusterClient, ids: string | string[]): Promise {
+ const res: SearchResponse = await this.search(client, ids);
return this.formatResponse(res);
}
+ /**
+ * Searches ES for the specified ids but do not format the response.
+ *
+ * @param client a client for searching ES
+ * @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid)
+ */
+ async search(client: ILegacyScopedClusterClient, ids: string | string[]) {
+ return client.callAsCurrentUser('search', this.buildSearch(ids));
+ }
+
/**
* Builds a query to search the legacy data format.
*
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts
index a4d4cd546ef60..8175764b3a0a2 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.test.ts
@@ -25,7 +25,7 @@ describe('Children query', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const msearch: any = query.buildMSearch(['1234', '5678']);
expect(msearch[0].index).toBe('index-pattern');
- expect(msearch[1].query.bool.filter[0]).toStrictEqual({
+ expect(msearch[1].query.bool.filter[0].bool.should[0]).toStrictEqual({
terms: { 'process.parent.entity_id': ['1234', '5678'] },
});
});
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts
index b7b1a16926a15..7fd3808662baa 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts
@@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
-import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
+import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
/**
* Builds a query for retrieving descendants of a node.
*/
-export class ChildrenQuery extends ResolverQuery {
+export class ChildrenQuery extends ResolverQuery {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
@@ -53,11 +53,7 @@ export class ChildrenQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(
- uniquePIDs.length,
- 'endgame.serial_event_id',
- 'endgame.unique_ppid'
- ),
+ ...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}
@@ -67,7 +63,16 @@ export class ChildrenQuery extends ResolverQuery {
bool: {
filter: [
{
- terms: { 'process.parent.entity_id': entityIDs },
+ bool: {
+ should: [
+ {
+ terms: { 'process.parent.entity_id': entityIDs },
+ },
+ {
+ terms: { 'process.Ext.ancestry': entityIDs },
+ },
+ ],
+ },
},
{
term: { 'event.category': 'process' },
@@ -81,14 +86,11 @@ export class ChildrenQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.parent.entity_id'),
+ ...this.pagination.buildQueryFields('event.id'),
};
}
- formatResponse(response: SearchResponse): PaginatedResults {
- return {
- results: ResolverQuery.getResults(response),
- totals: PaginationBuilder.getTotals(response.aggregations),
- };
+ formatResponse(response: SearchResponse): ResolverEvent[] {
+ return this.getResults(response);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
index ec65e30d1d5d4..abc86826e77dd 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts
@@ -6,13 +6,13 @@
import { SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { ResolverQuery } from './base';
-import { PaginationBuilder, PaginatedResults } from '../utils/pagination';
+import { PaginationBuilder } from '../utils/pagination';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
/**
* Builds a query for retrieving related events for a node.
*/
-export class EventsQuery extends ResolverQuery {
+export class EventsQuery extends ResolverQuery {
constructor(
private readonly pagination: PaginationBuilder,
indexPattern: string | string[],
@@ -45,11 +45,7 @@ export class EventsQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(
- uniquePIDs.length,
- 'endgame.serial_event_id',
- 'endgame.unique_pid'
- ),
+ ...this.pagination.buildQueryFields('endgame.serial_event_id'),
};
}
@@ -74,14 +70,11 @@ export class EventsQuery extends ResolverQuery {
],
},
},
- ...this.pagination.buildQueryFields(entityIDs.length, 'event.id', 'process.entity_id'),
+ ...this.pagination.buildQueryFields('event.id'),
};
}
- formatResponse(response: SearchResponse): PaginatedResults {
- return {
- results: ResolverQuery.getResults(response),
- totals: PaginationBuilder.getTotals(response.aggregations),
- };
+ formatResponse(response: SearchResponse): ResolverEvent[] {
+ return this.getResults(response);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts
index 93910293b00af..0b5728958e91f 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts
@@ -60,6 +60,6 @@ export class LifecycleQuery extends ResolverQuery {
}
formatResponse(response: SearchResponse): ResolverEvent[] {
- return ResolverQuery.getResults(response);
+ return this.getResults(response);
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts
index f873ab3019f64..02dbd92d9252b 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts
@@ -5,7 +5,7 @@
*/
import { ILegacyScopedClusterClient } from 'kibana/server';
-import { MSearchResponse } from 'elasticsearch';
+import { MSearchResponse, SearchResponse } from 'elasticsearch';
import { ResolverEvent } from '../../../../../common/endpoint/types';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
@@ -34,6 +34,10 @@ export interface QueryInfo {
* one or many unique identifiers to be searched for in this query
*/
ids: string | string[];
+ /**
+ * a function to handle the response
+ */
+ handler: (response: SearchResponse) => void;
}
/**
@@ -57,10 +61,10 @@ export class MultiSearcher {
throw new Error('No queries provided to MultiSearcher');
}
- let searchQuery: JsonObject[] = [];
- queries.forEach(
- (info) => (searchQuery = [...searchQuery, ...info.query.buildMSearch(info.ids)])
- );
+ const searchQuery: JsonObject[] = [];
+ for (const info of queries) {
+ searchQuery.push(...info.query.buildMSearch(info.ids));
+ }
const res: MSearchResponse = await this.client.callAsCurrentUser('msearch', {
body: searchQuery,
});
@@ -72,6 +76,8 @@ export class MultiSearcher {
if (res.responses.length !== queries.length) {
throw new Error(`Responses length was: ${res.responses.length} expected ${queries.length}`);
}
- return res.responses;
+ for (let i = 0; i < queries.length; i++) {
+ queries[i].handler(res.responses[i]);
+ }
}
}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts
index a728054bef219..b8fa409e2ca21 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts
@@ -7,13 +7,17 @@ import { SearchResponse } from 'elasticsearch';
import { ResolverQuery } from './base';
import { ResolverEvent, EventStats } from '../../../../../common/endpoint/types';
import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common';
-import { AggBucket } from '../utils/pagination';
export interface StatsResult {
alerts: Record;
events: Record;
}
+interface AggBucket {
+ key: string;
+ doc_count: number;
+}
+
interface CategoriesAgg extends AggBucket {
/**
* The reason categories is optional here is because if no data was returned in the query the categories aggregation
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts
index 181fb8c3df3f9..33011078ee823 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree.ts
@@ -21,7 +21,6 @@ export function handleTree(
params: { id },
query: {
children,
- generations,
ancestors,
events,
alerts,
@@ -37,7 +36,7 @@ export function handleTree(
const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID);
const [childrenNodes, ancestry, relatedEvents, relatedAlerts] = await Promise.all([
- fetcher.children(children, generations, afterChild),
+ fetcher.children(children, afterChild),
fetcher.ancestors(ancestors),
fetcher.events(events, afterEvent),
fetcher.alerts(alerts, afterAlert),
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts
new file mode 100644
index 0000000000000..ae17cf4c3a562
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { SearchResponse } from 'elasticsearch';
+import { ILegacyScopedClusterClient } from 'kibana/server';
+import { ResolverRelatedAlerts, ResolverEvent } from '../../../../../common/endpoint/types';
+import { createRelatedAlerts } from './node';
+import { AlertsQuery } from '../queries/alerts';
+import { PaginationBuilder } from './pagination';
+import { QueryInfo } from '../queries/multi_searcher';
+import { SingleQueryHandler } from './fetch';
+
+/**
+ * Requests related alerts for the given node.
+ */
+export class RelatedAlertsQueryHandler implements SingleQueryHandler {
+ private relatedAlerts: ResolverRelatedAlerts | undefined;
+ private readonly query: AlertsQuery;
+ constructor(
+ private readonly limit: number,
+ private readonly entityID: string,
+ after: string | undefined,
+ indexPattern: string,
+ legacyEndpointID: string | undefined
+ ) {
+ this.query = new AlertsQuery(
+ PaginationBuilder.createBuilder(limit, after),
+ indexPattern,
+ legacyEndpointID
+ );
+ }
+
+ private handleResponse = (response: SearchResponse) => {
+ const results = this.query.formatResponse(response);
+ this.relatedAlerts = createRelatedAlerts(
+ this.entityID,
+ results,
+ PaginationBuilder.buildCursorRequestLimit(this.limit, results)
+ );
+ };
+
+ /**
+ * Builds a QueryInfo object that defines the related alerts to search for and how to handle the response.
+ *
+ * This will return undefined onces the results have been retrieved from ES.
+ */
+ nextQuery(): QueryInfo | undefined {
+ if (this.getResults()) {
+ return;
+ }
+
+ return {
+ query: this.query,
+ ids: this.entityID,
+ handler: this.handleResponse,
+ };
+ }
+
+ /**
+ * Get the results after an msearch.
+ */
+ getResults() {
+ return this.relatedAlerts;
+ }
+
+ /**
+ * Perform a regular search and return the results.
+ *
+ * @param client the elasticsearch client
+ */
+ async search(client: ILegacyScopedClusterClient) {
+ const results = this.getResults();
+ if (results) {
+ return results;
+ }
+
+ this.handleResponse(await this.query.search(client, this.entityID));
+ return this.getResults() ?? createRelatedAlerts(this.entityID);
+ }
+}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts
new file mode 100644
index 0000000000000..9bf16dac791d7
--- /dev/null
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 { SearchResponse } from 'elasticsearch';
+import { ILegacyScopedClusterClient } from 'kibana/server';
+import {
+ parentEntityId,
+ entityId,
+ getAncestryAsArray,
+} from '../../../../../common/endpoint/models/event';
+import {
+ ResolverAncestry,
+ ResolverEvent,
+ ResolverLifecycleNode,
+} from '../../../../../common/endpoint/types';
+import { createAncestry, createLifecycle } from './node';
+import { LifecycleQuery } from '../queries/lifecycle';
+import { QueryInfo } from '../queries/multi_searcher';
+import { QueryHandler } from './fetch';
+
+/**
+ * Retrieve the ancestry portion of a resolver tree.
+ */
+export class AncestryQueryHandler implements QueryHandler {
+ private readonly ancestry: ResolverAncestry = createAncestry();
+ private ancestorsToFind: string[];
+ private readonly query: LifecycleQuery;
+
+ constructor(
+ private levels: number,
+ indexPattern: string,
+ legacyEndpointID: string | undefined,
+ originNode: ResolverLifecycleNode | undefined
+ ) {
+ this.ancestorsToFind = getAncestryAsArray(originNode?.lifecycle[0]).slice(0, levels);
+ this.query = new LifecycleQuery(indexPattern, legacyEndpointID);
+
+ // add the origin node to the response if it exists
+ if (originNode) {
+ this.ancestry.ancestors.push(originNode);
+ this.ancestry.nextAncestor = parentEntityId(originNode.lifecycle[0]) || null;
+ }
+ }
+
+ private toMapOfNodes(results: ResolverEvent[]) {
+ return results.reduce((nodes: Map, event: ResolverEvent) => {
+ const nodeId = entityId(event);
+ let node = nodes.get(nodeId);
+ if (!node) {
+ node = createLifecycle(nodeId, []);
+ }
+
+ node.lifecycle.push(event);
+ return nodes.set(nodeId, node);
+ }, new Map());
+ }
+
+ private setNoMore() {
+ this.ancestry.nextAncestor = null;
+ this.ancestorsToFind = [];
+ this.levels = 0;
+ }
+
+ private handleResponse = (searchResp: SearchResponse) => {
+ const results = this.query.formatResponse(searchResp);
+ if (results.length === 0) {
+ this.setNoMore();
+ return;
+ }
+
+ // bucket the start and end events together for a single node
+ const ancestryNodes = this.toMapOfNodes(results);
+
+ // the order of this array is going to be weird, it will look like this
+ // [furthest grandparent...closer grandparent, next recursive call furthest grandparent...closer grandparent]
+ this.ancestry.ancestors.push(...ancestryNodes.values());
+ this.ancestry.nextAncestor = parentEntityId(results[0]) || null;
+ this.levels = this.levels - ancestryNodes.size;
+ // the results come back in ascending order on timestamp so the first entry in the
+ // results should be the further ancestor (most distant grandparent)
+ this.ancestorsToFind = getAncestryAsArray(results[0]).slice(0, this.levels);
+ };
+
+ /**
+ * Returns whether there are more results to retrieve based on the limit that is passed in and the results that
+ * have already been received from ES.
+ */
+ hasMore(): boolean {
+ return this.levels > 0 && this.ancestorsToFind.length > 0;
+ }
+
+ /**
+ * Get a query info for retrieving the next set of results.
+ */
+ nextQuery(): QueryInfo | undefined {
+ if (this.hasMore()) {
+ return {
+ query: this.query,
+ ids: this.ancestorsToFind,
+ handler: this.handleResponse,
+ };
+ }
+ }
+
+ /**
+ * Return the results after using msearch to find them.
+ */
+ getResults() {
+ return this.ancestry;
+ }
+
+ /**
+ * Perform a regular search and return the results.
+ *
+ * @param client the elasticsearch client.
+ */
+ async search(client: ILegacyScopedClusterClient) {
+ while (this.hasMore()) {
+ const info = this.nextQuery();
+ if (!info) {
+ break;
+ }
+ this.handleResponse(await this.query.search(client, info.ids));
+ }
+ return this.getResults();
+ }
+}
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts
index 1d55cb7cfd735..ca5b5aef0f651 100644
--- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts
+++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts
@@ -3,95 +3,195 @@
* 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 { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
+import {
+ EndpointDocGenerator,
+ Tree,
+ Event,
+ TreeNode,
+} from '../../../../../common/endpoint/generate_data';
import { ChildrenNodesHelper } from './children_helper';
-import { eventId, entityId, parentEntityId } from '../../../../../common/endpoint/models/event';
-import { ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types';
-
-function findParents(events: ResolverEvent[]): ResolverEvent[] {
- const cache = _.groupBy(events, entityId);
+import { eventId, isProcessStart } from '../../../../../common/endpoint/models/event';
- const parents: ResolverEvent[] = [];
- Object.values(cache).forEach((lifecycle) => {
- const parentNode = cache[parentEntityId(lifecycle[0])!];
- if (parentNode) {
- parents.push(parentNode[0]);
+function getStartEvents(events: Event[]): Event[] {
+ const startEvents: Event[] = [];
+ for (const event of events) {
+ if (isProcessStart(event)) {
+ startEvents.push(event);
}
- });
- return parents;
+ }
+ return startEvents;
}
-function findNode(tree: ResolverChildren, id: string) {
- return tree.childNodes.find((node) => {
- return node.entityID === id;
- });
+function getAllChildrenEvents(tree: Tree) {
+ const children: Event[] = [];
+ for (const child of tree.children.values()) {
+ children.push(...child.lifecycle);
+ }
+ return children;
+}
+
+function getStartEventsFromLevels(levels: Array
- }
- >
- {indicesSwitch}
-
- ) : (
- indicesSwitch
- )}
- {isAllIndices ? null : (
-
-
-
-
-
-
-
- {
- setSelectIndicesMode('custom');
- updatePolicyConfig({ indices: indexPatterns.join(',') });
- }}
- >
-
-
-
-
- ) : (
-
-
-
-
-
- {
- setSelectIndicesMode('list');
- updatePolicyConfig({ indices: indicesSelection });
- }}
- >
-
-
-
-
- )
- }
- helpText={
- selectIndicesMode === 'list' ? (
- 0 ? (
- {
- // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
- indicesOptions.forEach((option: EuiSelectableOption) => {
- option.checked = undefined;
- });
- updatePolicyConfig({ indices: [] });
- setIndicesSelection([]);
- }}
- >
-
-
- ) : (
- {
- // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
- indicesOptions.forEach((option: EuiSelectableOption) => {
- option.checked = 'on';
- });
- updatePolicyConfig({ indices: [...indices] });
- setIndicesSelection([...indices]);
- }}
- >
-
-
- ),
- }}
- />
- ) : null
- }
- isInvalid={Boolean(errors.indices)}
- error={errors.indices}
- >
- {selectIndicesMode === 'list' ? (
- {
- const newSelectedIndices: string[] = [];
- options.forEach(({ label, checked }) => {
- if (checked === 'on') {
- newSelectedIndices.push(label);
- }
- });
- setIndicesOptions(options);
- updatePolicyConfig({ indices: newSelectedIndices });
- setIndicesSelection(newSelectedIndices);
- }}
- searchable
- height={300}
- >
- {(list, search) => (
-
- {search}
- {list}
-
- )}
-
- ) : (
- ({ label: index }))}
- placeholder={i18n.translate(
- 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder',
- {
- defaultMessage: 'Enter index patterns, i.e. logstash-*',
- }
- )}
- selectedOptions={indexPatterns.map((pattern) => ({ label: pattern }))}
- onCreateOption={(pattern: string) => {
- if (!pattern.trim().length) {
- return;
- }
- const newPatterns = [...indexPatterns, pattern];
- setIndexPatterns(newPatterns);
- updatePolicyConfig({
- indices: newPatterns.join(','),
- });
- }}
- onChange={(patterns: Array<{ label: string }>) => {
- const newPatterns = patterns.map(({ label }) => label);
- setIndexPatterns(newPatterns);
- updatePolicyConfig({
- indices: newPatterns.join(','),
- });
- }}
- />
- )}
-
-
- )}
-
-
-
- );
- };
-
- const renderIgnoreUnavailableField = () => (
-
-
-
-
-
- }
- description={
-
- }
- fullWidth
- >
-
-
- }
- checked={Boolean(config.ignoreUnavailable)}
- onChange={(e) => {
- updatePolicyConfig({
- ignoreUnavailable: e.target.checked,
- });
- }}
- />
-
-
- );
-
- const renderPartialField = () => (
-
-
-
-
-
- }
- description={
-
- }
- fullWidth
- >
-
-
- }
- checked={Boolean(config.partial)}
- onChange={(e) => {
- updatePolicyConfig({
- partial: e.target.checked,
- });
- }}
- />
-
-
- );
-
- const renderIncludeGlobalStateField = () => (
-
-
-
-
-
- }
- description={
-
- }
- fullWidth
- >
-
-
- }
- checked={config.includeGlobalState === undefined || config.includeGlobalState}
- onChange={(e) => {
- updatePolicyConfig({
- includeGlobalState: e.target.checked,
- });
- }}
- />
-
-
- );
- return (
-
- {/* Step title and doc link */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {renderIndicesField()}
- {renderIgnoreUnavailableField()}
- {renderPartialField()}
- {renderIncludeGlobalStateField()}
-
- );
-};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/index.ts
new file mode 100644
index 0000000000000..e0d632a58e4e1
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/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 { IndicesAndDataStreamsField } from './indices_and_data_streams_field';
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx
new file mode 100644
index 0000000000000..3570c74fb8fd0
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/data_streams_and_indices_list_help_text.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink } from '@elastic/eui';
+
+interface Props {
+ onSelectionChange: (selection: 'all' | 'none') => void;
+ selectedIndicesAndDataStreams: string[];
+ indices: string[];
+ dataStreams: string[];
+}
+
+export const DataStreamsAndIndicesListHelpText: FunctionComponent = ({
+ onSelectionChange,
+ selectedIndicesAndDataStreams,
+ indices,
+ dataStreams,
+}) => {
+ if (selectedIndicesAndDataStreams.length === 0) {
+ return (
+ {
+ onSelectionChange('all');
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ );
+ }
+
+ const indicesCount = selectedIndicesAndDataStreams.reduce(
+ (acc, v) => (indices.includes(v) ? acc + 1 : acc),
+ 0
+ );
+ const dataStreamsCount = selectedIndicesAndDataStreams.reduce(
+ (acc, v) => (dataStreams.includes(v) ? acc + 1 : acc),
+ 0
+ );
+
+ return (
+ {
+ onSelectionChange('none');
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts
new file mode 100644
index 0000000000000..9bf97af6400b5
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.test.ts
@@ -0,0 +1,69 @@
+/*
+ * 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 { determineListMode } from './helpers';
+
+describe('helpers', () => {
+ describe('determineListMode', () => {
+ test('list length (> 100)', () => {
+ expect(
+ determineListMode({
+ indices: Array.from(Array(101).keys()).map(String),
+ dataStreams: [],
+ configuredIndices: undefined,
+ })
+ ).toBe('custom');
+
+ // The length of indices and data streams are cumulative
+ expect(
+ determineListMode({
+ indices: Array.from(Array(51).keys()).map(String),
+ dataStreams: Array.from(Array(51).keys()).map(String),
+ configuredIndices: undefined,
+ })
+ ).toBe('custom');
+
+ // Other values should result in list mode
+ expect(
+ determineListMode({
+ indices: [],
+ dataStreams: [],
+ configuredIndices: undefined,
+ })
+ ).toBe('list');
+ });
+
+ test('configured indices is a string', () => {
+ expect(
+ determineListMode({
+ indices: [],
+ dataStreams: [],
+ configuredIndices: 'test',
+ })
+ ).toBe('custom');
+ });
+
+ test('configured indices not included in current indices and data streams', () => {
+ expect(
+ determineListMode({
+ indices: ['a'],
+ dataStreams: ['b'],
+ configuredIndices: ['a', 'b', 'c'],
+ })
+ ).toBe('custom');
+ });
+
+ test('configured indices included in current indices and data streams', () => {
+ expect(
+ determineListMode({
+ indices: ['a'],
+ dataStreams: ['b'],
+ configuredIndices: ['a', 'b'],
+ })
+ ).toBe('list');
+ });
+ });
+});
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx
new file mode 100644
index 0000000000000..98ad2fe9c5489
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/helpers.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiSelectableOption } from '@elastic/eui';
+import { orderDataStreamsAndIndices } from '../../../../../lib';
+import { DataStreamBadge } from '../../../../../data_stream_badge';
+
+export const mapSelectionToIndicesOptions = ({
+ allSelected,
+ selection,
+ dataStreams,
+ indices,
+}: {
+ allSelected: boolean;
+ selection: string[];
+ dataStreams: string[];
+ indices: string[];
+}): EuiSelectableOption[] => {
+ return orderDataStreamsAndIndices({
+ dataStreams: dataStreams.map(
+ (dataStream): EuiSelectableOption => {
+ return {
+ label: dataStream,
+ append: ,
+ checked: allSelected || selection.includes(dataStream) ? 'on' : undefined,
+ };
+ }
+ ),
+ indices: indices.map(
+ (index): EuiSelectableOption => {
+ return {
+ label: index,
+ checked: allSelected || selection.includes(index) ? 'on' : undefined,
+ };
+ }
+ ),
+ });
+};
+
+/**
+ * @remark
+ * Users with more than 100 indices will probably want to use an index pattern to select
+ * them instead, so we'll default to showing them the index pattern input. Also show the custom
+ * list if we have no exact matches in the configured array to some existing index.
+ */
+export const determineListMode = ({
+ configuredIndices,
+ indices,
+ dataStreams,
+}: {
+ configuredIndices: string | string[] | undefined;
+ indices: string[];
+ dataStreams: string[];
+}): 'custom' | 'list' => {
+ const indicesAndDataStreams = indices.concat(dataStreams);
+ return typeof configuredIndices === 'string' ||
+ indicesAndDataStreams.length > 100 ||
+ (Array.isArray(configuredIndices) &&
+ // If not every past configured index maps to an existing index or data stream
+ // we also show the custom list
+ !configuredIndices.every((c) => indicesAndDataStreams.some((i) => i === c)))
+ ? 'custom'
+ : 'list';
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/index.ts
new file mode 100644
index 0000000000000..e0d632a58e4e1
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/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 { IndicesAndDataStreamsField } from './indices_and_data_streams_field';
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx
new file mode 100644
index 0000000000000..94854905e6686
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/fields/indices_and_data_streams_field/indices_and_data_streams_field.tsx
@@ -0,0 +1,348 @@
+/*
+ * 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, FunctionComponent, useState } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import {
+ EuiComboBox,
+ EuiDescribedFormGroup,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFormRow,
+ EuiLink,
+ EuiPanel,
+ EuiSelectable,
+ EuiSelectableOption,
+ EuiSpacer,
+ EuiSwitch,
+ EuiTitle,
+ EuiToolTip,
+} from '@elastic/eui';
+
+import { SlmPolicyPayload } from '../../../../../../../../common/types';
+import { useServices } from '../../../../../../app_context';
+import { PolicyValidation } from '../../../../../../services/validation';
+
+import { orderDataStreamsAndIndices } from '../../../../../lib';
+import { DataStreamBadge } from '../../../../../data_stream_badge';
+
+import { mapSelectionToIndicesOptions, determineListMode } from './helpers';
+
+import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text';
+
+interface Props {
+ isManagedPolicy: boolean;
+ policy: SlmPolicyPayload;
+ indices: string[];
+ dataStreams: string[];
+ onUpdate: (arg: { indices?: string[] | string }) => void;
+ errors: PolicyValidation['errors'];
+}
+
+/**
+ * In future we may be able to split data streams to its own field, but for now
+ * they share an array "indices" in the snapshot lifecycle policy config. See
+ * this github issue for progress: https://github.com/elastic/elasticsearch/issues/58474
+ */
+export const IndicesAndDataStreamsField: FunctionComponent = ({
+ isManagedPolicy,
+ dataStreams,
+ indices,
+ policy,
+ onUpdate,
+ errors,
+}) => {
+ const { i18n } = useServices();
+ const { config = {} } = policy;
+
+ const indicesAndDataStreams = indices.concat(dataStreams);
+
+ // We assume all indices if the config has no indices entry or if we receive an empty array
+ const [isAllIndices, setIsAllIndices] = useState(
+ !config.indices || (Array.isArray(config.indices) && config.indices.length === 0)
+ );
+
+ const [indicesAndDataStreamsSelection, setIndicesAndDataStreamsSelection] = useState(
+ () =>
+ Array.isArray(config.indices) && !isAllIndices
+ ? indicesAndDataStreams.filter((i) => (config.indices! as string[]).includes(i))
+ : [...indicesAndDataStreams]
+ );
+
+ // States for choosing all indices, or a subset, including caching previously chosen subset list
+ const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState<
+ EuiSelectableOption[]
+ >(() =>
+ mapSelectionToIndicesOptions({
+ selection: indicesAndDataStreamsSelection,
+ dataStreams,
+ indices,
+ allSelected: isAllIndices || typeof config.indices === 'string',
+ })
+ );
+
+ // State for using selectable indices list or custom patterns
+ const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>(() =>
+ determineListMode({ configuredIndices: config.indices, dataStreams, indices })
+ );
+
+ // State for custom patterns
+ const [indexPatterns, setIndexPatterns] = useState(() =>
+ typeof config.indices === 'string'
+ ? (config.indices as string).split(',')
+ : Array.isArray(config.indices) && config.indices
+ ? config.indices
+ : []
+ );
+
+ const indicesSwitch = (
+
+ }
+ checked={isAllIndices}
+ disabled={isManagedPolicy}
+ data-test-subj="allIndicesToggle"
+ onChange={(e) => {
+ const isChecked = e.target.checked;
+ setIsAllIndices(isChecked);
+ if (isChecked) {
+ setIndicesAndDataStreamsSelection(indicesAndDataStreams);
+ setIndicesAndDataStreamsOptions(
+ mapSelectionToIndicesOptions({
+ allSelected: isAllIndices || typeof config.indices === 'string',
+ dataStreams,
+ indices,
+ selection: indicesAndDataStreamsSelection,
+ })
+ );
+ onUpdate({ indices: undefined });
+ } else {
+ onUpdate({
+ indices:
+ selectIndicesMode === 'custom'
+ ? indexPatterns.join(',')
+ : [...(indicesAndDataStreamsSelection || [])],
+ });
+ }
+ }}
+ />
+ );
+
+ return (
+
+
+
+
+
+ }
+ description={
+
+ }
+ fullWidth
+ >
+
+
+ {isManagedPolicy ? (
+
+
+
+ }
+ >
+ {indicesSwitch}
+
+ ) : (
+ indicesSwitch
+ )}
+ {isAllIndices ? null : (
+
+
+
+
+
+
+
+ {
+ setSelectIndicesMode('custom');
+ onUpdate({ indices: indexPatterns.join(',') });
+ }}
+ >
+
+
+
+
+ ) : (
+
+
+
+
+
+ {
+ setSelectIndicesMode('list');
+ onUpdate({ indices: indicesAndDataStreamsSelection });
+ }}
+ >
+
+
+
+
+ )
+ }
+ helpText={
+ selectIndicesMode === 'list' ? (
+ {
+ if (selection === 'all') {
+ // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
+ indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => {
+ option.checked = 'on';
+ });
+ onUpdate({ indices: [...indicesAndDataStreams] });
+ setIndicesAndDataStreamsSelection([...indicesAndDataStreams]);
+ } else {
+ // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
+ indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => {
+ option.checked = undefined;
+ });
+ onUpdate({ indices: [] });
+ setIndicesAndDataStreamsSelection([]);
+ }
+ }}
+ selectedIndicesAndDataStreams={indicesAndDataStreamsSelection}
+ indices={indices}
+ dataStreams={dataStreams}
+ />
+ ) : null
+ }
+ isInvalid={Boolean(errors.indices)}
+ error={errors.indices}
+ >
+ {selectIndicesMode === 'list' ? (
+ {
+ const newSelectedIndices: string[] = [];
+ options.forEach(({ label, checked }) => {
+ if (checked === 'on') {
+ newSelectedIndices.push(label);
+ }
+ });
+ setIndicesAndDataStreamsOptions(options);
+ onUpdate({ indices: newSelectedIndices });
+ setIndicesAndDataStreamsSelection(newSelectedIndices);
+ }}
+ searchable
+ height={300}
+ >
+ {(list, search) => (
+
+ {search}
+ {list}
+
+ )}
+
+ ) : (
+ ({
+ label: index,
+ value: { isDataStream: false },
+ })),
+ dataStreams: dataStreams.map((dataStream) => ({
+ label: dataStream,
+ value: { isDataStream: true },
+ })),
+ })}
+ renderOption={({ label, value }) => {
+ if (value?.isDataStream) {
+ return (
+
+ {label}
+
+
+
+
+ );
+ }
+ return label;
+ }}
+ placeholder={i18n.translate(
+ 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder',
+ {
+ defaultMessage: 'Enter index patterns, i.e. logstash-*',
+ }
+ )}
+ selectedOptions={indexPatterns.map((pattern) => ({ label: pattern }))}
+ onCreateOption={(pattern: string) => {
+ if (!pattern.trim().length) {
+ return;
+ }
+ const newPatterns = [...indexPatterns, pattern];
+ setIndexPatterns(newPatterns);
+ onUpdate({
+ indices: newPatterns.join(','),
+ });
+ }}
+ onChange={(patterns: Array<{ label: string }>) => {
+ const newPatterns = patterns.map(({ label }) => label);
+ setIndexPatterns(newPatterns);
+ onUpdate({
+ indices: newPatterns.join(','),
+ });
+ }}
+ />
+ )}
+
+
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/index.ts
new file mode 100644
index 0000000000000..24e9b36e74889
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/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 { PolicyStepSettings } from './step_settings';
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx
new file mode 100644
index 0000000000000..9d43c45d17ea7
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_settings/step_settings.tsx
@@ -0,0 +1,206 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiDescribedFormGroup,
+ EuiTitle,
+ EuiFormRow,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiSpacer,
+ EuiSwitch,
+} from '@elastic/eui';
+
+import { SlmPolicyPayload } from '../../../../../../common/types';
+import { documentationLinksService } from '../../../../services/documentation';
+import { StepProps } from '../';
+
+import { IndicesAndDataStreamsField } from './fields';
+
+export const PolicyStepSettings: React.FunctionComponent = ({
+ policy,
+ indices,
+ dataStreams,
+ updatePolicy,
+ errors,
+}) => {
+ const { config = {}, isManagedPolicy } = policy;
+
+ const updatePolicyConfig = (updatedFields: Partial): void => {
+ const newConfig = { ...config, ...updatedFields };
+ updatePolicy({
+ config: newConfig,
+ });
+ };
+
+ const renderIgnoreUnavailableField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ fullWidth
+ >
+
+
+ }
+ checked={Boolean(config.ignoreUnavailable)}
+ onChange={(e) => {
+ updatePolicyConfig({
+ ignoreUnavailable: e.target.checked,
+ });
+ }}
+ />
+
+
+ );
+
+ const renderPartialField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ fullWidth
+ >
+
+
+ }
+ checked={Boolean(config.partial)}
+ onChange={(e) => {
+ updatePolicyConfig({
+ partial: e.target.checked,
+ });
+ }}
+ />
+
+
+ );
+
+ const renderIncludeGlobalStateField = () => (
+
+
+
+
+
+ }
+ description={
+
+ }
+ fullWidth
+ >
+
+
+ }
+ checked={config.includeGlobalState === undefined || config.includeGlobalState}
+ onChange={(e) => {
+ updatePolicyConfig({
+ includeGlobalState: e.target.checked,
+ });
+ }}
+ />
+
+
+ );
+ return (
+
+ {/* Step title and doc link */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {renderIgnoreUnavailableField()}
+ {renderPartialField()}
+ {renderIncludeGlobalStateField()}
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts
index 3f3db0ff28eca..182d4ef8f583a 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/index.ts
@@ -14,6 +14,6 @@ export interface StepProps {
updateCurrentStep: (step: number) => void;
}
-export { RestoreSnapshotStepLogistics } from './step_logistics';
+export { RestoreSnapshotStepLogistics } from './step_logistics/step_logistics';
export { RestoreSnapshotStepSettings } from './step_settings';
export { RestoreSnapshotStepReview } from './step_review';
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx
new file mode 100644
index 0000000000000..877dbe8963926
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_and_indices_list_help_text.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiLink } from '@elastic/eui';
+
+interface Props {
+ onSelectionChange: (selection: 'all' | 'none') => void;
+ selectedIndicesAndDataStreams: string[];
+ indices: string[];
+ dataStreams: string[];
+}
+
+export const DataStreamsAndIndicesListHelpText: FunctionComponent = ({
+ onSelectionChange,
+ selectedIndicesAndDataStreams,
+ indices,
+ dataStreams,
+}) => {
+ if (selectedIndicesAndDataStreams.length === 0) {
+ return (
+ {
+ onSelectionChange('all');
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ );
+ }
+
+ const indicesCount = selectedIndicesAndDataStreams.reduce(
+ (acc, v) => (indices.includes(v) ? acc + 1 : acc),
+ 0
+ );
+ const dataStreamsCount = selectedIndicesAndDataStreams.reduce(
+ (acc, v) => (dataStreams.includes(v) ? acc + 1 : acc),
+ 0
+ );
+
+ return (
+ {
+ onSelectionChange('none');
+ }}
+ >
+
+
+ ),
+ }}
+ />
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx
new file mode 100644
index 0000000000000..64fce4dcfac43
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/data_streams_global_state_call_out.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import React, { FunctionComponent } from 'react';
+import { EuiCallOut, EuiLink } from '@elastic/eui';
+
+import { documentationLinksService } from '../../../../services/documentation';
+
+const i18nTexts = {
+ callout: {
+ title: (count: number) =>
+ i18n.translate('xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.title', {
+ defaultMessage:
+ 'This snapshot contains {count, plural, one {a data stream} other {data streams}}',
+ values: { count },
+ }),
+ body: () => (
+
+ {i18n.translate(
+ 'xpack.snapshotRestore.restoreForm.dataStreamsWarningCallOut.body.learnMoreLink',
+ { defaultMessage: 'Learn more' }
+ )}
+
+ ),
+ }}
+ />
+ ),
+ },
+};
+
+interface Props {
+ dataStreamsCount: number;
+}
+
+export const DataStreamsGlobalStateCallOut: FunctionComponent = ({ dataStreamsCount }) => {
+ return (
+
+ {i18nTexts.callout.body()}
+
+ );
+};
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/index.ts
new file mode 100644
index 0000000000000..8f4efcf2a91f1
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/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 { RestoreSnapshotStepLogistics } from './step_logistics';
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx
similarity index 69%
rename from x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx
rename to x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx
index c80c5a2e4c01d..d9fd4cca0d614 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_logistics/step_logistics.tsx
@@ -21,10 +21,22 @@ import {
EuiComboBox,
} from '@elastic/eui';
import { EuiSelectableOption } from '@elastic/eui';
-import { RestoreSettings } from '../../../../../common/types';
-import { documentationLinksService } from '../../../services/documentation';
-import { useServices } from '../../../app_context';
-import { StepProps } from './';
+
+import { csvToArray, isDataStreamBackingIndex } from '../../../../../../common/lib';
+import { RestoreSettings } from '../../../../../../common/types';
+
+import { documentationLinksService } from '../../../../services/documentation';
+
+import { useServices } from '../../../../app_context';
+
+import { orderDataStreamsAndIndices } from '../../../lib';
+import { DataStreamBadge } from '../../../data_stream_badge';
+
+import { StepProps } from '../index';
+
+import { DataStreamsGlobalStateCallOut } from './data_streams_global_state_call_out';
+
+import { DataStreamsAndIndicesListHelpText } from './data_streams_and_indices_list_help_text';
export const RestoreSnapshotStepLogistics: React.FunctionComponent = ({
snapshotDetails,
@@ -34,10 +46,30 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}) => {
const { i18n } = useServices();
const {
- indices: snapshotIndices,
+ indices: unfilteredSnapshotIndices,
+ dataStreams: snapshotDataStreams = [],
includeGlobalState: snapshotIncludeGlobalState,
} = snapshotDetails;
+ const snapshotIndices = unfilteredSnapshotIndices.filter(
+ (index) => !isDataStreamBackingIndex(index)
+ );
+ const snapshotIndicesAndDataStreams = snapshotIndices.concat(snapshotDataStreams);
+
+ const comboBoxOptions = orderDataStreamsAndIndices<{
+ label: string;
+ value: { isDataStream: boolean; name: string };
+ }>({
+ dataStreams: snapshotDataStreams.map((dataStream) => ({
+ label: dataStream,
+ value: { isDataStream: true, name: dataStream },
+ })),
+ indices: snapshotIndices.map((index) => ({
+ label: index,
+ value: { isDataStream: false, name: index },
+ })),
+ });
+
const {
indices: restoreIndices,
renamePattern,
@@ -47,28 +79,50 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
} = restoreSettings;
// States for choosing all indices, or a subset, including caching previously chosen subset list
- const [isAllIndices, setIsAllIndices] = useState(!Boolean(restoreIndices));
- const [indicesOptions, setIndicesOptions] = useState(
- snapshotIndices.map(
- (index): EuiSelectableOption => ({
- label: index,
- checked:
- isAllIndices ||
- // If indices is a string, we default to custom input mode, so we mark individual indices
- // as selected if user goes back to list mode
- typeof restoreIndices === 'string' ||
- (Array.isArray(restoreIndices) && restoreIndices.includes(index))
- ? 'on'
- : undefined,
- })
- )
+ const [isAllIndicesAndDataStreams, setIsAllIndicesAndDataStreams] = useState(
+ !Boolean(restoreIndices)
+ );
+ const [indicesAndDataStreamsOptions, setIndicesAndDataStreamsOptions] = useState<
+ EuiSelectableOption[]
+ >(() =>
+ orderDataStreamsAndIndices({
+ dataStreams: snapshotDataStreams.map(
+ (dataStream): EuiSelectableOption => ({
+ label: dataStream,
+ append: ,
+ checked:
+ isAllIndicesAndDataStreams ||
+ // If indices is a string, we default to custom input mode, so we mark individual indices
+ // as selected if user goes back to list mode
+ typeof restoreIndices === 'string' ||
+ (Array.isArray(restoreIndices) && restoreIndices.includes(dataStream))
+ ? 'on'
+ : undefined,
+ })
+ ),
+ indices: snapshotIndices.map(
+ (index): EuiSelectableOption => ({
+ label: index,
+ checked:
+ isAllIndicesAndDataStreams ||
+ // If indices is a string, we default to custom input mode, so we mark individual indices
+ // as selected if user goes back to list mode
+ typeof restoreIndices === 'string' ||
+ (Array.isArray(restoreIndices) && restoreIndices.includes(index))
+ ? 'on'
+ : undefined,
+ })
+ ),
+ })
);
// State for using selectable indices list or custom patterns
// Users with more than 100 indices will probably want to use an index pattern to select
// them instead, so we'll default to showing them the index pattern input.
const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>(
- typeof restoreIndices === 'string' || snapshotIndices.length > 100 ? 'custom' : 'list'
+ typeof restoreIndices === 'string' || snapshotIndicesAndDataStreams.length > 100
+ ? 'custom'
+ : 'list'
);
// State for custom patterns
@@ -83,13 +137,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
// Caching state for togglable settings
const [cachedRestoreSettings, setCachedRestoreSettings] = useState({
- indices: [...snapshotIndices],
+ indices: [...snapshotIndicesAndDataStreams],
renamePattern: '',
renameReplacement: '',
});
return (
-
+
{/* Step title and doc link */}
@@ -118,6 +175,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
+
+ {snapshotDataStreams.length ? (
+ <>
+
+
+ >
+ ) : undefined}
+
{/* Indices */}
@@ -126,16 +191,16 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent
=
}
description={
}
@@ -146,14 +211,14 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}
- checked={isAllIndices}
+ checked={isAllIndicesAndDataStreams}
onChange={(e) => {
const isChecked = e.target.checked;
- setIsAllIndices(isChecked);
+ setIsAllIndicesAndDataStreams(isChecked);
if (isChecked) {
updateRestoreSettings({ indices: undefined });
} else {
@@ -166,7 +231,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}
}}
/>
- {isAllIndices ? null : (
+ {isAllIndicesAndDataStreams ? null : (
=
@@ -210,8 +275,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}}
>
@@ -220,52 +285,35 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}
helpText={
selectIndicesMode === 'list' ? (
- 0 ? (
- {
- // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
- indicesOptions.forEach((option: EuiSelectableOption) => {
- option.checked = undefined;
- });
- updateRestoreSettings({ indices: [] });
- setCachedRestoreSettings({
- ...cachedRestoreSettings,
- indices: [],
- });
- }}
- >
-
-
- ) : (
- {
- // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
- indicesOptions.forEach((option: EuiSelectableOption) => {
- option.checked = 'on';
- });
- updateRestoreSettings({ indices: [...snapshotIndices] });
- setCachedRestoreSettings({
- ...cachedRestoreSettings,
- indices: [...snapshotIndices],
- });
- }}
- >
-
-
- ),
+ {
+ if (selection === 'all') {
+ // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
+ indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => {
+ option.checked = 'on';
+ });
+ updateRestoreSettings({
+ indices: [...snapshotIndicesAndDataStreams],
+ });
+ setCachedRestoreSettings({
+ ...cachedRestoreSettings,
+ indices: [...snapshotIndicesAndDataStreams],
+ });
+ } else {
+ // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
+ indicesAndDataStreamsOptions.forEach((option: EuiSelectableOption) => {
+ option.checked = undefined;
+ });
+ updateRestoreSettings({ indices: [] });
+ setCachedRestoreSettings({
+ ...cachedRestoreSettings,
+ indices: [],
+ });
+ }
}}
+ selectedIndicesAndDataStreams={csvToArray(restoreIndices)}
+ indices={snapshotIndices}
+ dataStreams={snapshotDataStreams}
/>
) : null
}
@@ -275,7 +323,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
{selectIndicesMode === 'list' ? (
{
const newSelectedIndices: string[] = [];
options.forEach(({ label, checked }) => {
@@ -283,7 +331,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
newSelectedIndices.push(label);
}
});
- setIndicesOptions(options);
+ setIndicesAndDataStreamsOptions(options);
updateRestoreSettings({ indices: [...newSelectedIndices] });
setCachedRestoreSettings({
...cachedRestoreSettings,
@@ -302,7 +350,24 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
) : (
({ label: index }))}
+ options={comboBoxOptions}
+ renderOption={({ value }) => {
+ return value?.isDataStream ? (
+
+ {value.name}
+
+
+
+
+ ) : (
+ value?.name
+ );
+ }}
placeholder={i18n.translate(
'xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder',
{
@@ -336,22 +401,22 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
- {/* Rename indices */}
+ {/* Rename data streams and indices */}
}
description={
}
fullWidth
@@ -361,8 +426,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
}
checked={isRenamingIndices}
@@ -405,7 +470,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
>
{
setCachedRestoreSettings({
...cachedRestoreSettings,
@@ -431,7 +496,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent =
>
{
setCachedRestoreSettings({
...cachedRestoreSettings,
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx
index 27a3717566d93..5dacba506fe18 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_review.tsx
@@ -24,7 +24,7 @@ import {
import { serializeRestoreSettings } from '../../../../../common/lib';
import { useServices } from '../../../app_context';
import { StepProps } from './';
-import { CollapsibleIndicesList } from '../../collapsible_indices_list';
+import { CollapsibleIndicesList } from '../../collapsible_lists/collapsible_indices_list';
export const RestoreSnapshotStepReview: React.FunctionComponent = ({
restoreSettings,
@@ -73,8 +73,8 @@ export const RestoreSnapshotStepReview: React.FunctionComponent = ({
diff --git a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx
index 5f3ebf804c5e1..b9a2d7e4b7cd9 100644
--- a/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/components/restore_snapshot_form/steps/step_settings.tsx
@@ -18,6 +18,7 @@ import {
EuiSwitch,
EuiTitle,
EuiLink,
+ EuiCallOut,
} from '@elastic/eui';
import { RestoreSettings } from '../../../../../common/types';
import { REMOVE_INDEX_SETTINGS_SUGGESTIONS } from '../../../constants';
@@ -28,10 +29,12 @@ import { StepProps } from './';
export const RestoreSnapshotStepSettings: React.FunctionComponent = ({
restoreSettings,
updateRestoreSettings,
+ snapshotDetails,
errors,
}) => {
const { i18n } = useServices();
const { indexSettings, ignoreIndexSettings } = restoreSettings;
+ const { dataStreams } = snapshotDetails;
// State for index setting toggles
const [isUsingIndexSettings, setIsUsingIndexSettings] = useState(Boolean(indexSettings));
@@ -96,6 +99,23 @@ export const RestoreSnapshotStepSettings: React.FunctionComponent = (
+ {dataStreams?.length ? (
+ <>
+
+
+
+
+ >
+ ) : undefined}
{/* Modify index settings */}
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
index 7bcee4f5f6621..e69b0fad8014e 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/tabs/tab_summary.tsx
@@ -236,8 +236,8 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => {
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
index 287a77493307d..1a0c26c854490 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/tabs/tab_summary.tsx
@@ -22,6 +22,7 @@ import {
DataPlaceholder,
FormattedDateTime,
CollapsibleIndicesList,
+ CollapsibleDataStreamsList,
} from '../../../../../components';
import { linkToPolicy } from '../../../../../services/navigation';
import { SnapshotState } from './snapshot_state';
@@ -40,6 +41,7 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => {
// TODO: Add a tooltip explaining that: a false value means that the cluster global state
// is not stored as part of the snapshot.
includeGlobalState,
+ dataStreams,
indices,
state,
startTimeInMillis,
@@ -135,6 +137,22 @@ export const TabSummary: React.FC = ({ snapshotDetails }) => {
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
index 6d1a432be7f9f..90cd26c821c5e 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx
@@ -25,13 +25,8 @@ export const PolicyAdd: React.FunctionComponent = ({
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(null);
- const {
- error: errorLoadingIndices,
- isLoading: isLoadingIndices,
- data: { indices } = {
- indices: [],
- },
- } = useLoadIndices();
+ const { error: errorLoadingIndices, isLoading: isLoadingIndices, data } = useLoadIndices();
+ const { indices, dataStreams } = data ?? { indices: [], dataStreams: [] };
// Set breadcrumb and page title
useEffect(() => {
@@ -123,6 +118,7 @@ export const PolicyAdd: React.FunctionComponent = ({
{
};
export const useLoadIndices = () => {
- return useRequest({
+ return useRequest({
path: `${API_BASE_PATH}policies/indices`,
method: 'get',
});
diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts
index 27a565ccb74bc..b4d0493098bbc 100644
--- a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts
@@ -18,6 +18,6 @@ export const sendRequest = (config: SendRequestConfig) => {
return _sendRequest(httpService.httpClient, config);
};
-export const useRequest = (config: UseRequestConfig) => {
- return _useRequest(httpService.httpClient, config);
+export const useRequest = (config: UseRequestConfig) => {
+ return _useRequest(httpService.httpClient, config);
};
diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts
index 0720994ca7669..24960b2533230 100644
--- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts
@@ -48,6 +48,7 @@ export const validatePolicy = (
snapshotName: [],
schedule: [],
repository: [],
+ dataStreams: [],
indices: [],
expireAfterValue: [],
minCount: [],
@@ -106,7 +107,7 @@ export const validatePolicy = (
if (config && Array.isArray(config.indices) && config.indices.length === 0) {
validation.errors.indices.push(
i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredErrorMessage', {
- defaultMessage: 'You must select at least one index.',
+ defaultMessage: 'You must select at least one data stream or index.',
})
);
}
diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts
index 5c1a1fbfab12d..93e278e51f093 100644
--- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts
+++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_restore.ts
@@ -48,7 +48,7 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida
if (Array.isArray(indices) && indices.length === 0) {
validation.errors.indices.push(
i18n.translate('xpack.snapshotRestore.restoreValidation.indicesRequiredError', {
- defaultMessage: 'You must select at least one index.',
+ defaultMessage: 'You must select at least one data stream or index.',
})
);
}
@@ -93,7 +93,6 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida
'xpack.snapshotRestore.restoreValidation.indexSettingsNotModifiableError',
{
defaultMessage: 'You can’t modify: {settings}',
- // @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299
values: {
settings: unmodifiableSettings.map((setting: string, index: number) =>
index === 0 ? `${setting} ` : setting
@@ -131,7 +130,6 @@ export const validateRestore = (restoreSettings: RestoreSettings): RestoreValida
validation.errors.ignoreIndexSettings.push(
i18n.translate('xpack.snapshotRestore.restoreValidation.indexSettingsNotRemovableError', {
defaultMessage: 'You can’t reset: {settings}',
- // @ts-ignore Bug filed: https://github.com/elastic/kibana/issues/39299
values: {
settings: unremovableSettings.map((setting: string, index: number) =>
index === 0 ? `${setting} ` : setting
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts
index eb29b7bad37e6..b96d305fa4a87 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts
@@ -6,6 +6,7 @@
import { addBasePath } from '../helpers';
import { registerPolicyRoutes } from './policy';
import { RouterMock, routeDependencies, RequestMock } from '../../test/helpers';
+import { ResolveIndexResponseFromES } from '../../types';
describe('[Snapshot and Restore API Routes] Policy', () => {
const mockEsPolicy = {
@@ -324,27 +325,45 @@ describe('[Snapshot and Restore API Routes] Policy', () => {
};
it('should arrify and sort index names returned from ES', async () => {
- const mockEsResponse = [
- {
- index: 'fooIndex',
- },
- {
- index: 'barIndex',
- },
- ];
+ const mockEsResponse: ResolveIndexResponseFromES = {
+ indices: [
+ {
+ name: 'fooIndex',
+ attributes: ['open'],
+ },
+ {
+ name: 'barIndex',
+ attributes: ['open'],
+ data_stream: 'testDataStream',
+ },
+ ],
+ aliases: [],
+ data_streams: [
+ {
+ name: 'testDataStream',
+ backing_indices: ['barIndex'],
+ timestamp_field: '@timestamp',
+ },
+ ],
+ };
router.callAsCurrentUserResponses = [mockEsResponse];
const expectedResponse = {
- indices: ['barIndex', 'fooIndex'],
+ indices: ['fooIndex'],
+ dataStreams: ['testDataStream'],
};
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
});
it('should return empty array if no indices returned from ES', async () => {
- const mockEsResponse: any[] = [];
+ const mockEsResponse: ResolveIndexResponseFromES = {
+ indices: [],
+ aliases: [],
+ data_streams: [],
+ };
router.callAsCurrentUserResponses = [mockEsResponse];
- const expectedResponse = { indices: [] };
+ const expectedResponse = { indices: [], dataStreams: [] };
await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse });
});
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts
index 90667eda23b35..b8e7012529554 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts
@@ -5,10 +5,10 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
-import { SlmPolicyEs } from '../../../common/types';
+import { SlmPolicyEs, PolicyIndicesResponse } from '../../../common/types';
import { deserializePolicy, serializePolicy } from '../../../common/lib';
import { getManagedPolicyNames } from '../../lib';
-import { RouteDependencies } from '../../types';
+import { RouteDependencies, ResolveIndexResponseFromES } from '../../types';
import { addBasePath } from '../helpers';
import { nameParameterSchema, policySchema } from './validate_schemas';
@@ -232,17 +232,26 @@ export function registerPolicyRoutes({
const { callAsCurrentUser } = ctx.snapshotRestore!.client;
try {
- const indices: Array<{
- index: string;
- }> = await callAsCurrentUser('cat.indices', {
- format: 'json',
- h: 'index',
- });
+ const resolvedIndicesResponse: ResolveIndexResponseFromES = await callAsCurrentUser(
+ 'transport.request',
+ {
+ method: 'GET',
+ path: `_resolve/index/*`,
+ query: {
+ expand_wildcards: 'all,hidden',
+ },
+ }
+ );
+
+ const body: PolicyIndicesResponse = {
+ dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(),
+ indices: resolvedIndicesResponse.indices
+ .flatMap((index) => (index.data_stream ? [] : index.name))
+ .sort(),
+ };
return res.ok({
- body: {
- indices: indices.map(({ index }) => index).sort(),
- },
+ body,
});
} catch (e) {
if (isEsError(e)) {
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
index f913299fc3992..a7e61d1e7c02a 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts
@@ -15,6 +15,7 @@ const defaultSnapshot = {
versionId: undefined,
version: undefined,
indices: [],
+ dataStreams: [],
includeGlobalState: undefined,
state: undefined,
startTime: undefined,
diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts
index 7794156eb1b88..8cfcaec1a2cd1 100644
--- a/x-pack/plugins/snapshot_restore/server/types.ts
+++ b/x-pack/plugins/snapshot_restore/server/types.ts
@@ -31,4 +31,20 @@ export interface RouteDependencies {
};
}
+/**
+ * An object representing a resolved index, data stream or alias
+ */
+interface IndexAndAliasFromEs {
+ name: string;
+ // per https://github.com/elastic/elasticsearch/pull/57626
+ attributes: Array<'open' | 'closed' | 'hidden' | 'frozen'>;
+ data_stream?: string;
+}
+
+export interface ResolveIndexResponseFromES {
+ indices: IndexAndAliasFromEs[];
+ aliases: IndexAndAliasFromEs[];
+ data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>;
+}
+
export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser'];
diff --git a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts
index d6a55579b322d..e59f4689d9e3f 100644
--- a/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts
+++ b/x-pack/plugins/snapshot_restore/test/fixtures/snapshot.ts
@@ -13,13 +13,23 @@ export const getSnapshot = ({
state = 'SUCCESS',
indexFailures = [],
totalIndices = getRandomNumber(),
-} = {}) => ({
+ totalDataStreams = getRandomNumber(),
+}: Partial<{
+ repository: string;
+ snapshot: string;
+ uuid: string;
+ state: string;
+ indexFailures: any[];
+ totalIndices: number;
+ totalDataStreams: number;
+}> = {}) => ({
repository,
snapshot,
uuid,
versionId: 8000099,
version: '8.0.0',
indices: new Array(totalIndices).fill('').map(getRandomString),
+ dataStreams: new Array(totalDataStreams).fill('').map(getRandomString),
includeGlobalState: 1,
state,
startTime: '2019-05-23T06:25:15.896Z',
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index b5cdad5583e1d..4c1572ddfcad1 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -13387,7 +13387,6 @@
"xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel": "いいえ",
"xpack.snapshotRestore.policyDetails.includeGlobalStateLabel": "グローバルステータスを含める",
"xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel": "はい",
- "xpack.snapshotRestore.policyDetails.indicesLabel": "インデックス",
"xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText": "「{snapshotName}」が進行中",
"xpack.snapshotRestore.policyDetails.lastFailure.dateLabel": "日付",
"xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel": "ポリシー「{name}」の前回のエラーの詳細",
@@ -13495,10 +13494,8 @@
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel": "いいえ",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel": "グローバルステータスを含める",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel": "はい",
- "xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel": "インデックス",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel": "ポリシー名",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel": "いいえ",
- "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel": "部分シャードを許可",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel": "はい",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel": "レポジトリ",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel": "スケジュール",
@@ -13507,7 +13504,6 @@
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel": "スナップショット名",
"xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle": "まとめ",
"xpack.snapshotRestore.policyForm.stepReviewTitle": "レビューポリシー",
- "xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel": "システムインデックスを含むすべてのインデックス",
"xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink": "すべて選択解除",
"xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel": "スナップショット設定ドキュメント",
"xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription": "スナップショットの撮影時に利用不可能なインデックスを無視します。これが設定されていない場合、スナップショット全体がエラーになります。",
@@ -13515,19 +13511,15 @@
"xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel": "利用不可能なインデックスを無視",
"xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription": "スナップショットの一部としてクラスターのグローバルステータスを格納します。",
"xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle": "グローバルステータスを含める",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesDescription": "バックアップするインデックスです。",
"xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel": "インデックスパターン",
"xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder": "logstash-* などのインデックスパターンを入力",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesTitle": "インデックス",
"xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink": "インデックスパターンを使用",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "インデックスを選択",
"xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip": "クラウドで管理されたポリシーにはすべてのインデックスが必要です。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "利用不可能なプライマリシャードのインデックスのスナップショットを許可します。これが設定されていない場合、スナップショット全体がエラーになります。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "部分インデックスを許可",
"xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch": "部分インデックスを許可",
"xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "グローバルステータスを含める",
"xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "すべて選択",
- "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}がバックアップされます。{selectOrDeselectAllLink}",
"xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel": "インデックスを選択",
"xpack.snapshotRestore.policyForm.stepSettingsTitle": "スナップショット設定",
"xpack.snapshotRestore.policyList.deniedPrivilegeDescription": "スナップショットライフサイクルポリシーを管理するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です: {missingPrivileges}。",
@@ -13874,31 +13866,22 @@
"xpack.snapshotRestore.restoreForm.navigation.stepSettingsName": "インデックス設定",
"xpack.snapshotRestore.restoreForm.nextButtonLabel": "次へ",
"xpack.snapshotRestore.restoreForm.savingButtonLabel": "復元中...",
- "xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel": "システムインデックスを含むすべてのインデックス",
"xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "すべて選択解除",
"xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "スナップショットと復元ドキュメント",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "現在クラスターに存在しないテンプレートを復元し、テンプレートを同じ名前で上書きします。永続的な設定も復元します。",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "このスナップショットでは使用できません。",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "グローバル状態の復元",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "グローバル状態の復元",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription": "存在しない場合は、新しいインデックスを作成します。閉じていて、スナップショットインデックスと同じ数のシャードがある場合は、既存のインデックスを復元します。",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "インデックスパターン",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder": "logstash-* などのインデックスパターンを入力",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle": "インデックス",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleCustomLink": "インデックスパターンを使用",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink": "インデックスを選択",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription": "すべてのシャードのスナップショットがないインデックスを復元できます。",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel": "部分復元",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle": "部分復元",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription": "復元時にインデックス名を変更します。",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel": "インデックス名の変更",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle": "インデックス名の変更",
"xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText": "正規表現を使用",
"xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel": "取り込みパターン",
"xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel": "置換パターン",
"xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "すべて選択",
- "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText": "{count} 件の{count, plural, one {インデックス} other {インデックス}}が復元されます。{selectOrDeselectAllLink}",
- "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel": "インデックスを選択",
"xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "詳細を復元",
"xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "実行する設定を復元",
"xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON",
@@ -13908,7 +13891,6 @@
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel": "グローバル状態の復元",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue": "はい",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel": "修正",
- "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel": "インデックス",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue": "インデックス設定の修正はありません",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue": "いいえ",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel": "部分復元",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 568397912141d..97f10e77dc717 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -13392,7 +13392,6 @@
"xpack.snapshotRestore.policyDetails.includeGlobalStateFalseLabel": "否",
"xpack.snapshotRestore.policyDetails.includeGlobalStateLabel": "包括全局状态",
"xpack.snapshotRestore.policyDetails.includeGlobalStateTrueLabel": "是",
- "xpack.snapshotRestore.policyDetails.indicesLabel": "索引",
"xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText": "“{snapshotName}”正在进行中",
"xpack.snapshotRestore.policyDetails.lastFailure.dateLabel": "日期",
"xpack.snapshotRestore.policyDetails.lastFailure.detailsAriaLabel": "策略“{name}”的上次失败详情",
@@ -13500,10 +13499,8 @@
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel": "否",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel": "包括全局状态",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel": "是",
- "xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel": "索引",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel": "策略名称",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel": "否",
- "xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel": "允许部分分片",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel": "是",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel": "存储库",
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel": "计划",
@@ -13512,7 +13509,6 @@
"xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel": "快照名称",
"xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle": "总结",
"xpack.snapshotRestore.policyForm.stepReviewTitle": "复查策略",
- "xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel": "所有索引,包括系统索引",
"xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink": "取消全选",
"xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel": "快照设置文档",
"xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription": "拍取快照时忽略不可用的索引。否则,整个快照将失败。",
@@ -13520,19 +13516,15 @@
"xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel": "忽略不可用索引",
"xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription": "将集群的全局状态存储为快照的一部分。",
"xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle": "包括全局状态",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesDescription": "要备份的索引。",
"xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel": "索引模式",
"xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder": "输入索引模式,例如 logstash-*",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesTitle": "索引",
"xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink": "使用索引模式",
- "xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink": "选择索引",
"xpack.snapshotRestore.policyForm.stepSettings.indicesTooltip": "云托管的策略需要所有索引。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescription": "允许具有不可用主分片的索引的快照。否则,整个快照将失败。",
"xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle": "允许部分索引",
"xpack.snapshotRestore.policyForm.stepSettings.partialIndicesToggleSwitch": "允许部分索引",
"xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel": "包括全局状态",
"xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink": "全选",
- "xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText": "将备份 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}",
"xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel": "选择索引",
"xpack.snapshotRestore.policyForm.stepSettingsTitle": "快照设置",
"xpack.snapshotRestore.policyList.deniedPrivilegeDescription": "要管理快照生命周期策略,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。",
@@ -13879,31 +13871,22 @@
"xpack.snapshotRestore.restoreForm.navigation.stepSettingsName": "索引设置",
"xpack.snapshotRestore.restoreForm.nextButtonLabel": "下一步",
"xpack.snapshotRestore.restoreForm.savingButtonLabel": "正在还原……",
- "xpack.snapshotRestore.restoreForm.stepLogistics.allIndicesLabel": "所有索引,包括系统索引",
"xpack.snapshotRestore.restoreForm.stepLogistics.deselectAllIndicesLink": "取消全选",
"xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel": "快照和还原文档",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDescription": "还原当前在集群中不存在的模板并覆盖同名模板。同时还原永久性设置。",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateDisabledDescription": "不适用于此快照。",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateLabel": "还原全局状态",
"xpack.snapshotRestore.restoreForm.stepLogistics.includeGlobalStateTitle": "还原全局状态",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesDescription": "如果不存在,则创建新索引。如果现有索引已关闭且与快照索引有相同数目的分片,则还原现有索引。",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternLabel": "索引模式",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesPatternPlaceholder": "输入索引模式,例如 logstash-*",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesTitle": "索引",
"xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleCustomLink": "使用索引模式",
- "xpack.snapshotRestore.restoreForm.stepLogistics.indicesToggleListLink": "选择索引",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialDescription": "允许还原不具有所有分片的快照的索引。",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialLabel": "部分还原",
"xpack.snapshotRestore.restoreForm.stepLogistics.partialTitle": "部分还原",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesDescription": "还原时重命名索引。",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesLabel": "重命名索引",
- "xpack.snapshotRestore.restoreForm.stepLogistics.renameIndicesTitle": "重命名索引",
"xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternHelpText": "使用正则表达式",
"xpack.snapshotRestore.restoreForm.stepLogistics.renamePatternLabel": "捕获模式",
"xpack.snapshotRestore.restoreForm.stepLogistics.renameReplacementLabel": "替换模式",
"xpack.snapshotRestore.restoreForm.stepLogistics.selectAllIndicesLink": "全选",
- "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesHelpText": "将还原 {count} 个 {count, plural, one {索引} other {索引}}。{selectOrDeselectAllLink}",
- "xpack.snapshotRestore.restoreForm.stepLogistics.selectIndicesLabel": "选择索引",
"xpack.snapshotRestore.restoreForm.stepLogisticsTitle": "还原详情",
"xpack.snapshotRestore.restoreForm.stepReview.jsonTab.jsonAriaLabel": "还原要执行的设置",
"xpack.snapshotRestore.restoreForm.stepReview.jsonTabTitle": "JSON",
@@ -13913,7 +13896,6 @@
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateLabel": "还原全局状态",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.includeGlobalStateTrueValue": "鏄",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indexSettingsLabel": "修改",
- "xpack.snapshotRestore.restoreForm.stepReview.summaryTab.indicesLabel": "索引",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.noSettingsValue": "无索引设置修改",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialFalseValue": "否",
"xpack.snapshotRestore.restoreForm.stepReview.summaryTab.partialLabel": "部分还原",
From c6867197ffc2808b8127d25a796c4fd21b51fd5e Mon Sep 17 00:00:00 2001
From: Rudolf Meijering
Date: Thu, 2 Jul 2020 15:50:43 +0200
Subject: [PATCH 09/18] Allow Saved Object type mappings to set a field's
`doc_values` property (#70433)
* Allow doc_values to be disabled
* Make doc_values optional
* doc_values type for CoreFieldMapping
* doc_values not doc_value
* Update docs
Co-authored-by: Elastic Machine
---
...rver.savedobjectscomplexfieldmapping.doc_values.md | 11 +++++++++++
...gin-core-server.savedobjectscomplexfieldmapping.md | 1 +
...-server.savedobjectscorefieldmapping.doc_values.md | 11 +++++++++++
...plugin-core-server.savedobjectscorefieldmapping.md | 1 +
src/core/server/saved_objects/mappings/types.ts | 2 ++
src/core/server/server.api.md | 4 ++++
6 files changed, 30 insertions(+)
create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md
create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md
new file mode 100644
index 0000000000000..3f2d81cc97c7c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [doc\_values](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md)
+
+## SavedObjectsComplexFieldMapping.doc\_values property
+
+Signature:
+
+```typescript
+doc_values?: boolean;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
index a7d13b0015e3f..cb81686b424ec 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
@@ -18,6 +18,7 @@ export interface SavedObjectsComplexFieldMapping
| Property | Type | Description |
| --- | --- | --- |
+| [doc\_values](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md) | boolean
| |
| [properties](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.properties.md) | SavedObjectsMappingProperties
| |
| [type](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.type.md) | string
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md
new file mode 100644
index 0000000000000..2a79eafd85a6c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) > [doc\_values](./kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md)
+
+## SavedObjectsCoreFieldMapping.doc\_values property
+
+Signature:
+
+```typescript
+doc_values?: boolean;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
index 9a31d37b3ff30..b9e726eac799d 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
@@ -16,6 +16,7 @@ export interface SavedObjectsCoreFieldMapping
| Property | Type | Description |
| --- | --- | --- |
+| [doc\_values](./kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md) | boolean
| |
| [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) | boolean
| |
| [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
ignore_above?: number;
};
}
| |
| [index](./kibana-plugin-core-server.savedobjectscorefieldmapping.index.md) | boolean
| |
diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts
index c037ed733549e..7521e4a4bee86 100644
--- a/src/core/server/saved_objects/mappings/types.ts
+++ b/src/core/server/saved_objects/mappings/types.ts
@@ -133,6 +133,7 @@ export interface SavedObjectsCoreFieldMapping {
type: string;
null_value?: number | boolean | string;
index?: boolean;
+ doc_values?: boolean;
enabled?: boolean;
fields?: {
[subfield: string]: {
@@ -153,6 +154,7 @@ export interface SavedObjectsCoreFieldMapping {
* @public
*/
export interface SavedObjectsComplexFieldMapping {
+ doc_values?: boolean;
type?: string;
properties: SavedObjectsMappingProperties;
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 1cabaa57e519c..cb413be2c19b8 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -1978,6 +1978,8 @@ export interface SavedObjectsClientWrapperOptions {
// @public
export interface SavedObjectsComplexFieldMapping {
+ // (undocumented)
+ doc_values?: boolean;
// (undocumented)
properties: SavedObjectsMappingProperties;
// (undocumented)
@@ -1986,6 +1988,8 @@ export interface SavedObjectsComplexFieldMapping {
// @public
export interface SavedObjectsCoreFieldMapping {
+ // (undocumented)
+ doc_values?: boolean;
// (undocumented)
enabled?: boolean;
// (undocumented)
From f8ba824ebc7e3d551b791ba7b8c04f31cddcaf10 Mon Sep 17 00:00:00 2001
From: Nick Partridge
Date: Thu, 2 Jul 2020 09:02:30 -0500
Subject: [PATCH 10/18] Fix discover, tsvb and Lens chart theming issues
(#69695)
---
package.json | 2 +-
packages/kbn-ui-shared-deps/package.json | 2 +-
src/plugins/charts/README.md | 72 +--------------
.../charts/public/services/theme/README.md | 92 +++++++++++++++++++
.../charts/public/services/theme/mock.ts | 14 ++-
.../public/services/theme/theme.test.tsx | 64 ++++++++++++-
.../charts/public/services/theme/theme.ts | 68 +++++++++++---
.../angular/directives/histogram.tsx | 16 +++-
.../components/vis_types/_vis_types.scss | 6 +-
.../components/vis_types/timeseries/vis.js | 6 +-
.../visualizations/views/timeseries/index.js | 23 +++--
.../views/timeseries/utils/theme.test.ts | 20 ++--
.../views/timeseries/utils/theme.ts | 10 +-
x-pack/plugins/lens/kibana.json | 3 +-
.../datapanel.test.tsx | 2 +
.../indexpattern_datasource/datapanel.tsx | 7 ++
.../field_item.test.tsx | 4 +
.../indexpattern_datasource/field_item.tsx | 16 +++-
.../fields_accordion.test.tsx | 2 +
.../fields_accordion.tsx | 2 +
.../public/indexpattern_datasource/index.ts | 5 +-
.../indexpattern.test.ts | 2 +
.../indexpattern_datasource/indexpattern.tsx | 4 +
.../lens/public/pie_visualization/index.ts | 10 +-
.../pie_visualization/register_expression.tsx | 8 +-
.../render_function.test.tsx | 6 +-
.../pie_visualization/render_function.tsx | 12 ++-
x-pack/plugins/lens/public/plugin.ts | 34 +++++--
.../__snapshots__/xy_expression.test.tsx.snap | 7 ++
.../lens/public/xy_visualization/index.ts | 9 +-
.../xy_visualization/xy_expression.test.tsx | 59 ++++++------
.../public/xy_visualization/xy_expression.tsx | 13 ++-
.../threshold/visualization.tsx | 5 +-
yarn.lock | 31 ++++++-
34 files changed, 439 insertions(+), 197 deletions(-)
create mode 100644 src/plugins/charts/public/services/theme/README.md
diff --git a/package.json b/package.json
index b520be4df6969..b1dd8686f818b 100644
--- a/package.json
+++ b/package.json
@@ -122,7 +122,7 @@
"@babel/plugin-transform-modules-commonjs": "^7.10.1",
"@babel/register": "^7.10.1",
"@elastic/apm-rum": "^5.2.0",
- "@elastic/charts": "19.5.2",
+ "@elastic/charts": "19.6.3",
"@elastic/datemath": "5.0.3",
"@elastic/ems-client": "7.9.3",
"@elastic/eui": "24.1.0",
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 0e3bb235c3d9f..ff09d8d4fc5ab 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -9,7 +9,7 @@
"kbn:watch": "node scripts/build --dev --watch"
},
"dependencies": {
- "@elastic/charts": "19.5.2",
+ "@elastic/charts": "19.6.3",
"@elastic/eui": "24.1.0",
"@elastic/numeral": "^2.5.0",
"@kbn/i18n": "1.0.0",
diff --git a/src/plugins/charts/README.md b/src/plugins/charts/README.md
index 319da67981aa9..31727b7acb7a1 100644
--- a/src/plugins/charts/README.md
+++ b/src/plugins/charts/README.md
@@ -18,7 +18,7 @@ Color mappings in `value`/`text` form
### `getHeatmapColors`
-Funciton to retrive heatmap related colors based on `value` and `colorSchemaName`
+Function to retrieve heatmap related colors based on `value` and `colorSchemaName`
### `truncatedColorSchemas`
@@ -26,72 +26,4 @@ Truncated color mappings in `value`/`text` form
## Theme
-the `theme` service offers utilities to interact with theme of kibana. EUI provides a light and dark theme object to work with Elastic-Charts. However, every instance of a Chart would need to pass down this the correctly EUI theme depending on Kibana's light or dark mode. There are several ways you can use the `theme` service to get the correct theme.
-
-> The current theme (light or dark) of Kibana is typically taken into account for the functions below.
-
-### `useChartsTheme`
-
-The simple fetching of the correct EUI theme; a **React hook**.
-
-```js
-import { npStart } from 'ui/new_platform';
-import { Chart, Settings } from '@elastic/charts';
-
-export const YourComponent = () => (
-
-
-
-);
-```
-
-### `chartsTheme$`
-
-An **observable** of the current charts theme. Use this implementation for more flexible updates to the chart theme without full page refreshes.
-
-```tsx
-import { npStart } from 'ui/new_platform';
-import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes';
-import { Subscription } from 'rxjs';
-import { Chart, Settings } from '@elastic/charts';
-
-interface YourComponentProps {};
-
-interface YourComponentState {
- chartsTheme: EuiChartThemeType['theme'];
-}
-
-export class YourComponent extends Component {
- private subscription?: Subscription;
- public state = {
- chartsTheme: npStart.plugins.charts.theme.chartsDefaultTheme,
- };
-
- componentDidMount() {
- this.subscription = npStart.plugins.charts.theme
- .chartsTheme$
- .subscribe(chartsTheme => this.setState({ chartsTheme }));
- }
-
- componentWillUnmount() {
- if (this.subscription) {
- this.subscription.unsubscribe();
- this.subscription = undefined;
- }
- }
-
- public render() {
- const { chartsTheme } = this.state;
-
- return (
-
-
-
- );
- }
-}
-```
-
-### `chartsDefaultTheme`
-
-Returns default charts theme (i.e. light).
+See Theme service [docs](public/services/theme/README.md)
diff --git a/src/plugins/charts/public/services/theme/README.md b/src/plugins/charts/public/services/theme/README.md
new file mode 100644
index 0000000000000..fb4f941f79344
--- /dev/null
+++ b/src/plugins/charts/public/services/theme/README.md
@@ -0,0 +1,92 @@
+# Theme Service
+
+The `theme` service offers utilities to interact with the kibana theme. EUI provides a light and dark theme object to supplement the Elastic-Charts `baseTheme`. However, every instance of a Chart would need to pass down the correct EUI theme depending on Kibana's light or dark mode. There are several ways you can use the `theme` service to get the correct shared `theme` and `baseTheme`.
+
+> The current theme (light or dark) of Kibana is typically taken into account for the functions below.
+
+## `chartsDefaultBaseTheme`
+
+Default `baseTheme` from `@elastic/charts` (i.e. light).
+
+## `chartsDefaultTheme`
+
+Default `theme` from `@elastic/eui` (i.e. light).
+
+## `useChartsTheme` and `useChartsBaseTheme`
+
+A **React hook** for simple fetching of the correct EUI `theme` and `baseTheme`.
+
+```js
+import { npStart } from 'ui/new_platform';
+import { Chart, Settings } from '@elastic/charts';
+
+export const YourComponent = () => (
+
+
+ {/* ... */}
+
+);
+```
+
+## `chartsTheme$` and `chartsBaseTheme$`
+
+An **`Observable`** of the current charts `theme` and `baseTheme`. Use this implementation for more flexible updates to the chart theme without full page refreshes.
+
+```tsx
+import { npStart } from 'ui/new_platform';
+import { EuiChartThemeType } from '@elastic/eui/src/themes/charts/themes';
+import { Subscription, combineLatest } from 'rxjs';
+import { Chart, Settings, Theme } from '@elastic/charts';
+
+interface YourComponentProps {};
+
+interface YourComponentState {
+ chartsTheme: EuiChartThemeType['theme'];
+ chartsBaseTheme: Theme;
+}
+
+export class YourComponent extends Component {
+ private subscriptions: Subscription[] = [];
+
+ public state = {
+ chartsTheme: npStart.plugins.charts.theme.chartsDefaultTheme,
+ chartsBaseTheme: npStart.plugins.charts.theme.chartsDefaultBaseTheme,
+ };
+
+ componentDidMount() {
+ this.subscription = combineLatest(
+ npStart.plugins.charts.theme.chartsTheme$,
+ npStart.plugins.charts.theme.chartsBaseTheme$
+ ).subscribe(([chartsTheme, chartsBaseTheme]) =>
+ this.setState({ chartsTheme, chartsBaseTheme })
+ );
+ }
+
+ componentWillUnmount() {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ public render() {
+ const { chartsBaseTheme, chartsTheme } = this.state;
+
+ return (
+
+
+ {/* ... */}
+
+ );
+ }
+}
+```
+
+## Why have `theme` and `baseTheme`?
+
+The `theme` prop is a recursive partial `Theme` that overrides properties from the `baseTheme`. This allows changes to the `Theme` TS type in `@elastic/charts` without having to update the `@elastic/eui` themes for every ``.
diff --git a/src/plugins/charts/public/services/theme/mock.ts b/src/plugins/charts/public/services/theme/mock.ts
index 8aa1a4f2368ac..7fecb862a3c65 100644
--- a/src/plugins/charts/public/services/theme/mock.ts
+++ b/src/plugins/charts/public/services/theme/mock.ts
@@ -21,9 +21,17 @@ import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { ThemeService } from './theme';
export const themeServiceMock: ThemeService = {
+ chartsDefaultTheme: EUI_CHARTS_THEME_LIGHT.theme,
chartsTheme$: jest.fn(() => ({
- subsribe: jest.fn(),
+ subscribe: jest.fn(),
})),
- chartsDefaultTheme: EUI_CHARTS_THEME_LIGHT.theme,
- useChartsTheme: jest.fn(),
+ chartsBaseTheme$: jest.fn(() => ({
+ subscribe: jest.fn(),
+ })),
+ darkModeEnabled$: jest.fn(() => ({
+ subscribe: jest.fn(),
+ })),
+ useDarkMode: jest.fn().mockReturnValue(false),
+ useChartsTheme: jest.fn().mockReturnValue({}),
+ useChartsBaseTheme: jest.fn().mockReturnValue({}),
} as any;
diff --git a/src/plugins/charts/public/services/theme/theme.test.tsx b/src/plugins/charts/public/services/theme/theme.test.tsx
index fca503e387ea2..52bc78dfec7df 100644
--- a/src/plugins/charts/public/services/theme/theme.test.tsx
+++ b/src/plugins/charts/public/services/theme/theme.test.tsx
@@ -25,15 +25,35 @@ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist
import { ThemeService } from './theme';
import { coreMock } from '../../../../../core/public/mocks';
+import { LIGHT_THEME, DARK_THEME } from '@elastic/charts';
const { uiSettings: setupMockUiSettings } = coreMock.createSetup();
describe('ThemeService', () => {
- describe('chartsTheme$', () => {
+ describe('darkModeEnabled$', () => {
it('should throw error if service has not been initialized', () => {
const themeService = new ThemeService();
- expect(() => themeService.chartsTheme$).toThrowError();
+ expect(() => themeService.darkModeEnabled$).toThrowError();
+ });
+
+ it('returns the false when not in dark mode', async () => {
+ setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false));
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+
+ expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toBe(false);
+ });
+
+ it('returns the true when in dark mode', async () => {
+ setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(true));
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+
+ expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toBe(true);
});
+ });
+
+ describe('chartsTheme$', () => {
it('returns the light theme when not in dark mode', async () => {
setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false));
const themeService = new ThemeService();
@@ -58,6 +78,28 @@ describe('ThemeService', () => {
});
});
+ describe('chartsBaseTheme$', () => {
+ it('returns the light theme when not in dark mode', async () => {
+ setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(false));
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+
+ expect(await themeService.chartsBaseTheme$.pipe(take(1)).toPromise()).toEqual(LIGHT_THEME);
+ });
+
+ describe('in dark mode', () => {
+ it(`returns the dark theme`, async () => {
+ // Fake dark theme turned returning true
+ setupMockUiSettings.get$.mockReturnValue(new BehaviorSubject(true));
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+ const result = await themeService.chartsBaseTheme$.pipe(take(1)).toPromise();
+
+ expect(result).toEqual(DARK_THEME);
+ });
+ });
+ });
+
describe('useChartsTheme', () => {
it('updates when the uiSettings change', () => {
const darkMode$ = new BehaviorSubject(false);
@@ -75,4 +117,22 @@ describe('ThemeService', () => {
expect(result.current).toBe(EUI_CHARTS_THEME_LIGHT.theme);
});
});
+
+ describe('useBaseChartTheme', () => {
+ it('updates when the uiSettings change', () => {
+ const darkMode$ = new BehaviorSubject(false);
+ setupMockUiSettings.get$.mockReturnValue(darkMode$);
+ const themeService = new ThemeService();
+ themeService.init(setupMockUiSettings);
+ const { useChartsBaseTheme } = themeService;
+
+ const { result } = renderHook(() => useChartsBaseTheme());
+ expect(result.current).toBe(LIGHT_THEME);
+
+ act(() => darkMode$.next(true));
+ expect(result.current).toBe(DARK_THEME);
+ act(() => darkMode$.next(false));
+ expect(result.current).toBe(LIGHT_THEME);
+ });
+ });
});
diff --git a/src/plugins/charts/public/services/theme/theme.ts b/src/plugins/charts/public/services/theme/theme.ts
index e1e71573caa3a..2d0c4de883218 100644
--- a/src/plugins/charts/public/services/theme/theme.ts
+++ b/src/plugins/charts/public/services/theme/theme.ts
@@ -18,34 +18,56 @@
*/
import { useEffect, useState } from 'react';
-import { map } from 'rxjs/operators';
-import { Observable } from 'rxjs';
+import { Observable, BehaviorSubject } from 'rxjs';
import { CoreSetup } from 'kibana/public';
-import { RecursivePartial, Theme } from '@elastic/charts';
+import { DARK_THEME, LIGHT_THEME, PartialTheme, Theme } from '@elastic/charts';
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
export class ThemeService {
- private _chartsTheme$?: Observable>;
-
/** Returns default charts theme */
public readonly chartsDefaultTheme = EUI_CHARTS_THEME_LIGHT.theme;
+ public readonly chartsDefaultBaseTheme = LIGHT_THEME;
+
+ private _uiSettingsDarkMode$?: Observable;
+ private _chartsTheme$ = new BehaviorSubject(this.chartsDefaultTheme);
+ private _chartsBaseTheme$ = new BehaviorSubject(this.chartsDefaultBaseTheme);
/** An observable of the current charts theme */
- public get chartsTheme$(): Observable> {
- if (!this._chartsTheme$) {
+ public chartsTheme$ = this._chartsTheme$.asObservable();
+
+ /** An observable of the current charts base theme */
+ public chartsBaseTheme$ = this._chartsBaseTheme$.asObservable();
+
+ /** An observable boolean for dark mode of kibana */
+ public get darkModeEnabled$(): Observable {
+ if (!this._uiSettingsDarkMode$) {
throw new Error('ThemeService not initialized');
}
- return this._chartsTheme$;
+ return this._uiSettingsDarkMode$;
}
+ /** A React hook for consuming the dark mode value */
+ public useDarkMode = (): boolean => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [value, update] = useState(false);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useEffect(() => {
+ const s = this.darkModeEnabled$.subscribe(update);
+ return () => s.unsubscribe();
+ }, []);
+
+ return value;
+ };
+
/** A React hook for consuming the charts theme */
- public useChartsTheme = () => {
- /* eslint-disable-next-line react-hooks/rules-of-hooks */
+ public useChartsTheme = (): PartialTheme => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
const [value, update] = useState(this.chartsDefaultTheme);
- /* eslint-disable-next-line react-hooks/rules-of-hooks */
+ // eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
const s = this.chartsTheme$.subscribe(update);
return () => s.unsubscribe();
@@ -54,12 +76,28 @@ export class ThemeService {
return value;
};
+ /** A React hook for consuming the charts theme */
+ public useChartsBaseTheme = (): Theme => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [value, update] = useState(this.chartsDefaultBaseTheme);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useEffect(() => {
+ const s = this.chartsBaseTheme$.subscribe(update);
+ return () => s.unsubscribe();
+ }, []);
+
+ return value;
+ };
+
/** initialize service with uiSettings */
public init(uiSettings: CoreSetup['uiSettings']) {
- this._chartsTheme$ = uiSettings
- .get$('theme:darkMode')
- .pipe(
- map((darkMode) => (darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme))
+ this._uiSettingsDarkMode$ = uiSettings.get$('theme:darkMode');
+ this._uiSettingsDarkMode$.subscribe((darkMode) => {
+ this._chartsTheme$.next(
+ darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme
);
+ this._chartsBaseTheme$.next(darkMode ? DARK_THEME : LIGHT_THEME);
+ });
}
}
diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx
index 9afe5e48bc5b8..4c39c8bb25542 100644
--- a/src/plugins/discover/public/application/angular/directives/histogram.tsx
+++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx
@@ -40,12 +40,13 @@ import {
ElementClickListener,
XYChartElementEvent,
BrushEndListener,
+ Theme,
} from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'kibana/public';
import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme';
-import { Subscription } from 'rxjs';
+import { Subscription, combineLatest } from 'rxjs';
import { getServices } from '../../../kibana_services';
import { Chart as IChart } from '../helpers/point_series';
@@ -56,6 +57,7 @@ export interface DiscoverHistogramProps {
interface DiscoverHistogramState {
chartsTheme: EuiChartThemeType['theme'];
+ chartsBaseTheme: Theme;
}
function findIntervalFromDuration(
@@ -126,18 +128,21 @@ export class DiscoverHistogram extends Component this.setState({ chartsTheme })
+ this.subscription = combineLatest(
+ getServices().theme.chartsTheme$,
+ getServices().theme.chartsBaseTheme$
+ ).subscribe(([chartsTheme, chartsBaseTheme]) =>
+ this.setState({ chartsTheme, chartsBaseTheme })
);
}
componentWillUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
- this.subscription = undefined;
}
}
@@ -204,7 +209,7 @@ export class DiscoverHistogram extends Component
values.map(({ key, docs }) => ({
@@ -56,7 +56,6 @@ const handleCursorUpdate = (cursor) => {
};
export const TimeSeries = ({
- darkMode,
backgroundColor,
showGrid,
legend,
@@ -90,15 +89,15 @@ export const TimeSeries = ({
const timeZone = getTimezone(uiSettings);
const hasBarChart = series.some(({ bars }) => bars?.show);
- // compute the theme based on the bg color
- const theme = getTheme(darkMode, backgroundColor);
// apply legend style change if bgColor is configured
const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor));
// If the color isn't configured by the user, use the color mapping service
// to assign a color from the Kibana palette. Colors will be shared across the
// session, including dashboards.
- const { colors } = getChartsSetup();
+ const { colors, theme: themeService } = getChartsSetup();
+ const baseTheme = getBaseTheme(themeService.useChartsBaseTheme(), backgroundColor);
+
colors.mappedColors.mapKeys(series.filter(({ color }) => !color).map(({ label }) => label));
const onBrushEndListener = ({ x }) => {
@@ -118,7 +117,7 @@ export const TimeSeries = ({
onBrushEnd={onBrushEndListener}
animateData={false}
onPointerUpdate={handleCursorUpdate}
- theme={
+ theme={[
hasBarChart
? {}
: {
@@ -127,9 +126,14 @@ export const TimeSeries = ({
fill: '#F00',
},
},
- }
- }
- baseTheme={theme}
+ },
+ {
+ background: {
+ color: backgroundColor,
+ },
+ },
+ ]}
+ baseTheme={baseTheme}
tooltip={{
snap: true,
type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor,
@@ -269,7 +273,6 @@ TimeSeries.defaultProps = {
};
TimeSeries.propTypes = {
- darkMode: PropTypes.bool,
backgroundColor: PropTypes.string,
showGrid: PropTypes.bool,
legend: PropTypes.bool,
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts
index 57ca38168ac27..d7e6560a8dc97 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.test.ts
@@ -17,28 +17,30 @@
* under the License.
*/
-import { getTheme } from './theme';
+import { getBaseTheme } from './theme';
import { LIGHT_THEME, DARK_THEME } from '@elastic/charts';
describe('TSVB theme', () => {
it('should return the basic themes if no bg color is specified', () => {
// use original dark/light theme
- expect(getTheme(false)).toEqual(LIGHT_THEME);
- expect(getTheme(true)).toEqual(DARK_THEME);
+ expect(getBaseTheme(LIGHT_THEME)).toEqual(LIGHT_THEME);
+ expect(getBaseTheme(DARK_THEME)).toEqual(DARK_THEME);
// discard any wrong/missing bg color
- expect(getTheme(true, null)).toEqual(DARK_THEME);
- expect(getTheme(true, '')).toEqual(DARK_THEME);
- expect(getTheme(true, undefined)).toEqual(DARK_THEME);
+ expect(getBaseTheme(DARK_THEME, null)).toEqual(DARK_THEME);
+ expect(getBaseTheme(DARK_THEME, '')).toEqual(DARK_THEME);
+ expect(getBaseTheme(DARK_THEME, undefined)).toEqual(DARK_THEME);
});
it('should return a highcontrast color theme for a different background', () => {
// red use a near full-black color
- expect(getTheme(false, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)');
+ expect(getBaseTheme(LIGHT_THEME, 'red').axes.axisTitleStyle.fill).toEqual('rgb(23,23,23)');
// violet increased the text color to full white for higer contrast
- expect(getTheme(false, '#ba26ff').axes.axisTitleStyle.fill).toEqual('rgb(255,255,255)');
+ expect(getBaseTheme(LIGHT_THEME, '#ba26ff').axes.axisTitleStyle.fill).toEqual(
+ 'rgb(255,255,255)'
+ );
// light yellow, prefer the LIGHT_THEME fill color because already with a good contrast
- expect(getTheme(false, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333');
+ expect(getBaseTheme(LIGHT_THEME, '#fff49f').axes.axisTitleStyle.fill).toEqual('#333');
});
});
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts
index 2694732aa381d..0e13fd7ef68f9 100644
--- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts
+++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/utils/theme.ts
@@ -94,9 +94,15 @@ function isValidColor(color: string | null | undefined): color is string {
}
}
-export function getTheme(darkMode: boolean, bgColor?: string | null): Theme {
+/**
+ * compute base chart theme based on the background color
+ *
+ * @param baseTheme
+ * @param bgColor
+ */
+export function getBaseTheme(baseTheme: Theme, bgColor?: string | null): Theme {
if (!isValidColor(bgColor)) {
- return darkMode ? DARK_THEME : LIGHT_THEME;
+ return baseTheme;
}
const bgLuminosity = computeRelativeLuminosity(bgColor);
diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json
index 346a5a24c269f..7da5eaed5155e 100644
--- a/x-pack/plugins/lens/kibana.json
+++ b/x-pack/plugins/lens/kibana.json
@@ -10,7 +10,8 @@
"navigation",
"kibanaLegacy",
"visualizations",
- "dashboard"
+ "dashboard",
+ "charts"
],
"optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"],
"configPath": ["xpack", "lens"],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
index f70df855fe0cb..0d60bd588f710 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
@@ -17,6 +17,7 @@ import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { ChangeIndexPattern } from './change_indexpattern';
import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui';
import { documentField } from './document_field';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
const initialState: IndexPatternPrivateState = {
indexPatternRefs: [],
@@ -230,6 +231,7 @@ describe('IndexPattern Data Panel', () => {
fromDate: 'now-7d',
toDate: 'now',
},
+ charts: chartPluginMock.createSetupContract(),
query: { query: '', language: 'lucene' },
filters: [],
showNoDataPopover: jest.fn(),
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
index 87fbf81fceba0..0e7cefb58fc28 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
@@ -47,9 +47,11 @@ export type Props = DatasourceDataPanelProps & {
state: IndexPatternPrivateState,
setState: StateSetter
) => void;
+ charts: ChartsPluginSetup;
};
import { LensFieldIcon } from './lens_field_icon';
import { ChangeIndexPattern } from './change_indexpattern';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
// TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted
const FixedEuiContextMenuPanel = (EuiContextMenuPanel as unknown) as React.FunctionComponent<
@@ -82,6 +84,7 @@ export function IndexPatternDataPanel({
filters,
dateRange,
changeIndexPattern,
+ charts,
showNoDataPopover,
}: Props) {
const { indexPatternRefs, indexPatterns, currentIndexPatternId } = state;
@@ -170,6 +173,7 @@ export function IndexPatternDataPanel({
dragDropContext={dragDropContext}
core={core}
data={data}
+ charts={charts}
onChangeIndexPattern={onChangeIndexPattern}
existingFields={state.existingFields}
/>
@@ -214,6 +218,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
core,
data,
existingFields,
+ charts,
}: Omit & {
data: DataPublicPluginStart;
currentIndexPatternId: string;
@@ -222,6 +227,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
dragDropContext: DragContextState;
onChangeIndexPattern: (newId: string) => void;
existingFields: IndexPatternPrivateState['existingFields'];
+ charts: ChartsPluginSetup;
}) {
const [localState, setLocalState] = useState({
nameFilter: '',
@@ -376,6 +382,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
dateRange,
query,
filters,
+ chartsThemeService: charts.theme,
}),
[core, data, currentIndexPattern, dateRange, query, filters, localState.nameFilter]
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
index e8dfbc250c539..0a3af97f8ad75 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx
@@ -13,6 +13,9 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { IndexPattern } from './types';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
+
+const chartsThemeService = chartPluginMock.createSetupContract().theme;
describe('IndexPattern Field Item', () => {
let defaultProps: FieldItemProps;
@@ -80,6 +83,7 @@ describe('IndexPattern Field Item', () => {
searchable: true,
},
exists: true,
+ chartsThemeService,
};
data.fieldFormats = ({
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index 1a1a34d30f8a8..815725f4331a6 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -20,7 +20,6 @@ import {
EuiText,
EuiToolTip,
} from '@elastic/eui';
-import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import {
Axis,
BarSeries,
@@ -41,6 +40,7 @@ import {
esQuery,
IIndexPattern,
} from '../../../../../src/plugins/data/public';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { DraggedField } from './indexpattern';
import { DragDrop } from '../drag_drop';
import { DatasourceDataPanelProps, DataType } from '../types';
@@ -60,6 +60,7 @@ export interface FieldItemProps {
exists: boolean;
query: Query;
dateRange: DatasourceDataPanelProps['dateRange'];
+ chartsThemeService: ChartsPluginSetup['theme'];
filters: Filter[];
hideDetails?: boolean;
}
@@ -254,11 +255,12 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
dateRange,
core,
sampledValues,
+ chartsThemeService,
data: { fieldFormats },
} = props;
- const IS_DARK_THEME = core.uiSettings.get('theme:darkMode');
- const chartTheme = IS_DARK_THEME ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme;
+ const chartTheme = chartsThemeService.useChartsTheme();
+ const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
let histogramDefault = !!props.histogram;
const totalValuesCount =
@@ -410,6 +412,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) {
-
+
{
let defaultProps: FieldsAccordionProps;
@@ -56,6 +57,7 @@ describe('Fields Accordion', () => {
},
query: { query: '', language: 'lucene' },
filters: [],
+ chartsThemeService: chartPluginMock.createSetupContract().theme,
};
defaultProps = {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
index b756cf81a9073..7cc049c107b87 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx
@@ -19,10 +19,12 @@ import { FieldItem } from './field_item';
import { Query, Filter } from '../../../../../src/plugins/data/public';
import { DatasourceDataPanelProps } from '../types';
import { IndexPattern } from './types';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
export interface FieldItemSharedProps {
core: DatasourceDataPanelProps['core'];
data: DataPublicPluginStart;
+ chartsThemeService: ChartsPluginSetup['theme'];
indexPattern: IndexPattern;
highlight?: string;
query: Query;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
index 73fd144b9c7f8..45d0ee45fab4c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts
@@ -9,6 +9,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public';
import { getIndexPatternDatasource } from './indexpattern';
import { renameColumns } from './rename_columns';
import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import {
DataPublicPluginSetup,
DataPublicPluginStart,
@@ -19,6 +20,7 @@ export interface IndexPatternDatasourceSetupPlugins {
expressions: ExpressionsSetup;
data: DataPublicPluginSetup;
editorFrame: EditorFrameSetup;
+ charts: ChartsPluginSetup;
}
export interface IndexPatternDatasourceStartPlugins {
@@ -30,7 +32,7 @@ export class IndexPatternDatasource {
setup(
core: CoreSetup,
- { expressions, editorFrame }: IndexPatternDatasourceSetupPlugins
+ { expressions, editorFrame, charts }: IndexPatternDatasourceSetupPlugins
) {
expressions.registerFunction(renameColumns);
@@ -40,6 +42,7 @@ export class IndexPatternDatasource {
core: coreStart,
storage: new Storage(localStorage),
data,
+ charts,
})
) as Promise
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index 6a79ce450cd9a..3bd0685551a4c 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -11,6 +11,7 @@ import { coreMock } from 'src/core/public/mocks';
import { IndexPatternPersistedState, IndexPatternPrivateState } from './types';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { Ast } from '@kbn/interpreter/common';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
jest.mock('./loader');
jest.mock('../id_generator');
@@ -140,6 +141,7 @@ describe('IndexPattern Data Source', () => {
storage: {} as IStorageWrapper,
core: coreMock.createStart(),
data: dataPluginMock.createStartContract(),
+ charts: chartPluginMock.createSetupContract(),
});
persistedState = {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
index a98f63cf9b360..e9d095bfbcef1 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx
@@ -46,6 +46,7 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
import { deleteColumn } from './state_helpers';
import { Datasource, StateSetter } from '../index';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
export { OperationType, IndexPatternColumn } from './operations';
@@ -102,10 +103,12 @@ export function getIndexPatternDatasource({
core,
storage,
data,
+ charts,
}: {
core: CoreStart;
storage: IStorageWrapper;
data: DataPublicPluginStart;
+ charts: ChartsPluginSetup;
}) {
const savedObjectsClient = core.savedObjects.client;
const uiSettings = core.uiSettings;
@@ -212,6 +215,7 @@ export function getIndexPatternDatasource({
});
}}
data={data}
+ charts={charts}
{...props}
/>
,
diff --git a/x-pack/plugins/lens/public/pie_visualization/index.ts b/x-pack/plugins/lens/public/pie_visualization/index.ts
index dd828c6c35300..401b6d634c696 100644
--- a/x-pack/plugins/lens/public/pie_visualization/index.ts
+++ b/x-pack/plugins/lens/public/pie_visualization/index.ts
@@ -4,18 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { CoreSetup } from 'src/core/public';
import { ExpressionsSetup } from 'src/plugins/expressions/public';
import { pieVisualization } from './pie_visualization';
import { pie, getPieRenderer } from './register_expression';
import { EditorFrameSetup, FormatFactory } from '../types';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
export interface PieVisualizationPluginSetupPlugins {
editorFrame: EditorFrameSetup;
expressions: ExpressionsSetup;
formatFactory: Promise;
+ charts: ChartsPluginSetup;
}
export interface PieVisualizationPluginStartPlugins {
@@ -27,17 +28,14 @@ export class PieVisualization {
setup(
core: CoreSetup,
- { expressions, formatFactory, editorFrame }: PieVisualizationPluginSetupPlugins
+ { expressions, formatFactory, editorFrame, charts }: PieVisualizationPluginSetupPlugins
) {
expressions.registerFunction(() => pie);
expressions.registerRenderer(
getPieRenderer({
formatFactory,
- chartTheme: core.uiSettings.get('theme:darkMode')
- ? EUI_CHARTS_THEME_DARK.theme
- : EUI_CHARTS_THEME_LIGHT.theme,
- isDarkMode: core.uiSettings.get('theme:darkMode'),
+ chartsThemeService: charts.theme,
})
);
diff --git a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
index bbc6a1dc75c3a..cea84db8b2794 100644
--- a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx
@@ -8,7 +8,6 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
-import { PartialTheme } from '@elastic/charts';
import {
IInterpreterRenderHandlers,
ExpressionRenderDefinition,
@@ -17,6 +16,7 @@ import {
import { LensMultiTable, FormatFactory, LensFilterEvent } from '../types';
import { PieExpressionProps, PieExpressionArgs } from './types';
import { PieComponent } from './render_function';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
export interface PieRender {
type: 'render';
@@ -93,8 +93,7 @@ export const pie: ExpressionFunctionDefinition<
export const getPieRenderer = (dependencies: {
formatFactory: Promise;
- chartTheme: PartialTheme;
- isDarkMode: boolean;
+ chartsThemeService: ChartsPluginSetup['theme'];
}): ExpressionRenderDefinition => ({
name: 'lens_pie_renderer',
displayName: i18n.translate('xpack.lens.pie.visualizationName', {
@@ -116,10 +115,9 @@ export const getPieRenderer = (dependencies: {
,
domNode,
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
index 2e29513ba548b..cfbeb27efb3d0 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx
@@ -11,6 +11,9 @@ import { LensMultiTable } from '../types';
import { PieComponent } from './render_function';
import { PieExpressionArgs } from './types';
import { EmptyPlaceholder } from '../shared_components';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
+
+const chartsThemeService = chartPluginMock.createSetupContract().theme;
describe('PieVisualization component', () => {
let getFormatSpy: jest.Mock;
@@ -57,9 +60,8 @@ describe('PieVisualization component', () => {
return {
data,
formatFactory: getFormatSpy,
- isDarkMode: false,
- chartTheme: {},
onClickValue: jest.fn(),
+ chartsThemeService,
};
}
diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
index 36e8d9660ab70..f349cc4dfd648 100644
--- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
+++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx
@@ -19,7 +19,6 @@ import {
PartitionConfig,
PartitionLayer,
PartitionLayout,
- PartialTheme,
PartitionFillLabel,
RecursivePartial,
LayerValue,
@@ -32,6 +31,7 @@ import { getSliceValueWithFallback, getFilterContext } from './render_helpers';
import { EmptyPlaceholder } from '../shared_components';
import './visualization.scss';
import { desanitizeFilterContext } from '../utils';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
const EMPTY_SLICE = Symbol('empty_slice');
@@ -40,15 +40,14 @@ const sortedColors = euiPaletteColorBlindBehindText();
export function PieComponent(
props: PieExpressionProps & {
formatFactory: FormatFactory;
- chartTheme: Exclude;
- isDarkMode: boolean;
+ chartsThemeService: ChartsPluginSetup['theme'];
onClickValue: (data: LensFilterEvent['data']) => void;
}
) {
const [firstTable] = Object.values(props.data.tables);
const formatters: Record> = {};
- const { chartTheme, isDarkMode, onClickValue } = props;
+ const { chartsThemeService, onClickValue } = props;
const {
shape,
groups,
@@ -60,6 +59,9 @@ export function PieComponent(
percentDecimals,
hideLabels,
} = props.args;
+ const isDarkMode = chartsThemeService.useDarkMode();
+ const chartTheme = chartsThemeService.useChartsTheme();
+ const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
if (!hideLabels) {
firstTable.columns.forEach((column) => {
@@ -245,6 +247,8 @@ export function PieComponent(
onClickValue(desanitizeFilterContext(context));
}}
+ theme={chartTheme}
+ baseTheme={chartBaseTheme}
/>
,
- { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies
+ {
+ kibanaLegacy,
+ expressions,
+ data,
+ embeddable,
+ visualizations,
+ charts,
+ }: LensPluginSetupDependencies
) {
const editorFrameSetupInterface = this.editorFrameService.setup(core, {
data,
embeddable,
expressions,
});
- const dependencies = {
+ const dependencies: IndexPatternDatasourceSetupPlugins &
+ XyVisualizationPluginSetupPlugins &
+ DatatableVisualizationPluginSetupPlugins &
+ MetricVisualizationPluginSetupPlugins &
+ PieVisualizationPluginSetupPlugins = {
expressions,
data,
+ charts,
editorFrame: editorFrameSetupInterface,
formatFactory: core
.getStartServices()
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
index 48c70e0a4a05b..8cb30037379da 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`xy_expression XYChart component it renders area 1`] = `
renderer="canvas"
>
;
editorFrame: EditorFrameSetup;
+ charts: ChartsPluginSetup;
}
function getTimeZone(uiSettings: IUiSettingsClient) {
@@ -34,7 +35,7 @@ export class XyVisualization {
setup(
core: CoreSetup,
- { expressions, formatFactory, editorFrame }: XyVisualizationPluginSetupPlugins
+ { expressions, formatFactory, editorFrame, charts }: XyVisualizationPluginSetupPlugins
) {
expressions.registerFunction(() => legendConfig);
expressions.registerFunction(() => yAxisConfig);
@@ -44,9 +45,7 @@ export class XyVisualization {
expressions.registerRenderer(
getXyChartRenderer({
formatFactory,
- chartTheme: core.uiSettings.get('theme:darkMode')
- ? EUI_CHARTS_THEME_DARK.theme
- : EUI_CHARTS_THEME_LIGHT.theme,
+ chartsThemeService: charts.theme,
timeZone: getTimeZone(core.uiSettings),
histogramBarTarget: core.uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
})
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
index 34f2a9111253b..f433a88e3bdbd 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
@@ -24,10 +24,13 @@ import { shallow } from 'enzyme';
import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './types';
import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
const onClickValue = jest.fn();
const onSelectRange = jest.fn();
+const chartsThemeService = chartPluginMock.createSetupContract().theme;
+
const dateHistogramData: LensMultiTable = {
type: 'lens_multitable',
tables: {
@@ -324,7 +327,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -347,7 +350,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -398,7 +401,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -434,7 +437,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -471,7 +474,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -509,7 +512,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -554,7 +557,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -589,7 +592,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -606,7 +609,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -626,7 +629,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -646,7 +649,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -671,7 +674,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -721,7 +724,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -758,7 +761,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -778,7 +781,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -801,7 +804,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -822,7 +825,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="CEST"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -842,7 +845,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [firstLayer] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -869,7 +872,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -890,7 +893,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1196,7 +1199,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1215,7 +1218,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1234,7 +1237,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1252,7 +1255,7 @@ describe('xy_expression', () => {
data={{ ...data }}
args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }}
formatFactory={getFormatSpy}
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
timeZone="UTC"
onClickValue={onClickValue}
@@ -1274,7 +1277,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1359,7 +1362,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1417,7 +1420,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
@@ -1473,7 +1476,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
- chartTheme={{}}
+ chartsThemeService={chartsThemeService}
histogramBarTarget={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
index 17ed04aa0e9c4..3ff7bd7fda304 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
@@ -15,7 +15,6 @@ import {
AreaSeries,
BarSeries,
Position,
- PartialTheme,
GeometryValue,
XYChartSeriesIdentifier,
} from '@elastic/charts';
@@ -38,6 +37,7 @@ import { XYArgs, SeriesType, visualizationTypes } from './types';
import { VisualizationContainer } from '../visualization_container';
import { isHorizontalChart } from './state_helpers';
import { parseInterval } from '../../../../../src/plugins/data/common';
+import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { EmptyPlaceholder } from '../shared_components';
import { desanitizeFilterContext } from '../utils';
import { getAxesConfiguration } from './axes_configuration';
@@ -59,7 +59,7 @@ export interface XYRender {
}
type XYChartRenderProps = XYChartProps & {
- chartTheme: PartialTheme;
+ chartsThemeService: ChartsPluginSetup['theme'];
formatFactory: FormatFactory;
timeZone: string;
histogramBarTarget: number;
@@ -115,7 +115,7 @@ export const xyChart: ExpressionFunctionDefinition<
export const getXyChartRenderer = (dependencies: {
formatFactory: Promise;
- chartTheme: PartialTheme;
+ chartsThemeService: ChartsPluginSetup['theme'];
histogramBarTarget: number;
timeZone: string;
}): ExpressionRenderDefinition => ({
@@ -144,7 +144,7 @@ export const getXyChartRenderer = (dependencies: {
{
return !(
@@ -276,6 +278,7 @@ export function XYChart({
legendPosition={legend.position}
showLegendExtra={false}
theme={chartTheme}
+ baseTheme={chartBaseTheme}
tooltip={{
headerFormatter: (d) => xAxisFormatter.convert(d.value),
}}
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx
index 244d431930f2e..a282fa08e8f38 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx
@@ -160,7 +160,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({
setLoadingState(LoadingStateType.Idle);
}
})();
- /* eslint-disable react-hooks/exhaustive-deps */
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [
index,
timeField,
@@ -175,12 +175,12 @@ export const ThresholdVisualization: React.FunctionComponent = ({
threshold,
startVisualizationAt,
]);
- /* eslint-enable react-hooks/exhaustive-deps */
if (!charts || !uiSettings || !dataFieldsFormats) {
return null;
}
const chartsTheme = charts.theme.useChartsTheme();
+ const chartsBaseTheme = charts.theme.useChartsBaseTheme();
const domain = getDomain(alertInterval, startVisualizationAt);
const visualizeOptions = {
@@ -261,6 +261,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({
Date: Thu, 2 Jul 2020 16:03:44 +0200
Subject: [PATCH 11/18] [Ingest Manager] Update asset paths to use _ instead of
- (#70320)
In https://github.com/elastic/package-registry/issues/517 the naming of the file paths inside a package is standardised to only use `_` and not `-`. This adjusts the paths for `ilm-policy`, `component-template`, `index-template` to the correct path.
An additional change here is to get rid of assets we don't support yet, like rollup jobs and ml jobs. We will reintroduce these when we support them.
---
.../ingest_manager/common/types/models/epm.ts | 19 ++++++++-----------
.../ingest_manager/sections/epm/constants.tsx | 8 ++++----
2 files changed, 12 insertions(+), 15 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 5b68cd2beeed4..bf6a8de15182d 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -32,10 +32,10 @@ export enum KibanaAssetType {
}
export enum ElasticsearchAssetType {
- componentTemplate = 'component-template',
- ingestPipeline = 'ingest-pipeline',
- indexTemplate = 'index-template',
- ilmPolicy = 'ilm-policy',
+ componentTemplate = 'component_template',
+ ingestPipeline = 'ingest_pipeline',
+ indexTemplate = 'index_template',
+ ilmPolicy = 'ilm_policy',
}
export enum AgentAssetType {
@@ -243,13 +243,10 @@ export type AssetReference = Pick & {
* Types of assets which can be installed/removed
*/
export enum IngestAssetType {
- DataFrameTransform = 'data-frame-transform',
- IlmPolicy = 'ilm-policy',
- IndexTemplate = 'index-template',
- ComponentTemplate = 'component-template',
- IngestPipeline = 'ingest-pipeline',
- MlJob = 'ml-job',
- RollupJob = 'rollup-job',
+ IlmPolicy = 'ilm_policy',
+ IndexTemplate = 'index_template',
+ ComponentTemplate = 'component_template',
+ IngestPipeline = 'ingest_pipeline',
}
export enum DefaultPackages {
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
index 54cb5171f5a3e..31c6d76446447 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx
@@ -17,11 +17,11 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
export const AssetTitleMap: Record = {
dashboard: 'Dashboard',
- 'ilm-policy': 'ILM Policy',
- 'ingest-pipeline': 'Ingest Pipeline',
+ ilm_policy: 'ILM Policy',
+ ingest_pipeline: 'Ingest Pipeline',
'index-pattern': 'Index Pattern',
- 'index-template': 'Index Template',
- 'component-template': 'Component Template',
+ index_template: 'Index Template',
+ component_template: 'Component Template',
search: 'Saved Search',
visualization: 'Visualization',
input: 'Agent input',
From 854e7a5204502ea1977382c89457c160ee7767b3 Mon Sep 17 00:00:00 2001
From: Dima Arnautov
Date: Thu, 2 Jul 2020 07:30:18 -0700
Subject: [PATCH 12/18] [ML] Anomaly Explorer swim lane pagination (#70063)
* [ML] use explorer service
* [ML] WIP pagination
* [ML] add to dashboard without the limit
* [ML] WIP
* [ML] loading states
* [ML] viewBySwimlaneDataLoading on field change
* [ML] fix dashboard control
* [ML] universal swim lane container, embeddable pagination
* [ML] fix css issue
* [ML] rename anomalyTimelineService
* [ML] rename callback
* [ML] rename container component
* [ML] empty state, increase pagination margin
* [ML] check for loading
* [ML] fix i18n
* [ML] fix unit test
* [ML] improve selected cells
* [ML] fix overall selection with changing job selection
* [ML] required props for pagination component
* [ML] move RESIZE_IGNORED_DIFF_PX
* [ML] jest tests
* [ML] add test subject
* [ML] SWIM_LANE_DEFAULT_PAGE_SIZE
* [ML] change empty state styling
* [ML] fix agg size for influencer filters
* [ML] remove debounce
* [ML] SCSS variables, rename swim lane class
* [ML] job selector using context
* [ML] set padding for embeddable panel
* [ML] adjust pagination styles
* [ML] replace custom time range subject with timefilter
* [ML] change loading indicator to mono
* [ML] use swim lane type constant
* [ML] change context naming
* [ML] update jest snapshot
* [ML] fix tests
---
x-pack/plugins/ml/public/application/app.tsx | 25 +-
.../job_selector/job_selector_flyout.tsx | 8 +-
.../date_picker_wrapper.tsx | 6 +-
.../contexts/kibana/kibana_context.ts | 4 +-
.../application/contexts/ml/ml_context.ts | 3 +-
.../application/explorer/_explorer.scss | 20 +-
.../explorer/actions/load_explorer_data.ts | 378 ++++++++++--------
.../explorer/add_to_dashboard_control.tsx | 7 +-
.../application/explorer/anomaly_timeline.tsx | 179 ++++-----
...explorer_no_influencers_found.test.js.snap | 21 +-
.../explorer_no_influencers_found.tsx | 38 +-
.../explorer/components/no_overall_data.tsx | 17 +
.../public/application/explorer/explorer.js | 154 +++----
.../explorer/explorer_constants.ts | 17 +-
.../explorer/explorer_dashboard_service.ts | 25 +-
.../explorer/explorer_swimlane.tsx | 10 +-
.../application/explorer/explorer_utils.d.ts | 22 +-
.../application/explorer/explorer_utils.js | 293 +-------------
.../explorer/hooks/use_selected_cells.ts | 87 ++--
.../application/explorer/legacy_utils.ts | 5 -
.../clear_influencer_filter_settings.ts | 1 +
.../explorer_reducer/job_selection_change.ts | 1 +
.../reducers/explorer_reducer/reducer.ts | 43 +-
.../set_influencer_filter_settings.ts | 1 +
.../reducers/explorer_reducer/state.ts | 12 +-
.../explorer/select_limit/index.ts | 7 -
.../select_limit/select_limit.test.tsx | 29 --
.../explorer/select_limit/select_limit.tsx | 40 --
.../explorer/swimlane_container.tsx | 150 +++++--
.../explorer/swimlane_pagination.tsx | 108 +++++
.../application/routing/routes/explorer.tsx | 22 +-
.../routes/timeseriesexplorer.test.tsx | 64 +--
.../public/application/routing/use_refresh.ts | 38 +-
...service.ts => anomaly_timeline_service.ts} | 85 +++-
.../services/dashboard_service.test.ts | 2 +-
.../application/services/dashboard_service.ts | 2 +-
.../services/ml_api_service/index.ts | 4 +-
.../services/ml_api_service/jobs.ts | 57 +--
.../results_service/results_service.d.ts | 16 +-
.../results_service/results_service.js | 43 +-
.../services/timefilter_refresh_service.tsx | 1 -
.../anomaly_swimlane_embeddable.tsx | 30 +-
...omaly_swimlane_embeddable_factory.test.tsx | 5 +-
.../anomaly_swimlane_embeddable_factory.ts | 14 +-
.../anomaly_swimlane_initializer.tsx | 22 +-
.../anomaly_swimlane_setup_flyout.tsx | 99 ++---
...> embeddable_swim_lane_container.test.tsx} | 66 +--
.../embeddable_swim_lane_container.tsx | 113 ++++++
.../explorer_swimlane_container.tsx | 126 ------
.../swimlane_input_resolver.test.ts | 14 +-
.../swimlane_input_resolver.ts | 96 ++++-
.../ui_actions/edit_swimlane_panel_action.tsx | 14 +-
.../translations/translations/ja-JP.json | 2 -
.../translations/translations/zh-CN.json | 2 -
.../services/ml/anomaly_explorer.ts | 2 +-
55 files changed, 1362 insertions(+), 1288 deletions(-)
create mode 100644 x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx
delete mode 100644 x-pack/plugins/ml/public/application/explorer/select_limit/index.ts
delete mode 100644 x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx
delete mode 100644 x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
create mode 100644 x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx
rename x-pack/plugins/ml/public/application/services/{explorer_service.ts => anomaly_timeline_service.ts} (82%)
rename x-pack/plugins/ml/public/embeddables/anomaly_swimlane/{explorer_swimlane_container.test.tsx => embeddable_swim_lane_container.test.tsx} (73%)
create mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx
delete mode 100644 x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx
diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx
index 9539d530bab04..9d5125532e5b8 100644
--- a/x-pack/plugins/ml/public/application/app.tsx
+++ b/x-pack/plugins/ml/public/application/app.tsx
@@ -7,7 +7,7 @@
import React, { FC } from 'react';
import ReactDOM from 'react-dom';
-import { AppMountParameters, CoreStart } from 'kibana/public';
+import { AppMountParameters, CoreStart, HttpStart } from 'kibana/public';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
@@ -17,6 +17,8 @@ import { setLicenseCache } from './license';
import { MlSetupDependencies, MlStartDependencies } from '../plugin';
import { MlRouter } from './routing';
+import { mlApiServicesProvider } from './services/ml_api_service';
+import { HttpService } from './services/http_service';
type MlDependencies = MlSetupDependencies & MlStartDependencies;
@@ -27,6 +29,23 @@ interface AppProps {
const localStorage = new Storage(window.localStorage);
+/**
+ * Provides global services available across the entire ML app.
+ */
+export function getMlGlobalServices(httpStart: HttpStart) {
+ const httpService = new HttpService(httpStart);
+ return {
+ httpService,
+ mlApiServices: mlApiServicesProvider(httpService),
+ };
+}
+
+export interface MlServicesContext {
+ mlServices: MlGlobalServices;
+}
+
+export type MlGlobalServices = ReturnType;
+
const App: FC = ({ coreStart, deps }) => {
const pageDeps = {
indexPatterns: deps.data.indexPatterns,
@@ -47,7 +66,9 @@ const App: FC = ({ coreStart, deps }) => {
const I18nContext = coreStart.i18n.Context;
return (
-
+
diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
index 3fb654f35be4d..803281bcd0ce9 100644
--- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
+++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector_flyout.tsx
@@ -27,7 +27,6 @@ import {
normalizeTimes,
} from './job_select_service_utils';
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';
-import { ml } from '../../services/ml_api_service';
import { useMlKibana } from '../../contexts/kibana';
import { JobSelectionMaps } from './job_selector';
@@ -66,7 +65,10 @@ export const JobSelectorFlyout: FC = ({
withTimeRangeSelector = true,
}) => {
const {
- services: { notifications },
+ services: {
+ notifications,
+ mlServices: { mlApiServices },
+ },
} = useMlKibana();
const [newSelection, setNewSelection] = useState(selectedIds);
@@ -151,7 +153,7 @@ export const JobSelectorFlyout: FC = ({
async function fetchJobs() {
try {
- const resp = await ml.jobs.jobsWithTimerange(dateFormatTz);
+ const resp = await mlApiServices.jobs.jobsWithTimerange(dateFormatTz);
const normalizedJobs = normalizeTimes(resp.jobs, dateFormatTz, DEFAULT_GANTT_BAR_WIDTH);
const { groups: groupsWithTimerange, groupsMap } = getGroupsFromJobs(normalizedJobs);
setJobs(normalizedJobs);
diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx
index 27f8c822d68e3..beafae1ecd2f6 100644
--- a/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx
+++ b/x-pack/plugins/ml/public/application/components/navigation_menu/date_picker_wrapper/date_picker_wrapper.tsx
@@ -9,10 +9,7 @@ import { Subscription } from 'rxjs';
import { EuiSuperDatePicker, OnRefreshProps } from '@elastic/eui';
import { TimeRange, TimeHistoryContract } from 'src/plugins/data/public';
-import {
- mlTimefilterRefresh$,
- mlTimefilterTimeChange$,
-} from '../../../services/timefilter_refresh_service';
+import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service';
import { useUrlState } from '../../../util/url_state';
import { useMlKibana } from '../../../contexts/kibana';
@@ -108,7 +105,6 @@ export const DatePickerWrapper: FC = () => {
timefilter.setTime(newTime);
setTime(newTime);
setRecentlyUsedRanges(getRecentlyUsedRanges());
- mlTimefilterTimeChange$.next({ lastRefresh: Date.now(), timeRange: { start, end } });
}
function updateInterval({
diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
index 2a156b5716ad4..3bc3b8c2c6dfd 100644
--- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
+++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts
@@ -13,6 +13,7 @@ import {
import { SecurityPluginSetup } from '../../../../../security/public';
import { LicenseManagementUIPluginSetup } from '../../../../../license_management/public';
import { SharePluginStart } from '../../../../../../../src/plugins/share/public';
+import { MlServicesContext } from '../../app';
interface StartPlugins {
data: DataPublicPluginStart;
@@ -20,7 +21,8 @@ interface StartPlugins {
licenseManagement?: LicenseManagementUIPluginSetup;
share: SharePluginStart;
}
-export type StartServices = CoreStart & StartPlugins & { kibanaVersion: string };
+export type StartServices = CoreStart &
+ StartPlugins & { kibanaVersion: string } & MlServicesContext;
// eslint-disable-next-line react-hooks/rules-of-hooks
export const useMlKibana = () => useKibana();
export type MlKibanaReactContextValue = KibanaReactContextValue;
diff --git a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
index 07d5a153664b7..95ef5e5b2938c 100644
--- a/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
+++ b/x-pack/plugins/ml/public/application/contexts/ml/ml_context.ts
@@ -7,6 +7,7 @@
import React from 'react';
import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public';
import { SavedSearchSavedObject } from '../../../../common/types/kibana';
+import { MlServicesContext } from '../../app';
export interface MlContextValue {
combinedQuery: any;
@@ -34,4 +35,4 @@ export type SavedSearchQuery = object;
// Multiple custom hooks can be created to access subsets of
// the overall context value if necessary too,
// see useCurrentIndexPattern() for example.
-export const MlContext = React.createContext>({});
+export const MlContext = React.createContext>({});
diff --git a/x-pack/plugins/ml/public/application/explorer/_explorer.scss b/x-pack/plugins/ml/public/application/explorer/_explorer.scss
index 7e5f354bbb402..63c471e66c49a 100644
--- a/x-pack/plugins/ml/public/application/explorer/_explorer.scss
+++ b/x-pack/plugins/ml/public/application/explorer/_explorer.scss
@@ -1,3 +1,5 @@
+$borderRadius: $euiBorderRadius / 2;
+
.ml-swimlane-selector {
visibility: hidden;
}
@@ -104,10 +106,9 @@
// SASSTODO: This entire selector needs to be rewritten.
// It looks extremely brittle with very specific sizing units
- .ml-explorer-swimlane {
+ .mlExplorerSwimlane {
user-select: none;
padding: 0;
- margin-bottom: $euiSizeS;
line.gridLine {
stroke: $euiBorderColor;
@@ -218,17 +219,20 @@
div.lane {
height: 30px;
border-bottom: 0px;
- border-radius: 2px;
- margin-top: -1px;
+ border-radius: $borderRadius;
white-space: nowrap;
+ &:not(:first-child) {
+ margin-top: -1px;
+ }
+
div.lane-label {
display: inline-block;
- font-size: 13px;
+ font-size: $euiFontSizeXS;
height: 30px;
text-align: right;
vertical-align: middle;
- border-radius: 2px;
+ border-radius: $borderRadius;
padding-right: 5px;
margin-right: 5px;
border: 1px solid transparent;
@@ -261,7 +265,7 @@
.sl-cell-inner-dragselect {
height: 26px;
margin: 1px;
- border-radius: 2px;
+ border-radius: $borderRadius;
text-align: center;
}
@@ -293,7 +297,7 @@
.sl-cell-inner,
.sl-cell-inner-dragselect {
border: 2px solid $euiColorDarkShade;
- border-radius: 2px;
+ border-radius: $borderRadius;
opacity: 1;
}
}
diff --git a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts
index 590a69283a819..095b42ffac5b7 100644
--- a/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts
+++ b/x-pack/plugins/ml/public/application/explorer/actions/load_explorer_data.ts
@@ -11,6 +11,7 @@ import useObservable from 'react-use/lib/useObservable';
import { forkJoin, of, Observable, Subject } from 'rxjs';
import { mergeMap, switchMap, tap } from 'rxjs/operators';
+import { useCallback, useMemo } from 'react';
import { anomalyDataChange } from '../explorer_charts/explorer_charts_container_service';
import { explorerService } from '../explorer_dashboard_service';
import {
@@ -22,15 +23,17 @@ import {
loadAnomaliesTableData,
loadDataForCharts,
loadFilteredTopInfluencers,
- loadOverallData,
loadTopInfluencers,
- loadViewBySwimlane,
- loadViewByTopFieldValuesForSelectedTime,
AppStateSelectedCells,
ExplorerJob,
TimeRangeBounds,
} from '../explorer_utils';
import { ExplorerState } from '../reducers';
+import { useMlKibana, useTimefilter } from '../../contexts/kibana';
+import { AnomalyTimelineService } from '../../services/anomaly_timeline_service';
+import { mlResultsServiceProvider } from '../../services/results_service';
+import { isViewBySwimLaneData } from '../swimlane_container';
+import { ANOMALY_SWIM_LANE_HARD_LIMIT } from '../explorer_constants';
// Memoize the data fetching methods.
// wrapWithLastRefreshArg() wraps any given function and preprends a `lastRefresh` argument
@@ -39,13 +42,13 @@ import { ExplorerState } from '../reducers';
// about this parameter. The generic type T retains and returns the type information of
// the original function.
const memoizeIsEqual = (newArgs: any[], lastArgs: any[]) => isEqual(newArgs, lastArgs);
-const wrapWithLastRefreshArg = any>(func: T) => {
+const wrapWithLastRefreshArg = any>(func: T, context: any = null) => {
return function (lastRefresh: number, ...args: Parameters): ReturnType {
- return func.apply(null, args);
+ return func.apply(context, args);
};
};
-const memoize = any>(func: T) => {
- return memoizeOne(wrapWithLastRefreshArg(func), memoizeIsEqual);
+const memoize = any>(func: T, context?: any) => {
+ return memoizeOne(wrapWithLastRefreshArg(func, context), memoizeIsEqual);
};
const memoizedAnomalyDataChange = memoize(anomalyDataChange);
@@ -56,9 +59,7 @@ const memoizedLoadDataForCharts = memoize(loadDataForC
const memoizedLoadFilteredTopInfluencers = memoize(
loadFilteredTopInfluencers
);
-const memoizedLoadOverallData = memoize(loadOverallData);
const memoizedLoadTopInfluencers = memoize(loadTopInfluencers);
-const memoizedLoadViewBySwimlane = memoize(loadViewBySwimlane);
const memoizedLoadAnomaliesTableData = memoize(loadAnomaliesTableData);
export interface LoadExplorerDataConfig {
@@ -73,6 +74,9 @@ export interface LoadExplorerDataConfig {
tableInterval: string;
tableSeverity: number;
viewBySwimlaneFieldName: string;
+ viewByFromPage: number;
+ viewByPerPage: number;
+ swimlaneContainerWidth: number;
}
export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfig => {
@@ -87,183 +91,213 @@ export const isLoadExplorerDataConfig = (arg: any): arg is LoadExplorerDataConfi
/**
* Fetches the data necessary for the Anomaly Explorer using observables.
- *
- * @param config LoadExplorerDataConfig
- *
- * @return Partial
*/
-function loadExplorerData(config: LoadExplorerDataConfig): Observable> {
- if (!isLoadExplorerDataConfig(config)) {
- return of({});
- }
-
- const {
- bounds,
- lastRefresh,
- influencersFilterQuery,
- noInfluencersConfigured,
- selectedCells,
- selectedJobs,
- swimlaneBucketInterval,
- swimlaneLimit,
- tableInterval,
- tableSeverity,
- viewBySwimlaneFieldName,
- } = config;
-
- const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName);
- const jobIds = getSelectionJobIds(selectedCells, selectedJobs);
- const timerange = getSelectionTimeRange(
- selectedCells,
- swimlaneBucketInterval.asSeconds(),
- bounds
+const loadExplorerDataProvider = (anomalyTimelineService: AnomalyTimelineService) => {
+ const memoizedLoadOverallData = memoize(
+ anomalyTimelineService.loadOverallData,
+ anomalyTimelineService
);
+ const memoizedLoadViewBySwimlane = memoize(
+ anomalyTimelineService.loadViewBySwimlane,
+ anomalyTimelineService
+ );
+ return (config: LoadExplorerDataConfig): Observable> => {
+ if (!isLoadExplorerDataConfig(config)) {
+ return of({});
+ }
- const dateFormatTz = getDateFormatTz();
-
- // First get the data where we have all necessary args at hand using forkJoin:
- // annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues
- return forkJoin({
- annotationsData: memoizedLoadAnnotationsTableData(
+ const {
+ bounds,
lastRefresh,
+ influencersFilterQuery,
+ noInfluencersConfigured,
selectedCells,
selectedJobs,
+ swimlaneBucketInterval,
+ swimlaneLimit,
+ tableInterval,
+ tableSeverity,
+ viewBySwimlaneFieldName,
+ swimlaneContainerWidth,
+ viewByFromPage,
+ viewByPerPage,
+ } = config;
+
+ const selectionInfluencers = getSelectionInfluencers(selectedCells, viewBySwimlaneFieldName);
+ const jobIds = getSelectionJobIds(selectedCells, selectedJobs);
+ const timerange = getSelectionTimeRange(
+ selectedCells,
swimlaneBucketInterval.asSeconds(),
bounds
- ),
- anomalyChartRecords: memoizedLoadDataForCharts(
- lastRefresh,
- jobIds,
- timerange.earliestMs,
- timerange.latestMs,
- selectionInfluencers,
- selectedCells,
- influencersFilterQuery
- ),
- influencers:
- selectionInfluencers.length === 0
- ? memoizedLoadTopInfluencers(
+ );
+
+ const dateFormatTz = getDateFormatTz();
+
+ // First get the data where we have all necessary args at hand using forkJoin:
+ // annotationsData, anomalyChartRecords, influencers, overallState, tableData, topFieldValues
+ return forkJoin({
+ annotationsData: memoizedLoadAnnotationsTableData(
+ lastRefresh,
+ selectedCells,
+ selectedJobs,
+ swimlaneBucketInterval.asSeconds(),
+ bounds
+ ),
+ anomalyChartRecords: memoizedLoadDataForCharts(
+ lastRefresh,
+ jobIds,
+ timerange.earliestMs,
+ timerange.latestMs,
+ selectionInfluencers,
+ selectedCells,
+ influencersFilterQuery
+ ),
+ influencers:
+ selectionInfluencers.length === 0
+ ? memoizedLoadTopInfluencers(
+ lastRefresh,
+ jobIds,
+ timerange.earliestMs,
+ timerange.latestMs,
+ [],
+ noInfluencersConfigured,
+ influencersFilterQuery
+ )
+ : Promise.resolve({}),
+ overallState: memoizedLoadOverallData(lastRefresh, selectedJobs, swimlaneContainerWidth),
+ tableData: memoizedLoadAnomaliesTableData(
+ lastRefresh,
+ selectedCells,
+ selectedJobs,
+ dateFormatTz,
+ swimlaneBucketInterval.asSeconds(),
+ bounds,
+ viewBySwimlaneFieldName,
+ tableInterval,
+ tableSeverity,
+ influencersFilterQuery
+ ),
+ topFieldValues:
+ selectedCells !== undefined && selectedCells.showTopFieldValues === true
+ ? anomalyTimelineService.loadViewByTopFieldValuesForSelectedTime(
+ timerange.earliestMs,
+ timerange.latestMs,
+ selectedJobs,
+ viewBySwimlaneFieldName,
+ swimlaneLimit,
+ viewByPerPage,
+ viewByFromPage,
+ swimlaneContainerWidth
+ )
+ : Promise.resolve([]),
+ }).pipe(
+ // Trigger a side-effect action to reset view-by swimlane,
+ // show the view-by loading indicator
+ // and pass on the data we already fetched.
+ tap(explorerService.setViewBySwimlaneLoading),
+ // Trigger a side-effect to update the charts.
+ tap(({ anomalyChartRecords }) => {
+ if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) {
+ memoizedAnomalyDataChange(
lastRefresh,
- jobIds,
+ anomalyChartRecords,
timerange.earliestMs,
timerange.latestMs,
+ tableSeverity
+ );
+ } else {
+ memoizedAnomalyDataChange(
+ lastRefresh,
[],
- noInfluencersConfigured,
- influencersFilterQuery
- )
- : Promise.resolve({}),
- overallState: memoizedLoadOverallData(
- lastRefresh,
- selectedJobs,
- swimlaneBucketInterval,
- bounds
- ),
- tableData: memoizedLoadAnomaliesTableData(
- lastRefresh,
- selectedCells,
- selectedJobs,
- dateFormatTz,
- swimlaneBucketInterval.asSeconds(),
- bounds,
- viewBySwimlaneFieldName,
- tableInterval,
- tableSeverity,
- influencersFilterQuery
- ),
- topFieldValues:
- selectedCells !== undefined && selectedCells.showTopFieldValues === true
- ? loadViewByTopFieldValuesForSelectedTime(
timerange.earliestMs,
timerange.latestMs,
- selectedJobs,
- viewBySwimlaneFieldName,
- swimlaneLimit,
- noInfluencersConfigured
- )
- : Promise.resolve([]),
- }).pipe(
- // Trigger a side-effect action to reset view-by swimlane,
- // show the view-by loading indicator
- // and pass on the data we already fetched.
- tap(explorerService.setViewBySwimlaneLoading),
- // Trigger a side-effect to update the charts.
- tap(({ anomalyChartRecords }) => {
- if (selectedCells !== undefined && Array.isArray(anomalyChartRecords)) {
- memoizedAnomalyDataChange(
- lastRefresh,
- anomalyChartRecords,
- timerange.earliestMs,
- timerange.latestMs,
- tableSeverity
- );
- } else {
- memoizedAnomalyDataChange(
- lastRefresh,
- [],
- timerange.earliestMs,
- timerange.latestMs,
- tableSeverity
- );
- }
- }),
- // Load view-by swimlane data and filtered top influencers.
- // mergeMap is used to have access to the already fetched data and act on it in arg #1.
- // In arg #2 of mergeMap we combine the data and pass it on in the action format
- // which can be consumed by explorerReducer() later on.
- mergeMap(
- ({ anomalyChartRecords, influencers, overallState, topFieldValues }) =>
- forkJoin({
- influencers:
- (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) &&
- anomalyChartRecords !== undefined &&
- anomalyChartRecords.length > 0
- ? memoizedLoadFilteredTopInfluencers(
- lastRefresh,
- jobIds,
- timerange.earliestMs,
- timerange.latestMs,
- anomalyChartRecords,
- selectionInfluencers,
- noInfluencersConfigured,
- influencersFilterQuery
- )
- : Promise.resolve(influencers),
- viewBySwimlaneState: memoizedLoadViewBySwimlane(
- lastRefresh,
- topFieldValues,
- {
- earliest: overallState.overallSwimlaneData.earliest,
- latest: overallState.overallSwimlaneData.latest,
- },
- selectedJobs,
- viewBySwimlaneFieldName,
- swimlaneLimit,
- influencersFilterQuery,
- noInfluencersConfigured
- ),
- }),
- (
- { annotationsData, overallState, tableData },
- { influencers, viewBySwimlaneState }
- ): Partial => {
- return {
- annotationsData,
- influencers,
- ...overallState,
- ...viewBySwimlaneState,
- tableData,
- };
- }
- )
- );
-}
-
-const loadExplorerData$ = new Subject();
-const explorerData$ = loadExplorerData$.pipe(
- switchMap((config: LoadExplorerDataConfig) => loadExplorerData(config))
-);
-
+ tableSeverity
+ );
+ }
+ }),
+ // Load view-by swimlane data and filtered top influencers.
+ // mergeMap is used to have access to the already fetched data and act on it in arg #1.
+ // In arg #2 of mergeMap we combine the data and pass it on in the action format
+ // which can be consumed by explorerReducer() later on.
+ mergeMap(
+ ({ anomalyChartRecords, influencers, overallState, topFieldValues }) =>
+ forkJoin({
+ influencers:
+ (selectionInfluencers.length > 0 || influencersFilterQuery !== undefined) &&
+ anomalyChartRecords !== undefined &&
+ anomalyChartRecords.length > 0
+ ? memoizedLoadFilteredTopInfluencers(
+ lastRefresh,
+ jobIds,
+ timerange.earliestMs,
+ timerange.latestMs,
+ anomalyChartRecords,
+ selectionInfluencers,
+ noInfluencersConfigured,
+ influencersFilterQuery
+ )
+ : Promise.resolve(influencers),
+ viewBySwimlaneState: memoizedLoadViewBySwimlane(
+ lastRefresh,
+ topFieldValues,
+ {
+ earliest: overallState.earliest,
+ latest: overallState.latest,
+ },
+ selectedJobs,
+ viewBySwimlaneFieldName,
+ ANOMALY_SWIM_LANE_HARD_LIMIT,
+ viewByPerPage,
+ viewByFromPage,
+ swimlaneContainerWidth,
+ influencersFilterQuery
+ ),
+ }),
+ (
+ { annotationsData, overallState, tableData },
+ { influencers, viewBySwimlaneState }
+ ): Partial => {
+ return {
+ annotationsData,
+ influencers,
+ loading: false,
+ viewBySwimlaneDataLoading: false,
+ overallSwimlaneData: overallState,
+ viewBySwimlaneData: viewBySwimlaneState,
+ tableData,
+ swimlaneLimit: isViewBySwimLaneData(viewBySwimlaneState)
+ ? viewBySwimlaneState.cardinality
+ : undefined,
+ };
+ }
+ )
+ );
+ };
+};
export const useExplorerData = (): [Partial | undefined, (d: any) => void] => {
+ const timefilter = useTimefilter();
+
+ const {
+ services: {
+ mlServices: { mlApiServices },
+ uiSettings,
+ },
+ } = useMlKibana();
+ const loadExplorerData = useMemo(() => {
+ const service = new AnomalyTimelineService(
+ timefilter,
+ uiSettings,
+ mlResultsServiceProvider(mlApiServices)
+ );
+ return loadExplorerDataProvider(service);
+ }, []);
+ const loadExplorerData$ = useMemo(() => new Subject(), []);
+ const explorerData$ = useMemo(() => loadExplorerData$.pipe(switchMap(loadExplorerData)), []);
const explorerData = useObservable(explorerData$);
- return [explorerData, (c) => loadExplorerData$.next(c)];
+
+ const update = useCallback((c) => {
+ loadExplorerData$.next(c);
+ }, []);
+
+ return [explorerData, update];
};
diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
index 16e2fb47a209d..3ad749c9d0631 100644
--- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx
@@ -52,7 +52,6 @@ function getDefaultEmbeddablepaPanelConfig(jobIds: JobId[]) {
interface AddToDashboardControlProps {
jobIds: JobId[];
viewBy: string;
- limit: number;
onClose: (callback?: () => Promise) => void;
}
@@ -63,7 +62,6 @@ export const AddToDashboardControl: FC = ({
onClose,
jobIds,
viewBy,
- limit,
}) => {
const {
notifications: { toasts },
@@ -141,7 +139,6 @@ export const AddToDashboardControl: FC = ({
jobIds,
swimlaneType,
viewBy,
- limit,
},
};
}
@@ -206,8 +203,8 @@ export const AddToDashboardControl: FC = ({
{
id: SWIMLANE_TYPE.VIEW_BY,
label: i18n.translate('xpack.ml.explorer.viewByFieldLabel', {
- defaultMessage: 'View by {viewByField}, up to {limit} rows',
- values: { viewByField: viewBy, limit },
+ defaultMessage: 'View by {viewByField}',
+ values: { viewByField: viewBy },
}),
},
];
diff --git a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
index b4d32e2af64b8..e00e2e1e1e2eb 100644
--- a/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/anomaly_timeline.tsx
@@ -22,12 +22,11 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { DRAG_SELECT_ACTION, VIEW_BY_JOB_LABEL } from './explorer_constants';
+import { DRAG_SELECT_ACTION, SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from './explorer_constants';
import { AddToDashboardControl } from './add_to_dashboard_control';
import { useMlKibana } from '../contexts/kibana';
import { TimeBuckets } from '../util/time_buckets';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/common';
-import { SelectLimit } from './select_limit';
import {
ALLOW_CELL_RANGE_SELECTION,
dragSelect$,
@@ -36,9 +35,9 @@ import {
import { ExplorerState } from './reducers/explorer_reducer';
import { hasMatchingPoints } from './has_matching_points';
import { ExplorerNoInfluencersFound } from './components/explorer_no_influencers_found/explorer_no_influencers_found';
-import { LoadingIndicator } from '../components/loading_indicator';
import { SwimlaneContainer } from './swimlane_container';
-import { OverallSwimlaneData } from './explorer_utils';
+import { OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils';
+import { NoOverallData } from './components/no_overall_data';
function mapSwimlaneOptionsToEuiOptions(options: string[]) {
return options.map((option) => ({
@@ -132,8 +131,11 @@ export const AnomalyTimeline: FC = React.memo(
viewBySwimlaneDataLoading,
viewBySwimlaneFieldName,
viewBySwimlaneOptions,
- swimlaneLimit,
selectedJobs,
+ viewByFromPage,
+ viewByPerPage,
+ swimlaneLimit,
+ loading,
} = explorerState;
const setSwimlaneSelectActive = useCallback((active: boolean) => {
@@ -159,25 +161,18 @@ export const AnomalyTimeline: FC = React.memo(
}, []);
// Listener for click events in the swimlane to load corresponding anomaly data.
- const swimlaneCellClick = useCallback((selectedCellsUpdate: any) => {
- // If selectedCells is an empty object we clear any existing selection,
- // otherwise we save the new selection in AppState and update the Explorer.
- if (Object.keys(selectedCellsUpdate).length === 0) {
- setSelectedCells();
- } else {
- setSelectedCells(selectedCellsUpdate);
- }
- }, []);
-
- const showOverallSwimlane =
- overallSwimlaneData !== null &&
- overallSwimlaneData.laneLabels &&
- overallSwimlaneData.laneLabels.length > 0;
-
- const showViewBySwimlane =
- viewBySwimlaneData !== null &&
- viewBySwimlaneData.laneLabels &&
- viewBySwimlaneData.laneLabels.length > 0;
+ const swimlaneCellClick = useCallback(
+ (selectedCellsUpdate: any) => {
+ // If selectedCells is an empty object we clear any existing selection,
+ // otherwise we save the new selection in AppState and update the Explorer.
+ if (Object.keys(selectedCellsUpdate).length === 0) {
+ setSelectedCells();
+ } else {
+ setSelectedCells(selectedCellsUpdate);
+ }
+ },
+ [setSelectedCells]
+ );
const menuItems = useMemo(() => {
const items = [];
@@ -235,21 +230,6 @@ export const AnomalyTimeline: FC = React.memo(
/>
-
-
-
-
- }
- display={'columnCompressed'}
- >
-
-
-
{viewByLoadedForTimeFormatted && (
@@ -305,68 +285,84 @@ export const AnomalyTimeline: FC
= React.memo(
- {showOverallSwimlane && (
- explorerService.setSwimlaneContainerWidth(width)}
- />
- )}
+ explorerService.setSwimlaneContainerWidth(width)}
+ isLoading={loading}
+ noDataWarning={}
+ />
+
+
{viewBySwimlaneOptions.length > 0 && (
<>
- {showViewBySwimlane && (
- <>
-
-
-
+
+ explorerService.setSwimlaneContainerWidth(width)}
+ fromPage={viewByFromPage}
+ perPage={viewByPerPage}
+ swimlaneLimit={swimlaneLimit}
+ onPaginationChange={({ perPage: perPageUpdate, fromPage: fromPageUpdate }) => {
+ if (perPageUpdate) {
+ explorerService.setViewByPerPage(perPageUpdate);
}
- timeBuckets={timeBuckets}
- swimlaneCellClick={swimlaneCellClick}
- swimlaneData={viewBySwimlaneData as OverallSwimlaneData}
- swimlaneType={'viewBy'}
- selection={selectedCells}
- swimlaneRenderDoneListener={swimlaneRenderDoneListener}
- onResize={(width) => explorerService.setSwimlaneContainerWidth(width)}
- />
-
- >
- )}
-
- {viewBySwimlaneDataLoading && }
-
- {!showViewBySwimlane &&
- !viewBySwimlaneDataLoading &&
- typeof viewBySwimlaneFieldName === 'string' && (
-
+ ) : (
+
+ )
+ ) : null
+ }
/>
- )}
+
+ >
>
)}
@@ -380,7 +376,6 @@ export const AnomalyTimeline: FC = React.memo(
}}
jobIds={selectedJobs.map(({ id }) => id)}
viewBy={viewBySwimlaneFieldName!}
- limit={swimlaneLimit}
/>
)}
>
diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap
index 3ba4ebb2acdea..d3190d2ac1dad 100644
--- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap
+++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/__snapshots__/explorer_no_influencers_found.test.js.snap
@@ -1,20 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExplorerNoInfluencersFound snapshot 1`] = `
-
-
-
+
`;
diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx
index 639c0f7b78504..24def01108584 100644
--- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_influencers_found/explorer_no_influencers_found.tsx
@@ -7,7 +7,6 @@
import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiEmptyPrompt } from '@elastic/eui';
/*
* React component for rendering EuiEmptyPrompt when no influencers were found.
@@ -15,26 +14,17 @@ import { EuiEmptyPrompt } from '@elastic/eui';
export const ExplorerNoInfluencersFound: FC<{
viewBySwimlaneFieldName: string;
showFilterMessage?: boolean;
-}> = ({ viewBySwimlaneFieldName, showFilterMessage = false }) => (
-
- {showFilterMessage === false && (
-
- )}
- {showFilterMessage === true && (
-
- )}
-
- }
- />
-);
+}> = ({ viewBySwimlaneFieldName, showFilterMessage = false }) =>
+ showFilterMessage === false ? (
+
+ ) : (
+
+ );
diff --git a/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx
new file mode 100644
index 0000000000000..e73aac66a0d9f
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx
@@ -0,0 +1,17 @@
+/*
+ * 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, { FC } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+export const NoOverallData: FC = () => {
+ return (
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js
index 71c96840d1b57..df4cea0c07987 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer.js
@@ -12,8 +12,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { Subject } from 'rxjs';
-import { takeUntil } from 'rxjs/operators';
import {
EuiFlexGroup,
@@ -27,6 +25,7 @@ import {
EuiPageHeaderSection,
EuiSpacer,
EuiTitle,
+ EuiLoadingContent,
} from '@elastic/eui';
import { AnnotationFlyout } from '../components/annotations/annotation_flyout';
@@ -36,12 +35,10 @@ import { DatePickerWrapper } from '../components/navigation_menu/date_picker_wra
import { InfluencersList } from '../components/influencers_list';
import { explorerService } from './explorer_dashboard_service';
import { AnomalyResultsViewSelector } from '../components/anomaly_results_view_selector';
-import { LoadingIndicator } from '../components/loading_indicator/loading_indicator';
import { NavigationMenu } from '../components/navigation_menu';
import { CheckboxShowCharts } from '../components/controls/checkbox_showcharts';
import { JobSelector } from '../components/job_selector';
import { SelectInterval } from '../components/controls/select_interval/select_interval';
-import { limit$ } from './select_limit/select_limit';
import { SelectSeverity } from '../components/controls/select_severity/select_severity';
import {
ExplorerQueryBar,
@@ -142,19 +139,6 @@ export class Explorer extends React.Component {
state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG };
- _unsubscribeAll = new Subject();
-
- componentDidMount() {
- limit$.pipe(takeUntil(this._unsubscribeAll)).subscribe(explorerService.setSwimlaneLimit);
- }
-
- componentWillUnmount() {
- this._unsubscribeAll.next();
- this._unsubscribeAll.complete();
- }
-
- viewByChangeHandler = (e) => explorerService.setViewBySwimlaneFieldName(e.target.value);
-
// Escape regular parens from fieldName as that portion of the query is not wrapped in double quotes
// and will cause a syntax error when called with getKqlQueryValues
applyFilter = (fieldName, fieldValue, action) => {
@@ -240,29 +224,7 @@ export class Explorer extends React.Component {
const noJobsFound = selectedJobs === null || selectedJobs.length === 0;
const hasResults = overallSwimlaneData.points && overallSwimlaneData.points.length > 0;
- if (loading === true) {
- return (
-
-
-
- );
- }
-
- if (noJobsFound) {
+ if (noJobsFound && !loading) {
return (
@@ -270,7 +232,7 @@ export class Explorer extends React.Component {
);
}
- if (noJobsFound && hasResults === false) {
+ if (noJobsFound && hasResults === false && !loading) {
return (
@@ -320,7 +282,11 @@ export class Explorer extends React.Component {
/>
-
+ {loading ? (
+
+ ) : (
+
+ )}
)}
@@ -352,59 +318,59 @@ export class Explorer extends React.Component {
>
)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
- {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && (
-
-
-
-
-
- )}
-
-
-
-
-
- {showCharts && }
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && (
+
+
+
+
+
+ )}
+
+
+
+ {showCharts && }
+
+
+ >
+ )}
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
index d1adf8c7ad744..21e13cb029d69 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts
@@ -27,9 +27,10 @@ export const EXPLORER_ACTION = {
SET_INFLUENCER_FILTER_SETTINGS: 'setInfluencerFilterSettings',
SET_SELECTED_CELLS: 'setSelectedCells',
SET_SWIMLANE_CONTAINER_WIDTH: 'setSwimlaneContainerWidth',
- SET_SWIMLANE_LIMIT: 'setSwimlaneLimit',
SET_VIEW_BY_SWIMLANE_FIELD_NAME: 'setViewBySwimlaneFieldName',
SET_VIEW_BY_SWIMLANE_LOADING: 'setViewBySwimlaneLoading',
+ SET_VIEW_BY_PER_PAGE: 'setViewByPerPage',
+ SET_VIEW_BY_FROM_PAGE: 'setViewByFromPage',
};
export const FILTER_ACTION = {
@@ -51,9 +52,23 @@ export const CHART_TYPE = {
};
export const MAX_CATEGORY_EXAMPLES = 10;
+
+/**
+ * Maximum amount of top influencer to fetch.
+ */
export const MAX_INFLUENCER_FIELD_VALUES = 10;
export const MAX_INFLUENCER_FIELD_NAMES = 50;
export const VIEW_BY_JOB_LABEL = i18n.translate('xpack.ml.explorer.jobIdLabel', {
defaultMessage: 'job ID',
});
+/**
+ * Hard limitation for the size of terms
+ * aggregations on influencers values.
+ */
+export const ANOMALY_SWIM_LANE_HARD_LIMIT = 1000;
+
+/**
+ * Default page size fot the anomaly swim lane.
+ */
+export const SWIM_LANE_DEFAULT_PAGE_SIZE = 10;
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts
index 30ab918983a77..1429bf0858361 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts
@@ -12,7 +12,7 @@
import { isEqual } from 'lodash';
import { from, isObservable, Observable, Subject } from 'rxjs';
-import { distinctUntilChanged, flatMap, map, scan } from 'rxjs/operators';
+import { distinctUntilChanged, flatMap, map, scan, shareReplay } from 'rxjs/operators';
import { DeepPartial } from '../../../common/types/common';
@@ -49,7 +49,9 @@ const explorerFilteredAction$ = explorerAction$.pipe(
// applies action and returns state
const explorerState$: Observable = explorerFilteredAction$.pipe(
- scan(explorerReducer, getExplorerDefaultState())
+ scan(explorerReducer, getExplorerDefaultState()),
+ // share the last emitted value among new subscribers
+ shareReplay(1)
);
interface ExplorerAppState {
@@ -59,6 +61,8 @@ interface ExplorerAppState {
selectedTimes?: number[];
showTopFieldValues?: boolean;
viewByFieldName?: string;
+ viewByPerPage?: number;
+ viewByFromPage?: number;
};
mlExplorerFilter: {
influencersFilterQuery?: unknown;
@@ -88,6 +92,14 @@ const explorerAppState$: Observable = explorerState$.pipe(
appState.mlExplorerSwimlane.viewByFieldName = state.viewBySwimlaneFieldName;
}
+ if (state.viewByFromPage !== undefined) {
+ appState.mlExplorerSwimlane.viewByFromPage = state.viewByFromPage;
+ }
+
+ if (state.viewByPerPage !== undefined) {
+ appState.mlExplorerSwimlane.viewByPerPage = state.viewByPerPage;
+ }
+
if (state.filterActive) {
appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery;
appState.mlExplorerFilter.filterActive = state.filterActive;
@@ -153,13 +165,16 @@ export const explorerService = {
payload,
});
},
- setSwimlaneLimit: (payload: number) => {
- explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIMLANE_LIMIT, payload });
- },
setViewBySwimlaneFieldName: (payload: string) => {
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME, payload });
},
setViewBySwimlaneLoading: (payload: any) => {
explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_LOADING, payload });
},
+ setViewByFromPage: (payload: number) => {
+ explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_FROM_PAGE, payload });
+ },
+ setViewByPerPage: (payload: number) => {
+ explorerAction$.next({ type: EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE, payload });
+ },
};
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
index 4e6dcdcc5129c..aa386288ac7e0 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_swimlane.tsx
@@ -29,7 +29,7 @@ import {
ChartTooltipService,
ChartTooltipValue,
} from '../components/chart_tooltip/chart_tooltip_service';
-import { OverallSwimlaneData } from './explorer_utils';
+import { OverallSwimlaneData, ViewBySwimLaneData } from './explorer_utils';
const SCSS = {
mlDragselectDragging: 'mlDragselectDragging',
@@ -57,7 +57,7 @@ export interface ExplorerSwimlaneProps {
maskAll?: boolean;
timeBuckets: InstanceType;
swimlaneCellClick?: Function;
- swimlaneData: OverallSwimlaneData;
+ swimlaneData: OverallSwimlaneData | ViewBySwimLaneData;
swimlaneType: SwimlaneType;
selection?: {
lanes: any[];
@@ -211,7 +211,7 @@ export class ExplorerSwimlane extends React.Component {
const { swimlaneType } = this.props;
// This selects both overall and viewby swimlane
- const wrapper = d3.selectAll('.ml-explorer-swimlane');
+ const wrapper = d3.selectAll('.mlExplorerSwimlane');
wrapper.selectAll('.lane-label').classed('lane-label-masked', true);
wrapper
@@ -242,7 +242,7 @@ export class ExplorerSwimlane extends React.Component {
maskIrrelevantSwimlanes(maskAll: boolean) {
if (maskAll === true) {
// This selects both overall and viewby swimlane
- const allSwimlanes = d3.selectAll('.ml-explorer-swimlane');
+ const allSwimlanes = d3.selectAll('.mlExplorerSwimlane');
allSwimlanes.selectAll('.lane-label').classed('lane-label-masked', true);
allSwimlanes
.selectAll('.sl-cell-inner,.sl-cell-inner-dragselect')
@@ -258,7 +258,7 @@ export class ExplorerSwimlane extends React.Component {
clearSelection() {
// This selects both overall and viewby swimlane
- const wrapper = d3.selectAll('.ml-explorer-swimlane');
+ const wrapper = d3.selectAll('.mlExplorerSwimlane');
wrapper.selectAll('.lane-label').classed('lane-label-masked', false);
wrapper.selectAll('.sl-cell-inner').classed('sl-cell-inner-masked', false);
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts
index 2d49fa737cef6..05fdb52e1ccb2 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.d.ts
@@ -8,8 +8,6 @@ import { Moment } from 'moment';
import { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
-import { TimeBucketsInterval } from '../util/time_buckets';
-
interface ClearedSelectedAnomaliesState {
selectedCells: undefined;
viewByLoadedForTimeFormatted: null;
@@ -35,6 +33,10 @@ export declare interface OverallSwimlaneData extends SwimlaneData {
latest: number;
}
+export interface ViewBySwimLaneData extends OverallSwimlaneData {
+ cardinality: number;
+}
+
export declare const getDateFormatTz: () => any;
export declare const getDefaultSwimlaneData: () => SwimlaneData;
@@ -163,22 +165,6 @@ declare interface LoadOverallDataResponse {
overallSwimlaneData: OverallSwimlaneData;
}
-export declare const loadOverallData: (
- selectedJobs: ExplorerJob[],
- interval: TimeBucketsInterval,
- bounds: TimeRangeBounds
-) => Promise;
-
-export declare const loadViewBySwimlane: (
- fieldValues: string[],
- bounds: SwimlaneBounds,
- selectedJobs: ExplorerJob[],
- viewBySwimlaneFieldName: string,
- swimlaneLimit: number,
- influencersFilterQuery: any,
- noInfluencersConfigured: boolean
-) => Promise;
-
export declare const loadViewByTopFieldValuesForSelectedTime: (
earliestMs: number,
latestMs: number,
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js
index bd6a7ee59c942..23da9669ee9a5 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.js
@@ -8,11 +8,9 @@
* utils for Anomaly Explorer.
*/
-import { chain, each, get, union, uniq } from 'lodash';
+import { chain, get, union, uniq } from 'lodash';
import moment from 'moment-timezone';
-import { i18n } from '@kbn/i18n';
-
import {
ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE,
ANOMALIES_TABLE_DEFAULT_QUERY_SIZE,
@@ -27,7 +25,7 @@ import { parseInterval } from '../../../common/util/parse_interval';
import { ml } from '../services/ml_api_service';
import { mlJobService } from '../services/job_service';
import { mlResultsService } from '../services/results_service';
-import { getBoundsRoundedToInterval, getTimeBucketsFromCache } from '../util/time_buckets';
+import { getTimeBucketsFromCache } from '../util/time_buckets';
import { getTimefilter, getUiSettings } from '../util/dependency_cache';
import {
@@ -36,7 +34,6 @@ import {
SWIMLANE_TYPE,
VIEW_BY_JOB_LABEL,
} from './explorer_constants';
-import { getSwimlaneContainerWidth } from './legacy_utils';
// create new job objects based on standard job config objects
// new job objects just contain job id, bucket span in seconds and a selected flag.
@@ -51,6 +48,7 @@ export function getClearedSelectedAnomaliesState() {
return {
selectedCells: undefined,
viewByLoadedForTimeFormatted: null,
+ swimlaneLimit: undefined,
};
}
@@ -267,58 +265,6 @@ export function getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth)
return buckets.getInterval();
}
-export function loadViewByTopFieldValuesForSelectedTime(
- earliestMs,
- latestMs,
- selectedJobs,
- viewBySwimlaneFieldName,
- swimlaneLimit,
- noInfluencersConfigured
-) {
- const selectedJobIds = selectedJobs.map((d) => d.id);
-
- // Find the top field values for the selected time, and then load the 'view by'
- // swimlane over the full time range for those specific field values.
- return new Promise((resolve) => {
- if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
- mlResultsService
- .getTopInfluencers(selectedJobIds, earliestMs, latestMs, swimlaneLimit)
- .then((resp) => {
- if (resp.influencers[viewBySwimlaneFieldName] === undefined) {
- resolve([]);
- }
-
- const topFieldValues = [];
- const topInfluencers = resp.influencers[viewBySwimlaneFieldName];
- if (Array.isArray(topInfluencers)) {
- topInfluencers.forEach((influencerData) => {
- if (influencerData.maxAnomalyScore > 0) {
- topFieldValues.push(influencerData.influencerFieldValue);
- }
- });
- }
- resolve(topFieldValues);
- });
- } else {
- mlResultsService
- .getScoresByBucket(
- selectedJobIds,
- earliestMs,
- latestMs,
- getSwimlaneBucketInterval(
- selectedJobs,
- getSwimlaneContainerWidth(noInfluencersConfigured)
- ).asSeconds() + 's',
- swimlaneLimit
- )
- .then((resp) => {
- const topFieldValues = Object.keys(resp.results);
- resolve(topFieldValues);
- });
- }
- });
-}
-
// Obtain the list of 'View by' fields per job and viewBySwimlaneFieldName
export function getViewBySwimlaneOptions({
currentViewBySwimlaneFieldName,
@@ -435,105 +381,6 @@ export function getViewBySwimlaneOptions({
};
}
-export function processOverallResults(scoresByTime, searchBounds, interval) {
- const overallLabel = i18n.translate('xpack.ml.explorer.overallLabel', {
- defaultMessage: 'Overall',
- });
- const dataset = {
- laneLabels: [overallLabel],
- points: [],
- interval,
- earliest: searchBounds.min.valueOf() / 1000,
- latest: searchBounds.max.valueOf() / 1000,
- };
-
- if (Object.keys(scoresByTime).length > 0) {
- // Store the earliest and latest times of the data returned by the ES aggregations,
- // These will be used for calculating the earliest and latest times for the swimlane charts.
- each(scoresByTime, (score, timeMs) => {
- const time = timeMs / 1000;
- dataset.points.push({
- laneLabel: overallLabel,
- time,
- value: score,
- });
-
- dataset.earliest = Math.min(time, dataset.earliest);
- dataset.latest = Math.max(time + dataset.interval, dataset.latest);
- });
- }
-
- return dataset;
-}
-
-export function processViewByResults(
- scoresByInfluencerAndTime,
- sortedLaneValues,
- bounds,
- viewBySwimlaneFieldName,
- interval
-) {
- // Processes the scores for the 'view by' swimlane.
- // Sorts the lanes according to the supplied array of lane
- // values in the order in which they should be displayed,
- // or pass an empty array to sort lanes according to max score over all time.
- const dataset = {
- fieldName: viewBySwimlaneFieldName,
- points: [],
- interval,
- };
-
- // Set the earliest and latest to be the same as the overall swimlane.
- dataset.earliest = bounds.earliest;
- dataset.latest = bounds.latest;
-
- const laneLabels = [];
- const maxScoreByLaneLabel = {};
-
- each(scoresByInfluencerAndTime, (influencerData, influencerFieldValue) => {
- laneLabels.push(influencerFieldValue);
- maxScoreByLaneLabel[influencerFieldValue] = 0;
-
- each(influencerData, (anomalyScore, timeMs) => {
- const time = timeMs / 1000;
- dataset.points.push({
- laneLabel: influencerFieldValue,
- time,
- value: anomalyScore,
- });
- maxScoreByLaneLabel[influencerFieldValue] = Math.max(
- maxScoreByLaneLabel[influencerFieldValue],
- anomalyScore
- );
- });
- });
-
- const sortValuesLength = sortedLaneValues.length;
- if (sortValuesLength === 0) {
- // Sort lanes in descending order of max score.
- // Note the keys in scoresByInfluencerAndTime received from the ES request
- // are not guaranteed to be sorted by score if they can be parsed as numbers
- // (e.g. if viewing by HTTP response code).
- dataset.laneLabels = laneLabels.sort((a, b) => {
- return maxScoreByLaneLabel[b] - maxScoreByLaneLabel[a];
- });
- } else {
- // Sort lanes according to supplied order
- // e.g. when a cell in the overall swimlane has been selected.
- // Find the index of each lane label from the actual data set,
- // rather than using sortedLaneValues as-is, just in case they differ.
- dataset.laneLabels = laneLabels.sort((a, b) => {
- let aIndex = sortedLaneValues.indexOf(a);
- let bIndex = sortedLaneValues.indexOf(b);
- aIndex = aIndex > -1 ? aIndex : sortValuesLength;
- bIndex = bIndex > -1 ? bIndex : sortValuesLength;
- return aIndex - bIndex;
- });
- }
-
- return dataset;
-}
-
export function loadAnnotationsTableData(selectedCells, selectedJobs, interval, bounds) {
const jobIds =
selectedCells !== undefined && selectedCells.viewByFieldName === VIEW_BY_JOB_LABEL
@@ -723,138 +570,6 @@ export async function loadDataForCharts(
});
}
-export function loadOverallData(selectedJobs, interval, bounds) {
- return new Promise((resolve) => {
- // Loads the overall data components i.e. the overall swimlane and influencers list.
- if (selectedJobs === null) {
- resolve({
- loading: false,
- hasResuts: false,
- });
- return;
- }
-
- // Ensure the search bounds align to the bucketing interval used in the swimlane so
- // that the first and last buckets are complete.
- const searchBounds = getBoundsRoundedToInterval(bounds, interval, false);
- const selectedJobIds = selectedJobs.map((d) => d.id);
-
- // Load the overall bucket scores by time.
- // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets
- // which wouldn't be the case if e.g. '1M' was used.
- // Pass 'true' when obtaining bucket bounds due to the way the overall_buckets endpoint works
- // to ensure the search is inclusive of end time.
- const overallBucketsBounds = getBoundsRoundedToInterval(bounds, interval, true);
- mlResultsService
- .getOverallBucketScores(
- selectedJobIds,
- // Note there is an optimization for when top_n == 1.
- // If top_n > 1, we should test what happens when the request takes long
- // and refactor the loading calls, if necessary, to avoid delays in loading other components.
- 1,
- overallBucketsBounds.min.valueOf(),
- overallBucketsBounds.max.valueOf(),
- interval.asSeconds() + 's'
- )
- .then((resp) => {
- const overallSwimlaneData = processOverallResults(
- resp.results,
- searchBounds,
- interval.asSeconds()
- );
-
- resolve({
- loading: false,
- overallSwimlaneData,
- });
- });
- });
-}
-
-export function loadViewBySwimlane(
- fieldValues,
- bounds,
- selectedJobs,
- viewBySwimlaneFieldName,
- swimlaneLimit,
- influencersFilterQuery,
- noInfluencersConfigured
-) {
- return new Promise((resolve) => {
- const finish = (resp) => {
- if (resp !== undefined) {
- const viewBySwimlaneData = processViewByResults(
- resp.results,
- fieldValues,
- bounds,
- viewBySwimlaneFieldName,
- getSwimlaneBucketInterval(
- selectedJobs,
- getSwimlaneContainerWidth(noInfluencersConfigured)
- ).asSeconds()
- );
-
- resolve({
- viewBySwimlaneData,
- viewBySwimlaneDataLoading: false,
- });
- } else {
- resolve({ viewBySwimlaneDataLoading: false });
- }
- };
-
- if (selectedJobs === undefined || viewBySwimlaneFieldName === undefined) {
- finish();
- return;
- } else {
- // Ensure the search bounds align to the bucketing interval used in the swimlane so
- // that the first and last buckets are complete.
- const timefilter = getTimefilter();
- const timefilterBounds = timefilter.getActiveBounds();
- const searchBounds = getBoundsRoundedToInterval(
- timefilterBounds,
- getSwimlaneBucketInterval(selectedJobs, getSwimlaneContainerWidth(noInfluencersConfigured)),
- false
- );
- const selectedJobIds = selectedJobs.map((d) => d.id);
-
- // load scores by influencer/jobId value and time.
- // Pass the interval in seconds as the swimlane relies on a fixed number of seconds between buckets
- // which wouldn't be the case if e.g. '1M' was used.
- const interval = `${getSwimlaneBucketInterval(
- selectedJobs,
- getSwimlaneContainerWidth(noInfluencersConfigured)
- ).asSeconds()}s`;
- if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
- mlResultsService
- .getInfluencerValueMaxScoreByTime(
- selectedJobIds,
- viewBySwimlaneFieldName,
- fieldValues,
- searchBounds.min.valueOf(),
- searchBounds.max.valueOf(),
- interval,
- swimlaneLimit,
- influencersFilterQuery
- )
- .then(finish);
- } else {
- const jobIds =
- fieldValues !== undefined && fieldValues.length > 0 ? fieldValues : selectedJobIds;
- mlResultsService
- .getScoresByBucket(
- jobIds,
- searchBounds.min.valueOf(),
- searchBounds.max.valueOf(),
- interval,
- swimlaneLimit
- )
- .then(finish);
- }
- }
- });
-}
-
export async function loadTopInfluencers(
selectedJobIds,
earliestMs,
@@ -871,6 +586,8 @@ export async function loadTopInfluencers(
earliestMs,
latestMs,
MAX_INFLUENCER_FIELD_VALUES,
+ 10,
+ 1,
influencers,
influencersFilterQuery
)
diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
index a19750494afdc..068f43a140c90 100644
--- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
+++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { useCallback, useMemo } from 'react';
import { useUrlState } from '../../util/url_state';
import { SWIMLANE_TYPE } from '../explorer_constants';
import { AppStateSelectedCells } from '../explorer_utils';
@@ -14,55 +15,55 @@ export const useSelectedCells = (): [
] => {
const [appState, setAppState] = useUrlState('_a');
- let selectedCells: AppStateSelectedCells | undefined;
-
// keep swimlane selection, restore selectedCells from AppState
- if (
- appState &&
- appState.mlExplorerSwimlane &&
- appState.mlExplorerSwimlane.selectedType !== undefined
- ) {
- selectedCells = {
- type: appState.mlExplorerSwimlane.selectedType,
- lanes: appState.mlExplorerSwimlane.selectedLanes,
- times: appState.mlExplorerSwimlane.selectedTimes,
- showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues,
- viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName,
- };
- }
+ const selectedCells = useMemo(() => {
+ return appState?.mlExplorerSwimlane?.selectedType !== undefined
+ ? {
+ type: appState.mlExplorerSwimlane.selectedType,
+ lanes: appState.mlExplorerSwimlane.selectedLanes,
+ times: appState.mlExplorerSwimlane.selectedTimes,
+ showTopFieldValues: appState.mlExplorerSwimlane.showTopFieldValues,
+ viewByFieldName: appState.mlExplorerSwimlane.viewByFieldName,
+ }
+ : undefined;
+ // TODO fix appState to use memoization
+ }, [JSON.stringify(appState?.mlExplorerSwimlane)]);
- const setSelectedCells = (swimlaneSelectedCells: AppStateSelectedCells) => {
- const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane };
+ const setSelectedCells = useCallback(
+ (swimlaneSelectedCells: AppStateSelectedCells) => {
+ const mlExplorerSwimlane = { ...appState.mlExplorerSwimlane };
- if (swimlaneSelectedCells !== undefined) {
- swimlaneSelectedCells.showTopFieldValues = false;
+ if (swimlaneSelectedCells !== undefined) {
+ swimlaneSelectedCells.showTopFieldValues = false;
- const currentSwimlaneType = selectedCells?.type;
- const currentShowTopFieldValues = selectedCells?.showTopFieldValues;
- const newSwimlaneType = swimlaneSelectedCells?.type;
+ const currentSwimlaneType = selectedCells?.type;
+ const currentShowTopFieldValues = selectedCells?.showTopFieldValues;
+ const newSwimlaneType = swimlaneSelectedCells?.type;
- if (
- (currentSwimlaneType === SWIMLANE_TYPE.OVERALL &&
- newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) ||
- newSwimlaneType === SWIMLANE_TYPE.OVERALL ||
- currentShowTopFieldValues === true
- ) {
- swimlaneSelectedCells.showTopFieldValues = true;
- }
+ if (
+ (currentSwimlaneType === SWIMLANE_TYPE.OVERALL &&
+ newSwimlaneType === SWIMLANE_TYPE.VIEW_BY) ||
+ newSwimlaneType === SWIMLANE_TYPE.OVERALL ||
+ currentShowTopFieldValues === true
+ ) {
+ swimlaneSelectedCells.showTopFieldValues = true;
+ }
- mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type;
- mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes;
- mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times;
- mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues;
- setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
- } else {
- delete mlExplorerSwimlane.selectedType;
- delete mlExplorerSwimlane.selectedLanes;
- delete mlExplorerSwimlane.selectedTimes;
- delete mlExplorerSwimlane.showTopFieldValues;
- setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
- }
- };
+ mlExplorerSwimlane.selectedType = swimlaneSelectedCells.type;
+ mlExplorerSwimlane.selectedLanes = swimlaneSelectedCells.lanes;
+ mlExplorerSwimlane.selectedTimes = swimlaneSelectedCells.times;
+ mlExplorerSwimlane.showTopFieldValues = swimlaneSelectedCells.showTopFieldValues;
+ setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
+ } else {
+ delete mlExplorerSwimlane.selectedType;
+ delete mlExplorerSwimlane.selectedLanes;
+ delete mlExplorerSwimlane.selectedTimes;
+ delete mlExplorerSwimlane.showTopFieldValues;
+ setAppState('mlExplorerSwimlane', mlExplorerSwimlane);
+ }
+ },
+ [appState?.mlExplorerSwimlane, selectedCells]
+ );
return [selectedCells, setSelectedCells];
};
diff --git a/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts b/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts
index 3b92ee3fa37f6..b85b0401c45ca 100644
--- a/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts
+++ b/x-pack/plugins/ml/public/application/explorer/legacy_utils.ts
@@ -11,8 +11,3 @@ export function getChartContainerWidth() {
const chartContainer = document.querySelector('.explorer-charts');
return Math.floor((chartContainer && chartContainer.clientWidth) || 0);
}
-
-export function getSwimlaneContainerWidth() {
- const explorerContainer = document.querySelector('.ml-explorer');
- return (explorerContainer && explorerContainer.clientWidth) || 0;
-}
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts
index 1614da14e355a..dd1d0516b6173 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/clear_influencer_filter_settings.ts
@@ -19,5 +19,6 @@ export function clearInfluencerFilterSettings(state: ExplorerState): ExplorerSta
queryString: '',
tableQueryString: '',
...getClearedSelectedAnomaliesState(),
+ viewByFromPage: 1,
};
}
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts
index a26c0564c6b16..49f5794273a04 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/job_selection_change.ts
@@ -17,6 +17,7 @@ export const jobSelectionChange = (state: ExplorerState, payload: ActionPayload)
noInfluencersConfigured: getInfluencers(selectedJobs).length === 0,
overallSwimlaneData: getDefaultSwimlaneData(),
selectedJobs,
+ viewByFromPage: 1,
};
// clear filter if selected jobs have no influencers
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts
index c31b26b7adb7b..c55c06c80ab81 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts
@@ -27,7 +27,7 @@ import { setKqlQueryBarPlaceholder } from './set_kql_query_bar_placeholder';
export const explorerReducer = (state: ExplorerState, nextAction: Action): ExplorerState => {
const { type, payload } = nextAction;
- let nextState;
+ let nextState: ExplorerState;
switch (type) {
case EXPLORER_ACTION.CLEAR_INFLUENCER_FILTER_SETTINGS:
@@ -39,6 +39,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
...state,
...getClearedSelectedAnomaliesState(),
loading: false,
+ viewByFromPage: 1,
selectedJobs: [],
};
break;
@@ -82,22 +83,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
break;
case EXPLORER_ACTION.SET_SWIMLANE_CONTAINER_WIDTH:
- if (state.noInfluencersConfigured === true) {
- // swimlane is full width, minus 30 for the 'no influencers' info icon,
- // minus 170 for the lane labels, minus 50 padding
- nextState = { ...state, swimlaneContainerWidth: payload - 250 };
- } else {
- // swimlane width is 5 sixths of the window,
- // minus 170 for the lane labels, minus 50 padding
- nextState = { ...state, swimlaneContainerWidth: (payload / 6) * 5 - 220 };
- }
- break;
-
- case EXPLORER_ACTION.SET_SWIMLANE_LIMIT:
- nextState = {
- ...state,
- swimlaneLimit: payload,
- };
+ nextState = { ...state, swimlaneContainerWidth: payload };
break;
case EXPLORER_ACTION.SET_VIEW_BY_SWIMLANE_FIELD_NAME:
@@ -117,6 +103,9 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
...getClearedSelectedAnomaliesState(),
maskAll,
viewBySwimlaneFieldName,
+ viewBySwimlaneData: getDefaultSwimlaneData(),
+ viewByFromPage: 1,
+ viewBySwimlaneDataLoading: true,
};
break;
@@ -125,7 +114,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
nextState = {
...state,
annotationsData,
- ...overallState,
+ overallSwimlaneData: overallState,
tableData,
viewBySwimlaneData: {
...getDefaultSwimlaneData(),
@@ -134,6 +123,22 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
};
break;
+ case EXPLORER_ACTION.SET_VIEW_BY_FROM_PAGE:
+ nextState = {
+ ...state,
+ viewByFromPage: payload,
+ };
+ break;
+
+ case EXPLORER_ACTION.SET_VIEW_BY_PER_PAGE:
+ nextState = {
+ ...state,
+ // reset current page on the page size change
+ viewByFromPage: 1,
+ viewByPerPage: payload,
+ };
+ break;
+
default:
nextState = state;
}
@@ -155,7 +160,7 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo
filteredFields: nextState.filteredFields,
isAndOperator: nextState.isAndOperator,
selectedJobs: nextState.selectedJobs,
- selectedCells: nextState.selectedCells,
+ selectedCells: nextState.selectedCells!,
});
const { bounds, selectedCells } = nextState;
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts
index 819f6ca1cac92..be87de7da8c88 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/set_influencer_filter_settings.ts
@@ -57,5 +57,6 @@ export function setInfluencerFilterSettings(
filteredFields.includes(selectedViewByFieldName) === false,
viewBySwimlaneFieldName: selectedViewByFieldName,
viewBySwimlaneOptions: filteredViewBySwimlaneOptions,
+ viewByFromPage: 1,
};
}
diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
index 4e1a2af9b13a6..892b46467345b 100644
--- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
+++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts
@@ -19,7 +19,9 @@ import {
TimeRangeBounds,
OverallSwimlaneData,
SwimlaneData,
+ ViewBySwimLaneData,
} from '../../explorer_utils';
+import { SWIM_LANE_DEFAULT_PAGE_SIZE } from '../../explorer_constants';
export interface ExplorerState {
annotationsData: any[];
@@ -42,14 +44,16 @@ export interface ExplorerState {
selectedJobs: ExplorerJob[] | null;
swimlaneBucketInterval: any;
swimlaneContainerWidth: number;
- swimlaneLimit: number;
tableData: AnomaliesTableData;
tableQueryString: string;
viewByLoadedForTimeFormatted: string | null;
- viewBySwimlaneData: SwimlaneData | OverallSwimlaneData;
+ viewBySwimlaneData: SwimlaneData | ViewBySwimLaneData;
viewBySwimlaneDataLoading: boolean;
viewBySwimlaneFieldName?: string;
+ viewByPerPage: number;
+ viewByFromPage: number;
viewBySwimlaneOptions: string[];
+ swimlaneLimit?: number;
}
function getDefaultIndexPattern() {
@@ -78,7 +82,6 @@ export function getExplorerDefaultState(): ExplorerState {
selectedJobs: null,
swimlaneBucketInterval: undefined,
swimlaneContainerWidth: 0,
- swimlaneLimit: 10,
tableData: {
anomalies: [],
examplesByJobId: [''],
@@ -92,5 +95,8 @@ export function getExplorerDefaultState(): ExplorerState {
viewBySwimlaneDataLoading: false,
viewBySwimlaneFieldName: undefined,
viewBySwimlaneOptions: [],
+ viewByPerPage: SWIM_LANE_DEFAULT_PAGE_SIZE,
+ viewByFromPage: 1,
+ swimlaneLimit: undefined,
};
}
diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/index.ts b/x-pack/plugins/ml/public/application/explorer/select_limit/index.ts
deleted file mode 100644
index 5b7040e5c3606..0000000000000
--- a/x-pack/plugins/ml/public/application/explorer/select_limit/index.ts
+++ /dev/null
@@ -1,7 +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.
- */
-
-export { useSwimlaneLimit, SelectLimit } from './select_limit';
diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx
deleted file mode 100644
index cf65419e4bd80..0000000000000
--- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.test.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { act } from 'react-dom/test-utils';
-import { shallow } from 'enzyme';
-import { SelectLimit } from './select_limit';
-
-describe('SelectLimit', () => {
- test('creates correct initial selected value', () => {
- const wrapper = shallow();
- expect(wrapper.props().value).toEqual(10);
- });
-
- test('state for currently selected value is updated correctly on click', () => {
- const wrapper = shallow();
- expect(wrapper.props().value).toEqual(10);
-
- act(() => {
- wrapper.simulate('change', { target: { value: 25 } });
- });
- wrapper.update();
-
- expect(wrapper.props().value).toEqual(10);
- });
-});
diff --git a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx b/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
deleted file mode 100644
index 7a2df1a0f0535..0000000000000
--- a/x-pack/plugins/ml/public/application/explorer/select_limit/select_limit.tsx
+++ /dev/null
@@ -1,40 +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.
- */
-
-/*
- * React component for rendering a select element with limit options.
- */
-import React from 'react';
-import useObservable from 'react-use/lib/useObservable';
-import { BehaviorSubject } from 'rxjs';
-
-import { EuiSelect } from '@elastic/eui';
-
-const limitOptions = [5, 10, 25, 50];
-
-const euiOptions = limitOptions.map((limit) => ({
- value: limit,
- text: `${limit}`,
-}));
-
-export const defaultLimit = limitOptions[1];
-export const limit$ = new BehaviorSubject(defaultLimit);
-
-export const useSwimlaneLimit = (): [number, (newLimit: number) => void] => {
- const limit = useObservable(limit$, defaultLimit);
-
- return [limit!, (newLimit: number) => limit$.next(newLimit)];
-};
-
-export const SelectLimit = () => {
- const [limit, setLimit] = useSwimlaneLimit();
-
- function onChange(e: React.ChangeEvent) {
- setLimit(parseInt(e.target.value, 10));
- }
-
- return ;
-};
diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
index 57d1fd81000b7..e34e1d26c9cab 100644
--- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
+++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx
@@ -5,7 +5,14 @@
*/
import React, { FC, useCallback, useState } from 'react';
-import { EuiResizeObserver, EuiText } from '@elastic/eui';
+import {
+ EuiText,
+ EuiLoadingChart,
+ EuiResizeObserver,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiEmptyPrompt,
+} from '@elastic/eui';
import { throttle } from 'lodash';
import {
@@ -14,48 +21,139 @@ import {
} from '../../application/explorer/explorer_swimlane';
import { MlTooltipComponent } from '../../application/components/chart_tooltip';
+import { SwimLanePagination } from './swimlane_pagination';
+import { SWIMLANE_TYPE } from './explorer_constants';
+import { ViewBySwimLaneData } from './explorer_utils';
+/**
+ * Ignore insignificant resize, e.g. browser scrollbar appearance.
+ */
+const RESIZE_IGNORED_DIFF_PX = 20;
const RESIZE_THROTTLE_TIME_MS = 500;
+export function isViewBySwimLaneData(arg: any): arg is ViewBySwimLaneData {
+ return arg && arg.hasOwnProperty('cardinality');
+}
+
+/**
+ * Anomaly swim lane container responsible for handling resizing, pagination and injecting
+ * tooltip service.
+ *
+ * @param children
+ * @param onResize
+ * @param perPage
+ * @param fromPage
+ * @param swimlaneLimit
+ * @param onPaginationChange
+ * @param props
+ * @constructor
+ */
export const SwimlaneContainer: FC<
Omit & {
onResize: (width: number) => void;
+ fromPage?: number;
+ perPage?: number;
+ swimlaneLimit?: number;
+ onPaginationChange?: (arg: { perPage?: number; fromPage?: number }) => void;
+ isLoading: boolean;
+ noDataWarning: string | JSX.Element | null;
}
-> = ({ children, onResize, ...props }) => {
+> = ({
+ children,
+ onResize,
+ perPage,
+ fromPage,
+ swimlaneLimit,
+ onPaginationChange,
+ isLoading,
+ noDataWarning,
+ ...props
+}) => {
const [chartWidth, setChartWidth] = useState(0);
const resizeHandler = useCallback(
throttle((e: { width: number; height: number }) => {
const labelWidth = 200;
- setChartWidth(e.width - labelWidth);
- onResize(e.width);
+ const resultNewWidth = e.width - labelWidth;
+ if (Math.abs(resultNewWidth - chartWidth) > RESIZE_IGNORED_DIFF_PX) {
+ setChartWidth(resultNewWidth);
+ onResize(resultNewWidth);
+ }
}, RESIZE_THROTTLE_TIME_MS),
- []
+ [chartWidth]
);
+ const showSwimlane =
+ props.swimlaneData &&
+ props.swimlaneData.laneLabels &&
+ props.swimlaneData.laneLabels.length > 0 &&
+ props.swimlaneData.points.length > 0;
+
+ const isPaginationVisible =
+ (showSwimlane || isLoading) &&
+ swimlaneLimit !== undefined &&
+ onPaginationChange &&
+ props.swimlaneType === SWIMLANE_TYPE.VIEW_BY &&
+ fromPage &&
+ perPage;
+
return (
-
- {(resizeRef) => (
- {
- resizeRef(el);
- }}
- >
-
-
-
- {(tooltipService) => (
-
+
+ {(resizeRef) => (
+ {
+ resizeRef(el);
+ }}
+ data-test-subj="mlSwimLaneContainer"
+ >
+
+
+ {showSwimlane && !isLoading && (
+
+ {(tooltipService) => (
+
+ )}
+
+ )}
+ {isLoading && (
+
+
+
+ )}
+ {!isLoading && !showSwimlane && (
+ {noDataWarning}}
/>
)}
-
-
-
-
- )}
-
+
+
+ {isPaginationVisible && (
+
+
+
+ )}
+
+ )}
+
+ >
);
};
diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx
new file mode 100644
index 0000000000000..0607f7fd35fad
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/swimlane_pagination.tsx
@@ -0,0 +1,108 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FC, useCallback, useState } from 'react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPopover,
+ EuiContextMenuPanel,
+ EuiPagination,
+ EuiContextMenuItem,
+ EuiButtonEmpty,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+interface SwimLanePaginationProps {
+ fromPage: number;
+ perPage: number;
+ cardinality: number;
+ onPaginationChange: (arg: { perPage?: number; fromPage?: number }) => void;
+}
+
+export const SwimLanePagination: FC = ({
+ cardinality,
+ fromPage,
+ perPage,
+ onPaginationChange,
+}) => {
+ const componentFromPage = fromPage - 1;
+
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const onButtonClick = () => setIsPopoverOpen(() => !isPopoverOpen);
+ const closePopover = () => setIsPopoverOpen(false);
+
+ const goToPage = useCallback((pageNumber: number) => {
+ onPaginationChange({ fromPage: pageNumber + 1 });
+ }, []);
+
+ const setPerPage = useCallback((perPageUpdate: number) => {
+ onPaginationChange({ perPage: perPageUpdate });
+ }, []);
+
+ const pageCount = Math.ceil(cardinality / perPage);
+
+ const items = [5, 10, 20, 50, 100].map((v) => {
+ return (
+ {
+ closePopover();
+ setPerPage(v);
+ }}
+ >
+
+
+ );
+ });
+
+ return (
+
+
+
+
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ panelPaddingSize="none"
+ >
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
index 2e355c6073abd..52b4408d1ac5b 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -22,7 +22,6 @@ import { ml } from '../../services/ml_api_service';
import { useExplorerData } from '../../explorer/actions';
import { explorerService } from '../../explorer/explorer_dashboard_service';
import { getDateFormatTz } from '../../explorer/explorer_utils';
-import { useSwimlaneLimit } from '../../explorer/select_limit';
import { useJobSelection } from '../../components/job_selector/use_job_selection';
import { useShowCharts } from '../../components/controls/checkbox_showcharts';
import { useTableInterval } from '../../components/controls/select_interval';
@@ -30,6 +29,7 @@ import { useTableSeverity } from '../../components/controls/select_severity';
import { useUrlState } from '../../util/url_state';
import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs';
import { useTimefilter } from '../../contexts/kibana';
+import { isViewBySwimLaneData } from '../../explorer/swimlane_container';
const breadcrumbs = [
ML_BREADCRUMB,
@@ -151,10 +151,6 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim
const [showCharts] = useShowCharts();
const [tableInterval] = useTableInterval();
const [tableSeverity] = useTableSeverity();
- const [swimlaneLimit] = useSwimlaneLimit();
- useEffect(() => {
- explorerService.setSwimlaneLimit(swimlaneLimit);
- }, [swimlaneLimit]);
const [selectedCells, setSelectedCells] = useSelectedCells();
useEffect(() => {
@@ -170,14 +166,26 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim
selectedCells,
selectedJobs: explorerState.selectedJobs,
swimlaneBucketInterval: explorerState.swimlaneBucketInterval,
- swimlaneLimit: explorerState.swimlaneLimit,
tableInterval: tableInterval.val,
tableSeverity: tableSeverity.val,
viewBySwimlaneFieldName: explorerState.viewBySwimlaneFieldName,
+ swimlaneContainerWidth: explorerState.swimlaneContainerWidth,
+ viewByPerPage: explorerState.viewByPerPage,
+ viewByFromPage: explorerState.viewByFromPage,
}) ||
undefined;
+
useEffect(() => {
- loadExplorerData(loadExplorerDataConfig);
+ if (explorerState && explorerState.swimlaneContainerWidth > 0) {
+ loadExplorerData({
+ ...loadExplorerDataConfig,
+ swimlaneLimit:
+ explorerState?.viewBySwimlaneData &&
+ isViewBySwimLaneData(explorerState?.viewBySwimlaneData)
+ ? explorerState?.viewBySwimlaneData.cardinality
+ : undefined,
+ });
+ }
}, [JSON.stringify(loadExplorerDataConfig)]);
if (explorerState === undefined || refresh === undefined || showCharts === undefined) {
diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
index ac4882b0055ae..11ec074bac1db 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
@@ -12,41 +12,47 @@ import { I18nProvider } from '@kbn/i18n/react';
import { TimeSeriesExplorerUrlStateManager } from './timeseriesexplorer';
-jest.mock('../../contexts/kibana/kibana_context', () => ({
- useMlKibana: () => {
- return {
- services: {
- uiSettings: { get: jest.fn() },
- data: {
- query: {
- timefilter: {
+jest.mock('../../contexts/kibana/kibana_context', () => {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { of } = require('rxjs');
+ return {
+ useMlKibana: () => {
+ return {
+ services: {
+ uiSettings: { get: jest.fn() },
+ data: {
+ query: {
timefilter: {
- disableTimeRangeSelector: jest.fn(),
- disableAutoRefreshSelector: jest.fn(),
- enableTimeRangeSelector: jest.fn(),
- enableAutoRefreshSelector: jest.fn(),
- getRefreshInterval: jest.fn(),
- setRefreshInterval: jest.fn(),
- getTime: jest.fn(),
- isAutoRefreshSelectorEnabled: jest.fn(),
- isTimeRangeSelectorEnabled: jest.fn(),
- getRefreshIntervalUpdate$: jest.fn(),
- getTimeUpdate$: jest.fn(),
- getEnabledUpdated$: jest.fn(),
+ timefilter: {
+ disableTimeRangeSelector: jest.fn(),
+ disableAutoRefreshSelector: jest.fn(),
+ enableTimeRangeSelector: jest.fn(),
+ enableAutoRefreshSelector: jest.fn(),
+ getRefreshInterval: jest.fn(),
+ setRefreshInterval: jest.fn(),
+ getTime: jest.fn(),
+ isAutoRefreshSelectorEnabled: jest.fn(),
+ isTimeRangeSelectorEnabled: jest.fn(),
+ getRefreshIntervalUpdate$: jest.fn(),
+ getTimeUpdate$: jest.fn(() => {
+ return of();
+ }),
+ getEnabledUpdated$: jest.fn(),
+ },
+ history: { get: jest.fn() },
},
- history: { get: jest.fn() },
},
},
- },
- notifications: {
- toasts: {
- addDanger: () => {},
+ notifications: {
+ toasts: {
+ addDanger: () => {},
+ },
},
},
- },
- };
- },
-}));
+ };
+ },
+ };
+});
jest.mock('../../util/dependency_cache', () => ({
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
diff --git a/x-pack/plugins/ml/public/application/routing/use_refresh.ts b/x-pack/plugins/ml/public/application/routing/use_refresh.ts
index f0b93c876526b..c247fd9765e96 100644
--- a/x-pack/plugins/ml/public/application/routing/use_refresh.ts
+++ b/x-pack/plugins/ml/public/application/routing/use_refresh.ts
@@ -5,26 +5,40 @@
*/
import { useObservable } from 'react-use';
-import { merge, Observable } from 'rxjs';
-import { map } from 'rxjs/operators';
+import { merge } from 'rxjs';
+import { map, skip } from 'rxjs/operators';
+import { useMemo } from 'react';
import { annotationsRefresh$ } from '../services/annotations_service';
-import {
- mlTimefilterRefresh$,
- mlTimefilterTimeChange$,
-} from '../services/timefilter_refresh_service';
+import { mlTimefilterRefresh$ } from '../services/timefilter_refresh_service';
+import { useTimefilter } from '../contexts/kibana';
export interface Refresh {
lastRefresh: number;
timeRange?: { start: string; end: string };
}
-const refresh$: Observable = merge(
- mlTimefilterRefresh$,
- mlTimefilterTimeChange$,
- annotationsRefresh$.pipe(map((d) => ({ lastRefresh: d })))
-);
-
+/**
+ * Hook that provides the latest refresh timestamp
+ * and the most recent applied time range.
+ */
export const useRefresh = () => {
+ const timefilter = useTimefilter();
+
+ const refresh$ = useMemo(() => {
+ return merge(
+ mlTimefilterRefresh$,
+ timefilter.getTimeUpdate$().pipe(
+ // skip initially emitted value
+ skip(1),
+ map((_) => {
+ const { from, to } = timefilter.getTime();
+ return { lastRefresh: Date.now(), timeRange: { start: from, end: to } };
+ })
+ ),
+ annotationsRefresh$.pipe(map((d) => ({ lastRefresh: d })))
+ );
+ }, []);
+
return useObservable(refresh$);
};
diff --git a/x-pack/plugins/ml/public/application/services/explorer_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
similarity index 82%
rename from x-pack/plugins/ml/public/application/services/explorer_service.ts
rename to x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
index 0944328db0052..f2e362f754f2b 100644
--- a/x-pack/plugins/ml/public/application/services/explorer_service.ts
+++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts
@@ -12,14 +12,19 @@ import {
UI_SETTINGS,
} from '../../../../../../src/plugins/data/public';
import { getBoundsRoundedToInterval, TimeBuckets, TimeRangeBounds } from '../util/time_buckets';
-import { ExplorerJob, OverallSwimlaneData, SwimlaneData } from '../explorer/explorer_utils';
+import {
+ ExplorerJob,
+ OverallSwimlaneData,
+ SwimlaneData,
+ ViewBySwimLaneData,
+} from '../explorer/explorer_utils';
import { VIEW_BY_JOB_LABEL } from '../explorer/explorer_constants';
import { MlResultsService } from './results_service';
/**
- * Anomaly Explorer Service
+ * Service for retrieving anomaly swim lanes data.
*/
-export class ExplorerService {
+export class AnomalyTimelineService {
private timeBuckets: TimeBuckets;
private _customTimeRange: TimeRange | undefined;
@@ -130,12 +135,27 @@ export class ExplorerService {
return overallSwimlaneData;
}
+ /**
+ * Fetches view by swim lane data.
+ *
+ * @param fieldValues
+ * @param bounds
+ * @param selectedJobs
+ * @param viewBySwimlaneFieldName
+ * @param swimlaneLimit
+ * @param perPage
+ * @param fromPage
+ * @param swimlaneContainerWidth
+ * @param influencersFilterQuery
+ */
public async loadViewBySwimlane(
fieldValues: string[],
bounds: { earliest: number; latest: number },
selectedJobs: ExplorerJob[],
viewBySwimlaneFieldName: string,
swimlaneLimit: number,
+ perPage: number,
+ fromPage: number,
swimlaneContainerWidth: number,
influencersFilterQuery?: any
): Promise {
@@ -172,7 +192,8 @@ export class ExplorerService {
searchBounds.min.valueOf(),
searchBounds.max.valueOf(),
interval,
- swimlaneLimit
+ perPage,
+ fromPage
);
} else {
response = await this.mlResultsService.getInfluencerValueMaxScoreByTime(
@@ -183,6 +204,8 @@ export class ExplorerService {
searchBounds.max.valueOf(),
interval,
swimlaneLimit,
+ perPage,
+ fromPage,
influencersFilterQuery
);
}
@@ -193,6 +216,7 @@ export class ExplorerService {
const viewBySwimlaneData = this.processViewByResults(
response.results,
+ response.cardinality,
fieldValues,
bounds,
viewBySwimlaneFieldName,
@@ -204,6 +228,55 @@ export class ExplorerService {
return viewBySwimlaneData;
}
+ public async loadViewByTopFieldValuesForSelectedTime(
+ earliestMs: number,
+ latestMs: number,
+ selectedJobs: ExplorerJob[],
+ viewBySwimlaneFieldName: string,
+ swimlaneLimit: number,
+ perPage: number,
+ fromPage: number,
+ swimlaneContainerWidth: number
+ ) {
+ const selectedJobIds = selectedJobs.map((d) => d.id);
+
+ // Find the top field values for the selected time, and then load the 'view by'
+ // swimlane over the full time range for those specific field values.
+ if (viewBySwimlaneFieldName !== VIEW_BY_JOB_LABEL) {
+ const resp = await this.mlResultsService.getTopInfluencers(
+ selectedJobIds,
+ earliestMs,
+ latestMs,
+ swimlaneLimit,
+ perPage,
+ fromPage
+ );
+ if (resp.influencers[viewBySwimlaneFieldName] === undefined) {
+ return [];
+ }
+
+ const topFieldValues: any[] = [];
+ const topInfluencers = resp.influencers[viewBySwimlaneFieldName];
+ if (Array.isArray(topInfluencers)) {
+ topInfluencers.forEach((influencerData) => {
+ if (influencerData.maxAnomalyScore > 0) {
+ topFieldValues.push(influencerData.influencerFieldValue);
+ }
+ });
+ }
+ return topFieldValues;
+ } else {
+ const resp = await this.mlResultsService.getScoresByBucket(
+ selectedJobIds,
+ earliestMs,
+ latestMs,
+ this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asSeconds() + 's',
+ swimlaneLimit
+ );
+ return Object.keys(resp.results);
+ }
+ }
+
private getTimeBounds(): TimeRangeBounds {
return this._customTimeRange !== undefined
? this.timeFilter.calculateBounds(this._customTimeRange)
@@ -245,6 +318,7 @@ export class ExplorerService {
private processViewByResults(
scoresByInfluencerAndTime: Record,
+ cardinality: number,
sortedLaneValues: string[],
bounds: any,
viewBySwimlaneFieldName: string,
@@ -254,7 +328,7 @@ export class ExplorerService {
// Sorts the lanes according to the supplied array of lane
// values in the order in which they should be displayed,
// or pass an empty array to sort lanes according to max score over all time.
- const dataset: OverallSwimlaneData = {
+ const dataset: ViewBySwimLaneData = {
fieldName: viewBySwimlaneFieldName,
points: [],
laneLabels: [],
@@ -262,6 +336,7 @@ export class ExplorerService {
// Set the earliest and latest to be the same as the overall swim lane.
earliest: bounds.earliest,
latest: bounds.latest,
+ cardinality,
};
const maxScoreByLaneLabel: Record = {};
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
index a618534d7ae00..00adb2d325833 100644
--- a/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.test.ts
@@ -37,7 +37,7 @@ describe('DashboardService', () => {
// assert
expect(mockSavedObjectClient.find).toHaveBeenCalledWith({
type: 'dashboard',
- perPage: 10,
+ perPage: 1000,
search: `test*`,
searchFields: ['title^3', 'description'],
});
diff --git a/x-pack/plugins/ml/public/application/services/dashboard_service.ts b/x-pack/plugins/ml/public/application/services/dashboard_service.ts
index 7f2bb71d18eb9..d6ccfc2f203e9 100644
--- a/x-pack/plugins/ml/public/application/services/dashboard_service.ts
+++ b/x-pack/plugins/ml/public/application/services/dashboard_service.ts
@@ -34,7 +34,7 @@ export function dashboardServiceProvider(
async fetchDashboards(query?: string) {
return await savedObjectClient.find({
type: 'dashboard',
- perPage: 10,
+ perPage: 1000,
search: query ? `${query}*` : '',
searchFields: ['title^3', 'description'],
});
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
index af6944d7ae2d2..d1b6f95f32bed 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts
@@ -12,7 +12,7 @@ import { annotations } from './annotations';
import { dataFrameAnalytics } from './data_frame_analytics';
import { filters } from './filters';
import { resultsApiProvider } from './results';
-import { jobs } from './jobs';
+import { jobsApiProvider } from './jobs';
import { fileDatavisualizer } from './datavisualizer';
import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info';
@@ -726,7 +726,7 @@ export function mlApiServicesProvider(httpService: HttpService) {
dataFrameAnalytics,
filters,
results: resultsApiProvider(httpService),
- jobs,
+ jobs: jobsApiProvider(httpService),
fileDatavisualizer,
};
}
diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
index 6aa62da3f0768..d356fc0ef339b 100644
--- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { http } from '../http_service';
+import { HttpService } from '../http_service';
import { basePath } from './index';
import { Dictionary } from '../../../../common/types/common';
@@ -24,10 +24,10 @@ import {
import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job';
import { Category } from '../../../../common/types/categories';
-export const jobs = {
+export const jobsApiProvider = (httpService: HttpService) => ({
jobsSummary(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/jobs_summary`,
method: 'POST',
body,
@@ -36,7 +36,10 @@ export const jobs = {
jobsWithTimerange(dateFormatTz: string) {
const body = JSON.stringify({ dateFormatTz });
- return http<{ jobs: MlJobWithTimeRange[]; jobsMap: Dictionary }>({
+ return httpService.http<{
+ jobs: MlJobWithTimeRange[];
+ jobsMap: Dictionary;
+ }>({
path: `${basePath()}/jobs/jobs_with_time_range`,
method: 'POST',
body,
@@ -45,7 +48,7 @@ export const jobs = {
jobs(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/jobs`,
method: 'POST',
body,
@@ -53,7 +56,7 @@ export const jobs = {
},
groups() {
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/groups`,
method: 'GET',
});
@@ -61,7 +64,7 @@ export const jobs = {
updateGroups(updatedJobs: string[]) {
const body = JSON.stringify({ jobs: updatedJobs });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/update_groups`,
method: 'POST',
body,
@@ -75,7 +78,7 @@ export const jobs = {
end,
});
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/force_start_datafeeds`,
method: 'POST',
body,
@@ -84,7 +87,7 @@ export const jobs = {
stopDatafeeds(datafeedIds: string[]) {
const body = JSON.stringify({ datafeedIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/stop_datafeeds`,
method: 'POST',
body,
@@ -93,7 +96,7 @@ export const jobs = {
deleteJobs(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/delete_jobs`,
method: 'POST',
body,
@@ -102,7 +105,7 @@ export const jobs = {
closeJobs(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/close_jobs`,
method: 'POST',
body,
@@ -111,7 +114,7 @@ export const jobs = {
forceStopAndCloseJob(jobId: string) {
const body = JSON.stringify({ jobId });
- return http<{ success: boolean }>({
+ return httpService.http<{ success: boolean }>({
path: `${basePath()}/jobs/force_stop_and_close_job`,
method: 'POST',
body,
@@ -121,7 +124,7 @@ export const jobs = {
jobAuditMessages(jobId: string, from?: number) {
const jobIdString = jobId !== undefined ? `/${jobId}` : '';
const query = from !== undefined ? { from } : {};
- return http({
+ return httpService.http({
path: `${basePath()}/job_audit_messages/messages${jobIdString}`,
method: 'GET',
query,
@@ -129,7 +132,7 @@ export const jobs = {
},
deletingJobTasks() {
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/deleting_jobs_tasks`,
method: 'GET',
});
@@ -137,7 +140,7 @@ export const jobs = {
jobsExist(jobIds: string[]) {
const body = JSON.stringify({ jobIds });
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/jobs_exist`,
method: 'POST',
body,
@@ -146,7 +149,7 @@ export const jobs = {
newJobCaps(indexPatternTitle: string, isRollup: boolean = false) {
const query = isRollup === true ? { rollup: true } : {};
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/new_job_caps/${indexPatternTitle}`,
method: 'GET',
query,
@@ -175,7 +178,7 @@ export const jobs = {
splitFieldName,
splitFieldValue,
});
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/new_job_line_chart`,
method: 'POST',
body,
@@ -202,7 +205,7 @@ export const jobs = {
aggFieldNamePairs,
splitFieldName,
});
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/new_job_population_chart`,
method: 'POST',
body,
@@ -210,7 +213,7 @@ export const jobs = {
},
getAllJobAndGroupIds() {
- return http({
+ return httpService.http({
path: `${basePath()}/jobs/all_jobs_and_group_ids`,
method: 'GET',
});
@@ -222,7 +225,7 @@ export const jobs = {
start,
end,
});
- return http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({
+ return httpService.http<{ progress: number; isRunning: boolean; isJobClosed: boolean }>({
path: `${basePath()}/jobs/look_back_progress`,
method: 'POST',
body,
@@ -249,7 +252,7 @@ export const jobs = {
end,
analyzer,
});
- return http<{
+ return httpService.http<{
examples: CategoryFieldExample[];
sampleSize: number;
overallValidStatus: CATEGORY_EXAMPLES_VALIDATION_STATUS;
@@ -263,7 +266,10 @@ export const jobs = {
topCategories(jobId: string, count: number) {
const body = JSON.stringify({ jobId, count });
- return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({
+ return httpService.http<{
+ total: number;
+ categories: Array<{ count?: number; category: Category }>;
+ }>({
path: `${basePath()}/jobs/top_categories`,
method: 'POST',
body,
@@ -278,10 +284,13 @@ export const jobs = {
calendarEvents?: Array<{ start: number; end: number; description: string }>
) {
const body = JSON.stringify({ jobId, snapshotId, replay, end, calendarEvents });
- return http<{ total: number; categories: Array<{ count?: number; category: Category }> }>({
+ return httpService.http<{
+ total: number;
+ categories: Array<{ count?: number; category: Category }>;
+ }>({
path: `${basePath()}/jobs/revert_model_snapshot`,
method: 'POST',
body,
});
},
-};
+});
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
index 1b2c01ab73fce..b26528b76037b 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts
@@ -14,9 +14,19 @@ export function resultsServiceProvider(
earliestMs: number,
latestMs: number,
interval: string | number,
- maxResults: number
+ perPage?: number,
+ fromPage?: number
+ ): Promise;
+ getTopInfluencers(
+ selectedJobIds: string[],
+ earliestMs: number,
+ latestMs: number,
+ maxFieldValues: number,
+ perPage?: number,
+ fromPage?: number,
+ influencers?: any[],
+ influencersFilterQuery?: any
): Promise;
- getTopInfluencers(): Promise;
getTopInfluencerValues(): Promise;
getOverallBucketScores(
jobIds: any,
@@ -33,6 +43,8 @@ export function resultsServiceProvider(
latestMs: number,
interval: string,
maxResults: number,
+ perPage: number,
+ fromPage: number,
influencersFilterQuery: any
): Promise;
getRecordInfluencers(): Promise;
diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
index 9e3fed189b6f4..55ddb1de3529e 100644
--- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js
+++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js
@@ -9,6 +9,10 @@ import _ from 'lodash';
import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils';
import { escapeForElasticsearchQuery } from '../../util/string_utils';
import { ML_RESULTS_INDEX_PATTERN } from '../../../../common/constants/index_patterns';
+import {
+ ANOMALY_SWIM_LANE_HARD_LIMIT,
+ SWIM_LANE_DEFAULT_PAGE_SIZE,
+} from '../../explorer/explorer_constants';
/**
* Service for carrying out Elasticsearch queries to obtain data for the Ml Results dashboards.
@@ -24,7 +28,7 @@ export function resultsServiceProvider(mlApiServices) {
// Pass an empty array or ['*'] to search over all job IDs.
// Returned response contains a results property, with a key for job
// which has results for the specified time range.
- getScoresByBucket(jobIds, earliestMs, latestMs, interval, maxResults) {
+ getScoresByBucket(jobIds, earliestMs, latestMs, interval, perPage = 10, fromPage = 1) {
return new Promise((resolve, reject) => {
const obj = {
success: true,
@@ -88,7 +92,7 @@ export function resultsServiceProvider(mlApiServices) {
jobId: {
terms: {
field: 'job_id',
- size: maxResults !== undefined ? maxResults : 5,
+ size: jobIds?.length ?? 1,
order: {
anomalyScore: 'desc',
},
@@ -99,6 +103,12 @@ export function resultsServiceProvider(mlApiServices) {
field: 'anomaly_score',
},
},
+ bucketTruncate: {
+ bucket_sort: {
+ from: (fromPage - 1) * perPage,
+ size: perPage === 0 ? 1 : perPage,
+ },
+ },
byTime: {
date_histogram: {
field: 'timestamp',
@@ -158,7 +168,9 @@ export function resultsServiceProvider(mlApiServices) {
jobIds,
earliestMs,
latestMs,
- maxFieldValues = 10,
+ maxFieldValues = ANOMALY_SWIM_LANE_HARD_LIMIT,
+ perPage = 10,
+ fromPage = 1,
influencers = [],
influencersFilterQuery
) {
@@ -272,6 +284,12 @@ export function resultsServiceProvider(mlApiServices) {
},
},
aggs: {
+ bucketTruncate: {
+ bucket_sort: {
+ from: (fromPage - 1) * perPage,
+ size: perPage,
+ },
+ },
maxAnomalyScore: {
max: {
field: 'influencer_score',
@@ -472,7 +490,9 @@ export function resultsServiceProvider(mlApiServices) {
earliestMs,
latestMs,
interval,
- maxResults,
+ maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT,
+ perPage = SWIM_LANE_DEFAULT_PAGE_SIZE,
+ fromPage = 1,
influencersFilterQuery
) {
return new Promise((resolve, reject) => {
@@ -565,10 +585,15 @@ export function resultsServiceProvider(mlApiServices) {
},
},
aggs: {
+ influencerValuesCardinality: {
+ cardinality: {
+ field: 'influencer_field_value',
+ },
+ },
influencerFieldValues: {
terms: {
field: 'influencer_field_value',
- size: maxResults !== undefined ? maxResults : 10,
+ size: !!maxResults ? maxResults : ANOMALY_SWIM_LANE_HARD_LIMIT,
order: {
maxAnomalyScore: 'desc',
},
@@ -579,6 +604,12 @@ export function resultsServiceProvider(mlApiServices) {
field: 'influencer_score',
},
},
+ bucketTruncate: {
+ bucket_sort: {
+ from: (fromPage - 1) * perPage,
+ size: perPage,
+ },
+ },
byTime: {
date_histogram: {
field: 'timestamp',
@@ -618,6 +649,8 @@ export function resultsServiceProvider(mlApiServices) {
obj.results[fieldValue] = fieldValues;
});
+ obj.cardinality = resp.aggregations?.influencerValuesCardinality?.value ?? 0;
+
resolve(obj);
})
.catch((resp) => {
diff --git a/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx b/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx
index 86c07a3577f7b..4f5d0723d65a4 100644
--- a/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx
+++ b/x-pack/plugins/ml/public/application/services/timefilter_refresh_service.tsx
@@ -9,4 +9,3 @@ import { Subject } from 'rxjs';
import { Refresh } from '../routing/use_refresh';
export const mlTimefilterRefresh$ = new Subject>();
-export const mlTimefilterTimeChange$ = new Subject>();
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
index 3b4562628051e..83070a5d94ba0 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable.tsx
@@ -16,10 +16,10 @@ import {
IContainer,
} from '../../../../../../src/plugins/embeddable/public';
import { MlStartDependencies } from '../../plugin';
-import { ExplorerSwimlaneContainer } from './explorer_swimlane_container';
+import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
import { JobId } from '../../../common/types/anomaly_detection_jobs';
-import { ExplorerService } from '../../application/services/explorer_service';
+import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service';
import {
Filter,
Query,
@@ -40,7 +40,7 @@ export interface AnomalySwimlaneEmbeddableCustomInput {
jobIds: JobId[];
swimlaneType: SwimlaneType;
viewBy?: string;
- limit?: number;
+ perPage?: number;
// Embeddable inputs which are not included in the default interface
filters: Filter[];
@@ -58,12 +58,12 @@ export interface AnomalySwimlaneEmbeddableCustomOutput {
jobIds: JobId[];
swimlaneType: SwimlaneType;
viewBy?: string;
- limit?: number;
+ perPage?: number;
}
export interface AnomalySwimlaneServices {
anomalyDetectorService: AnomalyDetectorService;
- explorerService: ExplorerService;
+ anomalyTimelineService: AnomalyTimelineService;
}
export type AnomalySwimlaneEmbeddableServices = [
@@ -101,14 +101,20 @@ export class AnomalySwimlaneEmbeddable extends Embeddable<
super.render(node);
this.node = node;
+ const I18nContext = this.services[0].i18n.Context;
+
ReactDOM.render(
- this.updateOutput(output)}
- />,
+
+ {
+ this.updateInput(input);
+ }}
+ />
+ ,
node
);
}
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx
index 6b2ab89de8a5d..243369982ac1f 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.test.tsx
@@ -46,6 +46,9 @@ describe('AnomalySwimlaneEmbeddableFactory', () => {
});
expect(Object.keys(createServices[0])).toEqual(Object.keys(coreStart));
expect(createServices[1]).toMatchObject(pluginsStart);
- expect(Object.keys(createServices[2])).toEqual(['anomalyDetectorService', 'explorerService']);
+ expect(Object.keys(createServices[2])).toEqual([
+ 'anomalyDetectorService',
+ 'anomalyTimelineService',
+ ]);
});
});
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
index 37c2cfb3e029b..0d587b428d89b 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_embeddable_factory.ts
@@ -22,7 +22,7 @@ import {
import { MlStartDependencies } from '../../plugin';
import { HttpService } from '../../application/services/http_service';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
-import { ExplorerService } from '../../application/services/explorer_service';
+import { AnomalyTimelineService } from '../../application/services/anomaly_timeline_service';
import { mlResultsServiceProvider } from '../../application/services/results_service';
import { resolveAnomalySwimlaneUserInput } from './anomaly_swimlane_setup_flyout';
import { mlApiServicesProvider } from '../../application/services/ml_api_service';
@@ -44,14 +44,10 @@ export class AnomalySwimlaneEmbeddableFactory
}
public async getExplicitInput(): Promise> {
- const [{ overlays, uiSettings }, , { anomalyDetectorService }] = await this.getServices();
+ const [coreStart] = await this.getServices();
try {
- return await resolveAnomalySwimlaneUserInput({
- anomalyDetectorService,
- overlays,
- uiSettings,
- });
+ return await resolveAnomalySwimlaneUserInput(coreStart);
} catch (e) {
return Promise.reject();
}
@@ -62,13 +58,13 @@ export class AnomalySwimlaneEmbeddableFactory
const httpService = new HttpService(coreStart.http);
const anomalyDetectorService = new AnomalyDetectorService(httpService);
- const explorerService = new ExplorerService(
+ const anomalyTimelineService = new AnomalyTimelineService(
pluginsStart.data.query.timefilter.timefilter,
coreStart.uiSettings,
mlResultsServiceProvider(mlApiServicesProvider(httpService))
);
- return [coreStart, pluginsStart, { anomalyDetectorService, explorerService }];
+ return [coreStart, pluginsStart, { anomalyDetectorService, anomalyTimelineService }];
}
public async create(
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
index 4977ece54bb57..be9a332e51dbc 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_initializer.tsx
@@ -27,7 +27,7 @@ export interface AnomalySwimlaneInitializerProps {
defaultTitle: string;
influencers: string[];
initialInput?: Partial<
- Pick
+ Pick
>;
onCreate: (swimlaneProps: {
panelTitle: string;
@@ -38,11 +38,6 @@ export interface AnomalySwimlaneInitializerProps {
onCancel: () => void;
}
-const limitOptions = [5, 10, 25, 50].map((limit) => ({
- value: limit,
- text: `${limit}`,
-}));
-
export const AnomalySwimlaneInitializer: FC = ({
defaultTitle,
influencers,
@@ -55,7 +50,6 @@ export const AnomalySwimlaneInitializer: FC = (
initialInput?.swimlaneType ?? SWIMLANE_TYPE.OVERALL
);
const [viewBySwimlaneFieldName, setViewBySwimlaneFieldName] = useState(initialInput?.viewBy);
- const [limit, setLimit] = useState(initialInput?.limit ?? 5);
const swimlaneTypeOptions = [
{
@@ -154,19 +148,6 @@ export const AnomalySwimlaneInitializer: FC = (
onChange={(e) => setViewBySwimlaneFieldName(e.target.value)}
/>
-
- }
- >
- setLimit(Number(e.target.value))}
- />
-
>
)}
@@ -186,7 +167,6 @@ export const AnomalySwimlaneInitializer: FC = (
panelTitle,
swimlaneType,
viewBy: viewBySwimlaneFieldName,
- limit,
})}
fill
>
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
index 54f50d2d3da32..1ffdadb60aaa3 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx
@@ -5,10 +5,13 @@
*/
import React from 'react';
-import { IUiSettingsClient, OverlayStart } from 'kibana/public';
+import { CoreStart } from 'kibana/public';
import moment from 'moment';
import { VIEW_BY_JOB_LABEL } from '../../application/explorer/explorer_constants';
-import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
+import {
+ KibanaContextProvider,
+ toMountPoint,
+} from '../../../../../../src/plugins/kibana_react/public';
import { AnomalySwimlaneInitializer } from './anomaly_swimlane_initializer';
import { JobSelectorFlyout } from '../../application/components/job_selector/job_selector_flyout';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
@@ -17,19 +20,17 @@ import {
AnomalySwimlaneEmbeddableInput,
getDefaultPanelTitle,
} from './anomaly_swimlane_embeddable';
+import { getMlGlobalServices } from '../../application/app';
+import { HttpService } from '../../application/services/http_service';
export async function resolveAnomalySwimlaneUserInput(
- {
- overlays,
- anomalyDetectorService,
- uiSettings,
- }: {
- anomalyDetectorService: AnomalyDetectorService;
- overlays: OverlayStart;
- uiSettings: IUiSettingsClient;
- },
+ coreStart: CoreStart,
input?: AnomalySwimlaneEmbeddableInput
): Promise> {
+ const { http, uiSettings, overlays } = coreStart;
+
+ const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
+
return new Promise(async (resolve, reject) => {
const maps = {
groupsMap: getInitialGroupsMap([]),
@@ -41,48 +42,50 @@ export async function resolveAnomalySwimlaneUserInput(
const selectedIds = input?.jobIds;
- const flyoutSession = overlays.openFlyout(
+ const flyoutSession = coreStart.overlays.openFlyout(
toMountPoint(
- {
- flyoutSession.close();
- reject();
- }}
- onSelectionConfirmed={async ({ jobIds, groups }) => {
- const title = input?.title ?? getDefaultPanelTitle(jobIds);
+
+ {
+ flyoutSession.close();
+ reject();
+ }}
+ onSelectionConfirmed={async ({ jobIds, groups }) => {
+ const title = input?.title ?? getDefaultPanelTitle(jobIds);
- const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
+ const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise();
- const influencers = anomalyDetectorService.extractInfluencers(jobs);
- influencers.push(VIEW_BY_JOB_LABEL);
+ const influencers = anomalyDetectorService.extractInfluencers(jobs);
+ influencers.push(VIEW_BY_JOB_LABEL);
- await flyoutSession.close();
+ await flyoutSession.close();
- const modalSession = overlays.openModal(
- toMountPoint(
- {
- modalSession.close();
- resolve({ jobIds, title: panelTitle, swimlaneType, viewBy, limit });
- }}
- onCancel={() => {
- modalSession.close();
- reject();
- }}
- />
- )
- );
- }}
- maps={maps}
- />
+ const modalSession = overlays.openModal(
+ toMountPoint(
+ {
+ modalSession.close();
+ resolve({ jobIds, title: panelTitle, swimlaneType, viewBy });
+ }}
+ onCancel={() => {
+ modalSession.close();
+ reject();
+ }}
+ />
+ )
+ );
+ }}
+ maps={maps}
+ />
+
),
{
'data-test-subj': 'mlAnomalySwimlaneEmbeddable',
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx
similarity index 73%
rename from x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx
rename to x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx
index 63ae89b5acdd1..846a3f543c2d4 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.test.tsx
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.test.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { ExplorerSwimlaneContainer } from './explorer_swimlane_container';
+import { EmbeddableSwimLaneContainer } from './embeddable_swim_lane_container';
import { BehaviorSubject, Observable } from 'rxjs';
import { I18nProvider } from '@kbn/i18n/react';
import {
@@ -17,6 +17,7 @@ import { CoreStart } from 'kibana/public';
import { MlStartDependencies } from '../../plugin';
import { useSwimlaneInputResolver } from './swimlane_input_resolver';
import { SWIMLANE_TYPE } from '../../application/explorer/explorer_constants';
+import { SwimlaneContainer } from '../../application/explorer/swimlane_container';
jest.mock('./swimlane_input_resolver', () => ({
useSwimlaneInputResolver: jest.fn(() => {
@@ -24,12 +25,11 @@ jest.mock('./swimlane_input_resolver', () => ({
}),
}));
-jest.mock('../../application/explorer/explorer_swimlane', () => ({
- ExplorerSwimlane: jest.fn(),
-}));
-
-jest.mock('../../application/components/chart_tooltip', () => ({
- MlTooltipComponent: jest.fn(),
+jest.mock('../../application/explorer/swimlane_container', () => ({
+ SwimlaneContainer: jest.fn(() => {
+ return null;
+ }),
+ isViewBySwimLaneData: jest.fn(),
}));
const defaultOptions = { wrapper: I18nProvider };
@@ -38,6 +38,7 @@ describe('ExplorerSwimlaneContainer', () => {
let embeddableInput: BehaviorSubject>;
let refresh: BehaviorSubject;
let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
+ const onInputChange = jest.fn();
beforeEach(() => {
embeddableInput = new BehaviorSubject({
@@ -61,25 +62,39 @@ describe('ExplorerSwimlaneContainer', () => {
};
(useSwimlaneInputResolver as jest.Mock).mockReturnValueOnce([
- mockOverallData,
SWIMLANE_TYPE.OVERALL,
- undefined,
+ mockOverallData,
+ 10,
+ jest.fn(),
+ {},
+ false,
+ null,
]);
- const { findByTestId } = render(
-
}
services={services}
refresh={refresh}
+ onInputChange={onInputChange}
/>,
defaultOptions
);
- expect(
- await findByTestId('mlMaxAnomalyScoreEmbeddable_test-swimlane-embeddable')
- ).toBeDefined();
+
+ const calledWith = ((SwimlaneContainer as unknown) as jest.Mock).mock
+ .calls[0][0];
+
+ expect(calledWith).toMatchObject({
+ perPage: 10,
+ swimlaneType: SWIMLANE_TYPE.OVERALL,
+ swimlaneData: mockOverallData,
+ isLoading: false,
+ swimlaneLimit: undefined,
+ fromPage: 1,
+ });
});
test('should render an error in case it could not fetch the ML swimlane data', async () => {
@@ -87,38 +102,25 @@ describe('ExplorerSwimlaneContainer', () => {
undefined,
undefined,
undefined,
+ undefined,
+ undefined,
+ false,
{ message: 'Something went wrong' },
]);
const { findByText } = render(
-
}
services={services}
refresh={refresh}
+ onInputChange={onInputChange}
/>,
defaultOptions
);
const errorMessage = await findByText('Something went wrong');
expect(errorMessage).toBeDefined();
});
-
- test('should render a loading indicator during the data fetching', async () => {
- const { findByTestId } = render(
-
- }
- services={services}
- refresh={refresh}
- />,
- defaultOptions
- );
- expect(
- await findByTestId('loading_mlMaxAnomalyScoreEmbeddable_test-swimlane-embeddable')
- ).toBeDefined();
- });
});
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx
new file mode 100644
index 0000000000000..5d91bdb41df6a
--- /dev/null
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/embeddable_swim_lane_container.tsx
@@ -0,0 +1,113 @@
+/*
+ * 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, { FC, useState } from 'react';
+import { EuiCallOut } from '@elastic/eui';
+import { Observable } from 'rxjs';
+
+import { CoreStart } from 'kibana/public';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { MlStartDependencies } from '../../plugin';
+import {
+ AnomalySwimlaneEmbeddableInput,
+ AnomalySwimlaneEmbeddableOutput,
+ AnomalySwimlaneServices,
+} from './anomaly_swimlane_embeddable';
+import { useSwimlaneInputResolver } from './swimlane_input_resolver';
+import { SwimlaneType } from '../../application/explorer/explorer_constants';
+import {
+ isViewBySwimLaneData,
+ SwimlaneContainer,
+} from '../../application/explorer/swimlane_container';
+
+export interface ExplorerSwimlaneContainerProps {
+ id: string;
+ embeddableInput: Observable;
+ services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
+ refresh: Observable;
+ onInputChange: (output: Partial) => void;
+}
+
+export const EmbeddableSwimLaneContainer: FC = ({
+ id,
+ embeddableInput,
+ services,
+ refresh,
+ onInputChange,
+}) => {
+ const [chartWidth, setChartWidth] = useState(0);
+ const [fromPage, setFromPage] = useState(1);
+
+ const [
+ swimlaneType,
+ swimlaneData,
+ perPage,
+ setPerPage,
+ timeBuckets,
+ isLoading,
+ error,
+ ] = useSwimlaneInputResolver(
+ embeddableInput,
+ onInputChange,
+ refresh,
+ services,
+ chartWidth,
+ fromPage
+ );
+
+ if (error) {
+ return (
+
+ }
+ color="danger"
+ iconType="alert"
+ style={{ width: '100%' }}
+ >
+ {error.message}
+
+ );
+ }
+
+ return (
+
+ {
+ setChartWidth(width);
+ }}
+ onPaginationChange={(update) => {
+ if (update.fromPage) {
+ setFromPage(update.fromPage);
+ }
+ if (update.perPage) {
+ setFromPage(1);
+ setPerPage(update.perPage);
+ }
+ }}
+ isLoading={isLoading}
+ noDataWarning={
+
+ }
+ />
+
+ );
+};
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx
deleted file mode 100644
index db2b9d55cfabb..0000000000000
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/explorer_swimlane_container.tsx
+++ /dev/null
@@ -1,126 +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, { FC, useCallback, useState } from 'react';
-import {
- EuiCallOut,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingChart,
- EuiResizeObserver,
- EuiSpacer,
- EuiText,
-} from '@elastic/eui';
-import { Observable } from 'rxjs';
-
-import { throttle } from 'lodash';
-import { CoreStart } from 'kibana/public';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { ExplorerSwimlane } from '../../application/explorer/explorer_swimlane';
-import { MlStartDependencies } from '../../plugin';
-import {
- AnomalySwimlaneEmbeddableInput,
- AnomalySwimlaneEmbeddableOutput,
- AnomalySwimlaneServices,
-} from './anomaly_swimlane_embeddable';
-import { MlTooltipComponent } from '../../application/components/chart_tooltip';
-import { useSwimlaneInputResolver } from './swimlane_input_resolver';
-import { SwimlaneType } from '../../application/explorer/explorer_constants';
-
-const RESIZE_THROTTLE_TIME_MS = 500;
-
-export interface ExplorerSwimlaneContainerProps {
- id: string;
- embeddableInput: Observable;
- services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
- refresh: Observable;
- onOutputChange?: (output: Partial) => void;
-}
-
-export const ExplorerSwimlaneContainer: FC = ({
- id,
- embeddableInput,
- services,
- refresh,
-}) => {
- const [chartWidth, setChartWidth] = useState(0);
-
- const [swimlaneType, swimlaneData, timeBuckets, error] = useSwimlaneInputResolver(
- embeddableInput,
- refresh,
- services,
- chartWidth
- );
-
- const onResize = useCallback(
- throttle((e: { width: number; height: number }) => {
- const labelWidth = 200;
- setChartWidth(e.width - labelWidth);
- }, RESIZE_THROTTLE_TIME_MS),
- []
- );
-
- if (error) {
- return (
-
- }
- color="danger"
- iconType="alert"
- style={{ width: '100%' }}
- >
- {error.message}
-
- );
- }
-
- return (
-
- {(resizeRef) => (
- {
- resizeRef(el);
- }}
- >
-
-
-
- {chartWidth > 0 && swimlaneData && swimlaneType ? (
-
-
- {(tooltipService) => (
-
- )}
-
-
- ) : (
-
-
-
-
-
- )}
-
-
- )}
-
- );
-};
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts
index 890c2bde6305d..a34955adebf62 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.test.ts
@@ -19,6 +19,7 @@ describe('useSwimlaneInputResolver', () => {
let embeddableInput: BehaviorSubject>;
let refresh: Subject;
let services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices];
+ let onInputChange: jest.Mock;
beforeEach(() => {
jest.useFakeTimers();
@@ -41,7 +42,7 @@ describe('useSwimlaneInputResolver', () => {
} as CoreStart,
(null as unknown) as MlStartDependencies,
({
- explorerService: {
+ anomalyTimelineService: {
setTimeRange: jest.fn(),
loadOverallData: jest.fn(() =>
Promise.resolve({
@@ -69,6 +70,7 @@ describe('useSwimlaneInputResolver', () => {
},
} as unknown) as AnomalySwimlaneServices,
];
+ onInputChange = jest.fn();
});
afterEach(() => {
jest.useRealTimers();
@@ -79,9 +81,11 @@ describe('useSwimlaneInputResolver', () => {
const { result, waitForNextUpdate } = renderHook(() =>
useSwimlaneInputResolver(
embeddableInput as Observable,
+ onInputChange,
refresh,
services,
- 1000
+ 1000,
+ 1
)
);
@@ -94,7 +98,7 @@ describe('useSwimlaneInputResolver', () => {
});
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(1);
- expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(1);
+ expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(1);
await act(async () => {
embeddableInput.next({
@@ -109,7 +113,7 @@ describe('useSwimlaneInputResolver', () => {
});
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2);
- expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(2);
+ expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(2);
await act(async () => {
embeddableInput.next({
@@ -124,7 +128,7 @@ describe('useSwimlaneInputResolver', () => {
});
expect(services[2].anomalyDetectorService.getJobs$).toHaveBeenCalledTimes(2);
- expect(services[2].explorerService.loadOverallData).toHaveBeenCalledTimes(3);
+ expect(services[2].anomalyTimelineService.loadOverallData).toHaveBeenCalledTimes(3);
});
});
diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
index 3829bbce5e5c9..9ed6f88150f68 100644
--- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
+++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts
@@ -16,23 +16,31 @@ import {
skipWhile,
startWith,
switchMap,
+ tap,
} from 'rxjs/operators';
import { CoreStart } from 'kibana/public';
import { TimeBuckets } from '../../application/util/time_buckets';
import {
AnomalySwimlaneEmbeddableInput,
+ AnomalySwimlaneEmbeddableOutput,
AnomalySwimlaneServices,
} from './anomaly_swimlane_embeddable';
import { MlStartDependencies } from '../../plugin';
-import { SWIMLANE_TYPE, SwimlaneType } from '../../application/explorer/explorer_constants';
+import {
+ ANOMALY_SWIM_LANE_HARD_LIMIT,
+ SWIM_LANE_DEFAULT_PAGE_SIZE,
+ SWIMLANE_TYPE,
+ SwimlaneType,
+} from '../../application/explorer/explorer_constants';
import { Filter } from '../../../../../../src/plugins/data/common/es_query/filters';
import { Query } from '../../../../../../src/plugins/data/common/query';
import { esKuery, UI_SETTINGS } from '../../../../../../src/plugins/data/public';
import { ExplorerJob, OverallSwimlaneData } from '../../application/explorer/explorer_utils';
import { parseInterval } from '../../../common/util/parse_interval';
import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service';
+import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container';
+import { ViewMode } from '../../../../../../src/plugins/embeddable/public';
-const RESIZE_IGNORED_DIFF_PX = 20;
const FETCH_RESULTS_DEBOUNCE_MS = 500;
function getJobsObservable(
@@ -48,17 +56,31 @@ function getJobsObservable(
export function useSwimlaneInputResolver(
embeddableInput: Observable,
+ onInputChange: (output: Partial) => void,
refresh: Observable,
services: [CoreStart, MlStartDependencies, AnomalySwimlaneServices],
- chartWidth: number
-): [string | undefined, OverallSwimlaneData | undefined, TimeBuckets, Error | null | undefined] {
- const [{ uiSettings }, , { explorerService, anomalyDetectorService }] = services;
+ chartWidth: number,
+ fromPage: number
+): [
+ string | undefined,
+ OverallSwimlaneData | undefined,
+ number,
+ (perPage: number) => void,
+ TimeBuckets,
+ boolean,
+ Error | null | undefined
+] {
+ const [{ uiSettings }, , { anomalyTimelineService, anomalyDetectorService }] = services;
const [swimlaneData, setSwimlaneData] = useState();
const [swimlaneType, setSwimlaneType] = useState();
const [error, setError] = useState();
+ const [perPage, setPerPage] = useState();
+ const [isLoading, setIsLoading] = useState(false);
const chartWidth$ = useMemo(() => new Subject(), []);
+ const fromPage$ = useMemo(() => new Subject(), []);
+ const perPage$ = useMemo(() => new Subject(), []);
const timeBuckets = useMemo(() => {
return new TimeBuckets({
@@ -73,28 +95,32 @@ export function useSwimlaneInputResolver(
const subscription = combineLatest([
getJobsObservable(embeddableInput, anomalyDetectorService),
embeddableInput,
- chartWidth$.pipe(
- skipWhile((v) => !v),
- distinctUntilChanged((prev, curr) => {
- // emit only if the width has been changed significantly
- return Math.abs(curr - prev) < RESIZE_IGNORED_DIFF_PX;
- })
+ chartWidth$.pipe(skipWhile((v) => !v)),
+ fromPage$,
+ perPage$.pipe(
+ startWith(undefined),
+ // no need to emit when the initial value has been set
+ distinctUntilChanged(
+ (prev, curr) => prev === undefined && curr === SWIM_LANE_DEFAULT_PAGE_SIZE
+ )
),
refresh.pipe(startWith(null)),
])
.pipe(
+ tap(setIsLoading.bind(null, true)),
debounceTime(FETCH_RESULTS_DEBOUNCE_MS),
- switchMap(([jobs, input, swimlaneContainerWidth]) => {
+ switchMap(([jobs, input, swimlaneContainerWidth, fromPageInput, perPageFromState]) => {
const {
viewBy,
swimlaneType: swimlaneTypeInput,
- limit,
+ perPage: perPageInput,
timeRange,
filters,
query,
+ viewMode,
} = input;
- explorerService.setTimeRange(timeRange);
+ anomalyTimelineService.setTimeRange(timeRange);
if (!swimlaneType) {
setSwimlaneType(swimlaneTypeInput);
@@ -118,18 +144,34 @@ export function useSwimlaneInputResolver(
return of(undefined);
}
- return from(explorerService.loadOverallData(explorerJobs, swimlaneContainerWidth)).pipe(
+ return from(
+ anomalyTimelineService.loadOverallData(explorerJobs, swimlaneContainerWidth)
+ ).pipe(
switchMap((overallSwimlaneData) => {
const { earliest, latest } = overallSwimlaneData;
if (overallSwimlaneData && swimlaneTypeInput === SWIMLANE_TYPE.VIEW_BY) {
+ if (perPageFromState === undefined) {
+ // set initial pagination from the input or default one
+ setPerPage(perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE);
+ }
+
+ if (viewMode === ViewMode.EDIT && perPageFromState !== perPageInput) {
+ // store per page value when the dashboard is in the edit mode
+ onInputChange({ perPage: perPageFromState });
+ }
+
return from(
- explorerService.loadViewBySwimlane(
+ anomalyTimelineService.loadViewBySwimlane(
[],
{ earliest, latest },
explorerJobs,
viewBy!,
- limit!,
+ isViewBySwimLaneData(swimlaneData)
+ ? swimlaneData.cardinality
+ : ANOMALY_SWIM_LANE_HARD_LIMIT,
+ perPageFromState ?? perPageInput ?? SWIM_LANE_DEFAULT_PAGE_SIZE,
+ fromPageInput,
swimlaneContainerWidth,
appliedFilters
)
@@ -156,6 +198,7 @@ export function useSwimlaneInputResolver(
if (data !== undefined) {
setError(null);
setSwimlaneData(data);
+ setIsLoading(false);
}
});
@@ -164,11 +207,28 @@ export function useSwimlaneInputResolver(
};
}, []);
+ useEffect(() => {
+ fromPage$.next(fromPage);
+ }, [fromPage]);
+
+ useEffect(() => {
+ if (perPage === undefined) return;
+ perPage$.next(perPage);
+ }, [perPage]);
+
useEffect(() => {
chartWidth$.next(chartWidth);
}, [chartWidth]);
- return [swimlaneType, swimlaneData, timeBuckets, error];
+ return [
+ swimlaneType,
+ swimlaneData,
+ perPage ?? SWIM_LANE_DEFAULT_PAGE_SIZE,
+ setPerPage,
+ timeBuckets,
+ isLoading,
+ error,
+ ];
}
export function processFilters(filters: Filter[], query: Query) {
diff --git a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx
index 312b9f31124b1..0db41c1ed104e 100644
--- a/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx
+++ b/x-pack/plugins/ml/public/ui_actions/edit_swimlane_panel_action.tsx
@@ -14,8 +14,6 @@ import {
AnomalySwimlaneEmbeddableOutput,
} from '../embeddables/anomaly_swimlane/anomaly_swimlane_embeddable';
import { resolveAnomalySwimlaneUserInput } from '../embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout';
-import { HttpService } from '../application/services/http_service';
-import { AnomalyDetectorService } from '../application/services/anomaly_detector_service';
export const EDIT_SWIMLANE_PANEL_ACTION = 'editSwimlanePanelAction';
@@ -39,18 +37,10 @@ export function createEditSwimlanePanelAction(getStartServices: CoreSetup['getSt
throw new Error('Not possible to execute an action without the embeddable context');
}
- const [{ overlays, uiSettings, http }] = await getStartServices();
- const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http));
+ const [coreStart] = await getStartServices();
try {
- const result = await resolveAnomalySwimlaneUserInput(
- {
- anomalyDetectorService,
- overlays,
- uiSettings,
- },
- embeddable.getInput()
- );
+ const result = await resolveAnomalySwimlaneUserInput(coreStart, embeddable.getInput());
embeddable.updateInput(result);
} catch (e) {
return Promise.reject();
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 4c1572ddfcad1..5c5d270d324ff 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -9790,8 +9790,6 @@
"xpack.ml.explorer.jobIdLabel": "ジョブ ID",
"xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(すべての影響因子のジョブスコア)",
"xpack.ml.explorer.kueryBar.filterPlaceholder": "影響因子フィールドでフィルタリング… ({queryExample})",
- "xpack.ml.explorer.limitLabel": "制限",
- "xpack.ml.explorer.loadingLabel": "読み込み中",
"xpack.ml.explorer.noConfiguredInfluencersTooltip": "選択されたジョブに影響因子が構成されていないため、トップインフルエンスリストは非表示になっています。",
"xpack.ml.explorer.noInfluencersFoundTitle": "{viewBySwimlaneFieldName}影響因子が見つかりません",
"xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "指定されたフィルターの{viewBySwimlaneFieldName} 影響因子が見つかりません",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 97f10e77dc717..c71215d2bfb74 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -9794,8 +9794,6 @@
"xpack.ml.explorer.jobIdLabel": "作业 ID",
"xpack.ml.explorer.jobScoreAcrossAllInfluencersLabel": "(所有影响因素的作业分数)",
"xpack.ml.explorer.kueryBar.filterPlaceholder": "按影响因素字段筛选……({queryExample})",
- "xpack.ml.explorer.limitLabel": "限制",
- "xpack.ml.explorer.loadingLabel": "正在加载",
"xpack.ml.explorer.noConfiguredInfluencersTooltip": "“顶级影响因素”列表被隐藏,因为没有为所选作业配置影响因素。",
"xpack.ml.explorer.noInfluencersFoundTitle": "未找到任何 {viewBySwimlaneFieldName} 影响因素",
"xpack.ml.explorer.noInfluencersFoundTitleFilterMessage": "对于指定筛选找不到任何 {viewBySwimlaneFieldName} 影响因素",
diff --git a/x-pack/test/functional/services/ml/anomaly_explorer.ts b/x-pack/test/functional/services/ml/anomaly_explorer.ts
index 7c479a4234673..80df235bf6ff8 100644
--- a/x-pack/test/functional/services/ml/anomaly_explorer.ts
+++ b/x-pack/test/functional/services/ml/anomaly_explorer.ts
@@ -76,7 +76,7 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid
async addAndEditSwimlaneInDashboard(dashboardTitle: string) {
await this.filterWithSearchString(dashboardTitle);
await testSubjects.isDisplayed('mlDashboardSelectionTable > checkboxSelectAll');
- await testSubjects.click('mlDashboardSelectionTable > checkboxSelectAll');
+ await testSubjects.clickWhenNotDisabled('mlDashboardSelectionTable > checkboxSelectAll');
expect(await testSubjects.isChecked('mlDashboardSelectionTable > checkboxSelectAll')).to.be(
true
);
From 3c56371153cad2c99d94511299b2aba7dd4225ea Mon Sep 17 00:00:00 2001
From: Andrea Villaverde
Date: Thu, 2 Jul 2020 17:01:31 +0200
Subject: [PATCH 13/18] Update known-plugins.asciidoc (#69370)
Co-authored-by: Elastic Machine
---
docs/plugins/known-plugins.asciidoc | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/plugins/known-plugins.asciidoc b/docs/plugins/known-plugins.asciidoc
index cd07596ad37ef..8fc2b7381de83 100644
--- a/docs/plugins/known-plugins.asciidoc
+++ b/docs/plugins/known-plugins.asciidoc
@@ -59,6 +59,7 @@ This list of plugins is not guaranteed to work on your version of Kibana. Instea
* https://github.com/sbeyn/kibana-plugin-traffic-sg[Traffic] (sbeyn)
* https://github.com/PhaedrusTheGreek/transform_vis[Transform Visualization] (PhaedrusTheGreek)
* https://github.com/nyurik/kibana-vega-vis[Vega-based visualizations] (nyurik) - Support for user-defined graphs, external data sources, maps, images, and user-defined interactivity.
+* https://github.com/Camichan/kbn_aframe[VR Graph Visualizations] (Camichan)
[float]
=== Other
From 59ece7992b29597b83f2acc78268c51fc1675de5 Mon Sep 17 00:00:00 2001
From: CJ Cenizal
Date: Thu, 2 Jul 2020 08:24:42 -0700
Subject: [PATCH 14/18] Make Index Management functional and API integration
tests robust against side effects introduced by Ingest Manager. (#70533)
---
.../apis/management/index_management/templates.js | 8 ++++----
.../test/functional/apps/index_management/home_page.ts | 10 +++++-----
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js
index 003fb21b09ccc..fcee8ed6a183f 100644
--- a/x-pack/test/api_integration/apis/management/index_management/templates.js
+++ b/x-pack/test/api_integration/apis/management/index_management/templates.js
@@ -24,8 +24,7 @@ export default function ({ getService }) {
updateTemplate,
} = registerHelpers({ supertest });
- // blocking es snapshot promotion: https://github.com/elastic/kibana/issues/70532
- describe.skip('index templates', () => {
+ describe('index templates', () => {
after(() => Promise.all([cleanUpEsResources()]));
describe('get all', () => {
@@ -41,8 +40,9 @@ export default function ({ getService }) {
it('should list all the index templates with the expected parameters', async () => {
const { body: allTemplates } = await getAllTemplates().expect(200);
- // Composable templates
- expect(allTemplates.templates).to.eql([]);
+ // Composable index templates may have been created by other apps, e.g. Ingest Manager,
+ // so we don't make any assertion about these contents.
+ expect(allTemplates.templates).to.be.an('array');
// Legacy templates
const legacyTemplate = allTemplates.legacyTemplates.find(
diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts
index b5b0197aad4b3..eab90e1fc19cf 100644
--- a/x-pack/test/functional/apps/index_management/home_page.ts
+++ b/x-pack/test/functional/apps/index_management/home_page.ts
@@ -13,8 +13,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const log = getService('log');
const browser = getService('browser');
- // blocking es snapshot promotion: https://github.com/elastic/kibana/issues/70532
- describe.skip('Home page', function () {
+ describe('Home page', function () {
before(async () => {
await pageObjects.common.navigateToApp('indexManagement');
});
@@ -82,9 +81,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const url = await browser.getCurrentUrl();
expect(url).to.contain(`/component_templates`);
- // There should be no component templates by default, so we verify the empty prompt displays
- const componentTemplateEmptyPrompt = await testSubjects.exists('emptyList');
- expect(componentTemplateEmptyPrompt).to.be(true);
+ // Verify content. Component templates may have been created by other apps, e.g. Ingest Manager,
+ // so we don't make any assertion about the presence or absence of component templates.
+ const componentTemplateList = await testSubjects.exists('componentTemplateList');
+ expect(componentTemplateList).to.be(true);
});
});
});
From 8a09f247e30987eff89824fc007953b815670fa3 Mon Sep 17 00:00:00 2001
From: Michael Hirsch
Date: Thu, 2 Jul 2020 11:35:40 -0400
Subject: [PATCH 15/18] [ML] Updates APM Module to Work with Service Maps
(#70361)
* updates apm integration job to work with service maps
* rename apm job in setup_module test
* modifies detector description
Co-authored-by: Elastic Machine
---
.../modules/apm_transaction/manifest.json | 16 ++++-----
...afeed_high_mean_transaction_duration.json} | 2 +-
.../ml/high_mean_response_time.json | 30 ----------------
.../ml/high_mean_transaction_duration.json | 35 +++++++++++++++++++
.../apis/ml/modules/setup_module.ts | 2 +-
5 files changed, 45 insertions(+), 40 deletions(-)
rename x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/{datafeed_high_mean_response_time.json => datafeed_high_mean_transaction_duration.json} (75%)
delete mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json
create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json
index 5e185e80a6038..f8feaef3be5f8 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json
@@ -1,29 +1,29 @@
{
"id": "apm_transaction",
"title": "APM",
- "description": "Detect anomalies in high mean of transaction duration (ECS).",
+ "description": "Detect anomalies in transactions from your APM services.",
"type": "Transaction data",
"logoFile": "logo.json",
- "defaultIndexPattern": "apm-*",
+ "defaultIndexPattern": "apm-*-transaction",
"query": {
"bool": {
"filter": [
{ "term": { "processor.event": "transaction" } },
- { "term": { "transaction.type": "request" } }
+ { "exists": { "field": "transaction.duration" } }
]
}
},
"jobs": [
{
- "id": "high_mean_response_time",
- "file": "high_mean_response_time.json"
+ "id": "high_mean_transaction_duration",
+ "file": "high_mean_transaction_duration.json"
}
],
"datafeeds": [
{
- "id": "datafeed-high_mean_response_time",
- "file": "datafeed_high_mean_response_time.json",
- "job_id": "high_mean_response_time"
+ "id": "datafeed-high_mean_transaction_duration",
+ "file": "datafeed_high_mean_transaction_duration.json",
+ "job_id": "high_mean_transaction_duration"
}
]
}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json
similarity index 75%
rename from x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json
rename to x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json
index dc37d05d18111..d312577902f51 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json
@@ -7,7 +7,7 @@
"bool": {
"filter": [
{ "term": { "processor.event": "transaction" } },
- { "term": { "transaction.type": "request" } }
+ { "exists": { "field": "transaction.duration.us" } }
]
}
}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json
deleted file mode 100644
index f6c230a6792fb..0000000000000
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_response_time.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "job_type": "anomaly_detector",
- "groups": [
- "apm"
- ],
- "description": "Detect anomalies in high mean of transaction duration",
- "analysis_config": {
- "bucket_span": "15m",
- "detectors": [
- {
- "detector_description": "high_mean(\"transaction.duration.us\")",
- "function": "high_mean",
- "field_name": "transaction.duration.us"
- }
- ],
- "influencers": []
- },
- "analysis_limits": {
- "model_memory_limit": "10mb"
- },
- "data_description": {
- "time_field": "@timestamp"
- },
- "model_plot_config": {
- "enabled": true
- },
- "custom_settings": {
- "created_by": "ml-module-apm-transaction"
- }
-}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json
new file mode 100644
index 0000000000000..77284cb275cd8
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json
@@ -0,0 +1,35 @@
+{
+ "job_type": "anomaly_detector",
+ "groups": [
+ "apm"
+ ],
+ "description": "Detect transaction duration anomalies across transaction types for your APM services.",
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "high duration by transaction type for an APM service",
+ "function": "high_mean",
+ "field_name": "transaction.duration.us",
+ "by_field_name": "transaction.type",
+ "partition_field_name": "service.name"
+ }
+ ],
+ "influencers": [
+ "transaction.type",
+ "service.name"
+ ]
+ },
+ "analysis_limits": {
+ "model_memory_limit": "32mb"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "model_plot_config": {
+ "enabled": true
+ },
+ "custom_settings": {
+ "created_by": "ml-module-apm-transaction"
+ }
+}
diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
index 1c98cd3a4e379..10c0f00234abc 100644
--- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
+++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts
@@ -218,7 +218,7 @@ export default ({ getService }: FtrProviderContext) => {
responseCode: 200,
jobs: [
{
- jobId: 'pf5_high_mean_response_time',
+ jobId: 'pf5_high_mean_transaction_duration',
jobState: JOB_STATE.CLOSED,
datafeedState: DATAFEED_STATE.STOPPED,
modelMemoryLimit: '11mb',
From 55922cb9a083007f7f14775d18873478ac09d1ae Mon Sep 17 00:00:00 2001
From: patrykkopycinski
Date: Thu, 2 Jul 2020 17:37:29 +0200
Subject: [PATCH 16/18] [Security Solution] Reposition EuiPopovers on scroll
(#69433)
* [Security Solution] Reposition EuiPopovers on scroll
* update snapshots
Co-authored-by: Elastic Machine
---
src/plugins/data/public/ui/filter_bar/filter_bar.tsx | 1 +
src/plugins/data/public/ui/filter_bar/filter_options.tsx | 1 +
.../data/public/ui/query_string_input/language_switcher.tsx | 1 +
.../saved_query_management/saved_query_management_component.tsx | 1 +
.../rule_actions_overflow/__snapshots__/index.test.tsx.snap | 1 +
.../alerts/components/rules/rule_actions_overflow/index.tsx | 1 +
.../rules/all/rules_table_filters/tags_filter_popover.tsx | 1 +
.../public/cases/components/filter_popover/index.tsx | 1 +
.../public/cases/components/property_actions/index.tsx | 1 +
.../components/exceptions/viewer/exceptions_pagination.tsx | 1 +
.../ml/score/__snapshots__/anomaly_score.test.tsx.snap | 1 +
.../public/common/components/ml/score/anomaly_score.tsx | 1 +
.../filters/__snapshots__/groups_filter_popover.test.tsx.snap | 1 +
.../ml_popover/jobs_table/filters/groups_filter_popover.tsx | 1 +
.../public/common/components/ml_popover/ml_popover.tsx | 2 ++
.../public/common/components/paginated_table/index.tsx | 1 +
.../public/common/components/tables/helpers.tsx | 1 +
.../public/common/components/utility_bar/utility_bar_action.tsx | 1 +
.../public/management/pages/policy/view/policy_list.tsx | 1 +
.../plugins/security_solution/public/resolver/view/submenu.tsx | 1 +
.../timelines/components/field_renderers/field_renderers.tsx | 1 +
.../components/timeline/body/events/event_column_view.tsx | 1 +
.../components/timeline/insert_timeline_popover/index.tsx | 1 +
.../components/timeline/properties/properties_right.tsx | 1 +
24 files changed, 25 insertions(+)
diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx
index 43dba150bf8d4..fdd952e2207d9 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx
@@ -109,6 +109,7 @@ function FilterBarUI(props: Props) {
panelPaddingSize="none"
ownFocus={true}
initialFocus=".filterEditor__hiddenItem"
+ repositionOnScroll
>
diff --git a/src/plugins/data/public/ui/filter_bar/filter_options.tsx b/src/plugins/data/public/ui/filter_bar/filter_options.tsx
index 3fb7f198d5466..b97e0e33f2400 100644
--- a/src/plugins/data/public/ui/filter_bar/filter_options.tsx
+++ b/src/plugins/data/public/ui/filter_bar/filter_options.tsx
@@ -167,6 +167,7 @@ class FilterOptionsUI extends Component
{
anchorPosition="rightUp"
panelPaddingSize="none"
withTitle
+ repositionOnScroll
>
setIsPopoverOpen(false)}
withTitle
+ repositionOnScroll
>
diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx
index b453125223c30..fd75c229d479d 100644
--- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx
+++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx
@@ -61,6 +61,7 @@ export const TagsFilterPopoverComponent = ({
isOpen={isTagPopoverOpen}
closePopover={() => setIsTagPopoverOpen(!isTagPopoverOpen)}
panelPaddingSize="none"
+ repositionOnScroll
>
{tags.map((tag, index) => (
diff --git a/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx b/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx
index 7b66bcffc89a1..4c16a8c0f3243 100644
--- a/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/filter_popover/index.tsx
@@ -84,6 +84,7 @@ export const FilterPopoverComponent = ({
isOpen={isPopoverOpen}
closePopover={setIsPopoverOpenCb}
panelPaddingSize="none"
+ repositionOnScroll
>
{options.map((option, index) => (
diff --git a/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx
index 6b8e00921abcb..29f1a2c5a1495 100644
--- a/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/property_actions/index.tsx
@@ -71,6 +71,7 @@ export const PropertyActions = React.memo(({ propertyActio
id="settingsPopover"
isOpen={showActions}
closePopover={onClosePopover}
+ repositionOnScroll
>
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap
index a9ec474a7b684..6694cec53987b 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap
@@ -95,6 +95,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = `
onClick={[Function]}
ownFocus={false}
panelPaddingSize="m"
+ repositionOnScroll={true}
>
setIsOpen(!isOpen)}
closePopover={() => setIsOpen(!isOpen)}
button={}
+ repositionOnScroll
>
setIsGroupPopoverOpen(!isGroupPopoverOpen)}
panelPaddingSize="none"
+ repositionOnScroll
>
{uniqueGroups.map((group, index) => (
{
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(!isPopoverOpen)}
+ repositionOnScroll
>
@@ -147,6 +148,7 @@ export const MlPopover = React.memo(() => {
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(!isPopoverOpen)}
+ repositionOnScroll
>
{i18n.ML_JOB_SETTINGS}
diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx
index 3b3130af77cfd..9f95284d989a9 100644
--- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx
@@ -273,6 +273,7 @@ const PaginatedTableComponent: FC = ({
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
+ repositionOnScroll
>
diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx
index b8ea32969c015..55e5758775504 100644
--- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.tsx
@@ -195,6 +195,7 @@ export const PopoverComponent = ({
closePopover={() => setIsOpen(!isOpen)}
id={`${idPrefix}-popover`}
isOpen={isOpen}
+ repositionOnScroll
>
{children}
diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx
index 250ed75f134c1..f072b27274ed7 100644
--- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_action.tsx
@@ -33,6 +33,7 @@ const Popover = React.memo(
}
closePopover={() => setPopoverState(false)}
isOpen={popoverState}
+ repositionOnScroll
>
{popoverContent?.(closePopover)}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
index 08c6ec89ff051..447a70ef998a9 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx
@@ -92,6 +92,7 @@ export const TableRowActions = React.memo<{ items: EuiContextMenuPanelProps['ite
}
isOpen={isOpen}
closePopover={handleCloseMenu}
+ repositionOnScroll
>
diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
index d3bb6123ce04d..ce126bf695559 100644
--- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
+++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx
@@ -215,6 +215,7 @@ const NodeSubMenuComponents = React.memo(
button={submenuPopoverButton}
isOpen={menuIsOpen}
closePopover={closePopover}
+ repositionOnScroll
>
{menuIsOpen && typeof optionsWithActions === 'object' && (
diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx
index 7296e0ee4b971..80fe7cb33779a 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx
@@ -274,6 +274,7 @@ export const DefaultFieldRendererOverflow = React.memo setIsOpen(!isOpen)}
+ repositionOnScroll
>
(
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
+ repositionOnScroll
>
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
index 83417cdb51b69..0adf767308269 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx
@@ -100,6 +100,7 @@ export const InsertTimelinePopoverComponent: React.FC = ({
button={insertTimelineButton}
isOpen={isPopoverOpen}
closePopover={handleClosePopover}
+ repositionOnScroll
>
= ({
id="timelineSettingsPopover"
isOpen={showActions}
closePopover={onClosePopover}
+ repositionOnScroll
>
{capabilitiesCanUserCRUD && (
From 0b6674edf5bf201064fdd9787e93ca479fbd0e8c Mon Sep 17 00:00:00 2001
From: Tyler Smalley
Date: Thu, 2 Jul 2020 08:37:37 -0700
Subject: [PATCH 17/18] Adds FOSSA CLI configuration file (#70137)
FOSSA analysis by default checks for dependencies in the following order:
1. Parse output from `npm ls --json --production` - Runs if npm exists on the system and provides an accurate list of all dependencies needed to build the production project.
2. Parse `package.json` - Runs if `package.json` can be successfully parsed into a dependency graph.
3. Run yarn list --json - This command verifies through yarn what the actual dependencies which are installed on the system are. This strategy runs with `NODE_ENV=production` by default to find production dependencies.
4. Parse `yarn.lock` - Detects dependencies based on the yarn lockfile.
5. Parse `npm-shrinkwrap.json` - Detects dependencies based on the lockfile.
6. Parse `package-lock.json` - Detects dependencies based on the lockfile.
Since our dependencies specified in `package.json` use compatible version matching (`^`), the reported version would often not be what the `yarn.lock` is currently specified to use. Because of this, we are defining a single module with a strategy on `yarn.lock`. Our `yarn.lock` file includes all dependencies.
Signed-off-by: Tyler Smalley
---
.fossa.yml | 15 +++++++++++++++
1 file changed, 15 insertions(+)
create mode 100755 .fossa.yml
diff --git a/.fossa.yml b/.fossa.yml
new file mode 100755
index 0000000000000..17d86d1f85521
--- /dev/null
+++ b/.fossa.yml
@@ -0,0 +1,15 @@
+# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
+# Visit https://fossa.com to learn more
+
+version: 2
+cli:
+ server: https://app.fossa.com
+ fetcher: custom
+ project: kibana
+analyze:
+ modules:
+ - name: kibana
+ type: nodejs
+ strategy: yarn.lock
+ target: .
+ path: .
From 12460b456cc9faa1c64e6a50eaaa873d08f8754d Mon Sep 17 00:00:00 2001
From: Tre
Date: Thu, 2 Jul 2020 10:01:54 -0600
Subject: [PATCH 18/18] [QA] [Code Coverage] Add Three Dot Compare Url (#70525)
---
.ci/Jenkinsfile_coverage | 9 +-
.../ingest_coverage/__tests__/either.test.js | 20 ++--
.../__tests__/transforms.test.js | 10 +-
.../integration_tests/ingest_coverage.test.js | 94 +++++++++++++------
.../code_coverage/ingest_coverage/maybe.js | 84 +++++++++++++++++
.../ingest_coverage/transforms.js | 50 +++++++---
.../shell_scripts/ingest_coverage.sh | 4 +
vars/kibanaCoverage.groovy | 56 +++++++++--
8 files changed, 268 insertions(+), 59 deletions(-)
create mode 100644 src/dev/code_coverage/ingest_coverage/maybe.js
diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage
index bd55bd73966ff..3986367d660a1 100644
--- a/.ci/Jenkinsfile_coverage
+++ b/.ci/Jenkinsfile_coverage
@@ -23,15 +23,22 @@ kibanaPipeline(timeoutMinutes: 240) {
}
def handleIngestion(timestamp) {
+ def previousSha = handlePreviousSha()
kibanaPipeline.downloadCoverageArtifacts()
kibanaCoverage.prokLinks("### Process HTML Links")
kibanaCoverage.collectVcsInfo("### Collect VCS Info")
kibanaCoverage.generateReports("### Merge coverage reports")
kibanaCoverage.uploadCombinedReports()
- kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, '### Ingest && Upload')
+ kibanaCoverage.ingest(env.JOB_NAME, BUILD_NUMBER, BUILD_URL, timestamp, previousSha, '### Ingest && Upload')
kibanaCoverage.uploadCoverageStaticSite(timestamp)
}
+def handlePreviousSha() {
+ def previous = kibanaCoverage.downloadPrevious('### Download OLD Previous')
+ kibanaCoverage.uploadPrevious('### Upload NEW Previous')
+ return previous
+}
+
def handleFail() {
def buildStatus = buildUtils.getBuildStatus()
if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED' && buildStatus != 'UNSTABLE') {
diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js
index cce8fd3c2e62d..3a493539f6743 100644
--- a/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js
+++ b/src/dev/code_coverage/ingest_coverage/__tests__/either.test.js
@@ -17,39 +17,39 @@
* under the License.
*/
-import { fromNullable, tryCatch, left, right } from '../either';
+import * as Either from '../either';
import { noop } from '../utils';
import expect from '@kbn/expect';
const pluck = (x) => (obj) => obj[x];
const expectNull = (x) => expect(x).to.equal(null);
-const attempt = (obj) => fromNullable(obj).map(pluck('detail'));
+const attempt = (obj) => Either.fromNullable(obj).map(pluck('detail'));
describe(`either datatype functions`, () => {
describe(`helpers`, () => {
it(`'fromNullable' should be a fn`, () => {
- expect(typeof fromNullable).to.be('function');
+ expect(typeof Either.fromNullable).to.be('function');
});
- it(`'tryCatch' should be a fn`, () => {
- expect(typeof tryCatch).to.be('function');
+ it(`' Either.tryCatch' should be a fn`, () => {
+ expect(typeof Either.tryCatch).to.be('function');
});
it(`'left' should be a fn`, () => {
- expect(typeof left).to.be('function');
+ expect(typeof Either.left).to.be('function');
});
it(`'right' should be a fn`, () => {
- expect(typeof right).to.be('function');
+ expect(typeof Either.right).to.be('function');
});
});
- describe('tryCatch', () => {
+ describe(' Either.tryCatch', () => {
let sut = undefined;
it(`should return a 'Left' on error`, () => {
- sut = tryCatch(() => {
+ sut = Either.tryCatch(() => {
throw new Error('blah');
});
expect(sut.inspect()).to.be('Left(Error: blah)');
});
it(`should return a 'Right' on successful execution`, () => {
- sut = tryCatch(noop);
+ sut = Either.tryCatch(noop);
expect(sut.inspect()).to.be('Right(undefined)');
});
});
diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js
index 2fd1d5cbe8d48..746bccc3d718a 100644
--- a/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js
+++ b/src/dev/code_coverage/ingest_coverage/__tests__/transforms.test.js
@@ -18,7 +18,7 @@
*/
import expect from '@kbn/expect';
-import { ciRunUrl, coveredFilePath, itemizeVcs } from '../transforms';
+import { ciRunUrl, coveredFilePath, itemizeVcs, prokPrevious } from '../transforms';
describe(`Transform fn`, () => {
describe(`ciRunUrl`, () => {
@@ -61,6 +61,14 @@ describe(`Transform fn`, () => {
});
});
});
+ describe(`prokPrevious`, () => {
+ const comparePrefixF = () => 'https://github.com/elastic/kibana/compare';
+ process.env.FETCHED_PREVIOUS = 'A';
+ it(`should return a previous compare url`, () => {
+ const actual = prokPrevious(comparePrefixF)('B');
+ expect(actual).to.be(`https://github.com/elastic/kibana/compare/A...B`);
+ });
+ });
describe(`itemizeVcs`, () => {
it(`should return a sha url`, () => {
const vcsInfo = [
diff --git a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js
index 2a65839f85ac3..95056d9f0d8d7 100644
--- a/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js
+++ b/src/dev/code_coverage/ingest_coverage/integration_tests/ingest_coverage.test.js
@@ -31,6 +31,7 @@ const env = {
ES_HOST: 'https://super:changeme@some.fake.host:9243',
NODE_ENV: 'integration_test',
COVERAGE_INGESTION_KIBANA_ROOT: '/var/lib/jenkins/workspace/elastic+kibana+code-coverage/kibana',
+ FETCHED_PREVIOUS: 'FAKE_PREVIOUS_SHA',
};
describe('Ingesting coverage', () => {
@@ -68,31 +69,64 @@ describe('Ingesting coverage', () => {
expect(folderStructure.test(actualUrl)).ok();
});
});
-
describe(`vcsInfo`, () => {
+ let stdOutWithVcsInfo = '';
describe(`without a commit msg in the vcs info file`, () => {
- let vcsInfo;
- const args = [
- 'scripts/ingest_coverage.js',
- '--verbose',
- '--vcsInfoPath',
- 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO_missing_commit_msg.txt',
- '--path',
- ];
-
beforeAll(async () => {
+ const args = [
+ 'scripts/ingest_coverage.js',
+ '--verbose',
+ '--vcsInfoPath',
+ 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO_missing_commit_msg.txt',
+ '--path',
+ ];
const opts = [...args, resolved];
const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env });
- vcsInfo = stdout;
+ stdOutWithVcsInfo = stdout;
});
it(`should be an obj w/o a commit msg`, () => {
const commitMsgRE = /"commitMsg"/;
- expect(commitMsgRE.test(vcsInfo)).to.not.be.ok();
+ expect(commitMsgRE.test(stdOutWithVcsInfo)).to.not.be.ok();
+ });
+ });
+ describe(`including previous sha`, () => {
+ let stdOutWithPrevious = '';
+ beforeAll(async () => {
+ const opts = [...verboseArgs, resolved];
+ const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env });
+ stdOutWithPrevious = stdout;
+ });
+
+ it(`should have a vcsCompareUrl`, () => {
+ const previousCompareUrlRe = /vcsCompareUrl.+\s*.*https.+compare\/FAKE_PREVIOUS_SHA\.\.\.f07b34f6206/;
+ expect(previousCompareUrlRe.test(stdOutWithPrevious)).to.be.ok();
+ });
+ });
+ describe(`with a commit msg in the vcs info file`, () => {
+ beforeAll(async () => {
+ const args = [
+ 'scripts/ingest_coverage.js',
+ '--verbose',
+ '--vcsInfoPath',
+ 'src/dev/code_coverage/ingest_coverage/integration_tests/mocks/VCS_INFO.txt',
+ '--path',
+ ];
+ const opts = [...args, resolved];
+ const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env });
+ stdOutWithVcsInfo = stdout;
+ });
+
+ it(`should be an obj w/ a commit msg`, () => {
+ const commitMsgRE = /commitMsg/;
+ expect(commitMsgRE.test(stdOutWithVcsInfo)).to.be.ok();
});
});
});
describe(`team assignment`, () => {
+ let shouldNotHavePipelineOut = '';
+ let shouldIndeedHavePipelineOut = '';
+
const args = [
'scripts/ingest_coverage.js',
'--verbose',
@@ -101,26 +135,30 @@ describe('Ingesting coverage', () => {
'--path',
];
- it(`should not occur when going to the totals index`, async () => {
- const teamAssignRE = /"pipeline":/;
- const shouldNotHavePipelineOut = await prokJustTotalOrNot(true, args);
+ const teamAssignRE = /pipeline:/;
+
+ beforeAll(async () => {
+ const summaryPath = 'jest-combined/coverage-summary-just-total.json';
+ const resolved = resolve(MOCKS_DIR, summaryPath);
+ const opts = [...args, resolved];
+ const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env });
+ shouldNotHavePipelineOut = stdout;
+ });
+ beforeAll(async () => {
+ const summaryPath = 'jest-combined/coverage-summary-manual-mix.json';
+ const resolved = resolve(MOCKS_DIR, summaryPath);
+ const opts = [...args, resolved];
+ const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env });
+ shouldIndeedHavePipelineOut = stdout;
+ });
+
+ it(`should not occur when going to the totals index`, () => {
const actual = teamAssignRE.test(shouldNotHavePipelineOut);
expect(actual).to.not.be.ok();
});
- it(`should indeed occur when going to the coverage index`, async () => {
- const shouldIndeedHavePipelineOut = await prokJustTotalOrNot(false, args);
- const onlyForTestingRe = /ingest-pipe=>team_assignment/;
- const actual = onlyForTestingRe.test(shouldIndeedHavePipelineOut);
+ it(`should indeed occur when going to the coverage index`, () => {
+ const actual = /ingest-pipe=>team_assignment/.test(shouldIndeedHavePipelineOut);
expect(actual).to.be.ok();
});
});
});
-async function prokJustTotalOrNot(isTotal, args) {
- const justTotalPath = 'jest-combined/coverage-summary-just-total.json';
- const notJustTotalPath = 'jest-combined/coverage-summary-manual-mix.json';
-
- const resolved = resolve(MOCKS_DIR, isTotal ? justTotalPath : notJustTotalPath);
- const opts = [...args, resolved];
- const { stdout } = await execa(process.execPath, opts, { cwd: ROOT_DIR, env });
- return stdout;
-}
diff --git a/src/dev/code_coverage/ingest_coverage/maybe.js b/src/dev/code_coverage/ingest_coverage/maybe.js
new file mode 100644
index 0000000000000..89936d6fc4b0e
--- /dev/null
+++ b/src/dev/code_coverage/ingest_coverage/maybe.js
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+/* eslint new-cap: 0 */
+/* eslint no-unused-vars: 0 */
+
+/**
+ * Just monad used for valid values
+ */
+export function Just(x) {
+ return {
+ value: () => x,
+ map: (f) => Maybe.of(f(x)),
+ isJust: () => true,
+ inspect: () => `Just(${x})`,
+ };
+}
+Just.of = function of(x) {
+ return Just(x);
+};
+export function just(x) {
+ return Just.of(x);
+}
+
+/**
+ * Maybe monad.
+ * Maybe.fromNullable` lifts an `x` into either a `Just`
+ * or a `Nothing` typeclass.
+ */
+export function Maybe(x) {
+ return {
+ chain: (f) => f(x),
+ map: (f) => Maybe(f(x)),
+ inspect: () => `Maybe(${x})`,
+ nothing: () => Nothing(),
+ isNothing: () => false,
+ isJust: () => false,
+ };
+}
+Maybe.of = function of(x) {
+ return just(x);
+};
+
+export function maybe(x) {
+ return Maybe.of(x);
+}
+export function fromNullable(x) {
+ return x !== null && x !== undefined && x !== false && x !== 'undefined' ? just(x) : nothing();
+}
+
+/**
+ * Nothing wraps undefined or null values and prevents errors
+ * that otherwise occur when mapping unexpected undefined or null
+ * values
+ */
+export function Nothing() {
+ return {
+ value: () => {
+ throw new TypeError(`Nothing algebraic data type returns...no value :)`);
+ },
+ map: (f) => {},
+ isNothing: () => true,
+ inspect: () => `[Nothing]`,
+ };
+}
+export function nothing() {
+ return Nothing();
+}
diff --git a/src/dev/code_coverage/ingest_coverage/transforms.js b/src/dev/code_coverage/ingest_coverage/transforms.js
index 4cb6c2892c4f2..b8c9acd6fc49d 100644
--- a/src/dev/code_coverage/ingest_coverage/transforms.js
+++ b/src/dev/code_coverage/ingest_coverage/transforms.js
@@ -17,10 +17,11 @@
* under the License.
*/
-import { left, right, fromNullable } from './either';
+import * as Either from './either';
+import { fromNullable } from './maybe';
import { always, id, noop } from './utils';
-const maybeTotal = (x) => (x === 'total' ? left(x) : right(x));
+const maybeTotal = (x) => (x === 'total' ? Either.left(x) : Either.right(x));
const trimLeftFrom = (text, x) => x.substr(x.indexOf(text));
@@ -54,13 +55,13 @@ const root = (urlBase) => (ts) => (testRunnerType) =>
`${urlBase}/${ts}/${testRunnerType.toLowerCase()}-combined`;
const prokForTotalsIndex = (mutateTrue) => (urlRoot) => (obj) =>
- right(obj)
+ Either.right(obj)
.map(mutateTrue)
.map(always(`${urlRoot}/index.html`))
.fold(noop, id);
const prokForCoverageIndex = (root) => (mutateFalse) => (urlRoot) => (obj) => (siteUrl) =>
- right(siteUrl)
+ Either.right(siteUrl)
.map((x) => {
mutateFalse(obj);
return x;
@@ -87,7 +88,7 @@ export const coveredFilePath = (obj) => {
const withoutCoveredFilePath = always(obj);
const leadingSlashRe = /^\//;
- const maybeDropLeadingSlash = (x) => (leadingSlashRe.test(x) ? right(x) : left(x));
+ const maybeDropLeadingSlash = (x) => (leadingSlashRe.test(x) ? Either.right(x) : Either.left(x));
const dropLeadingSlash = (x) => x.replace(leadingSlashRe, '');
const dropRoot = (root) => (x) =>
maybeDropLeadingSlash(x.replace(root, '')).fold(id, dropLeadingSlash);
@@ -97,11 +98,23 @@ export const coveredFilePath = (obj) => {
};
export const ciRunUrl = (obj) =>
- fromNullable(process.env.CI_RUN_URL).fold(always(obj), (ciRunUrl) => ({ ...obj, ciRunUrl }));
+ Either.fromNullable(process.env.CI_RUN_URL).fold(always(obj), (ciRunUrl) => ({
+ ...obj,
+ ciRunUrl,
+ }));
const size = 50;
-const truncateMsg = (msg) => (msg.length > size ? `${msg.slice(0, 50)}...` : msg);
-
+const truncateMsg = (msg) => {
+ const res = msg.length > size ? `${msg.slice(0, 50)}...` : msg;
+ return res;
+};
+const comparePrefix = () => 'https://github.com/elastic/kibana/compare';
+export const prokPrevious = (comparePrefixF) => (currentSha) => {
+ return Either.fromNullable(process.env.FETCHED_PREVIOUS).fold(
+ noop,
+ (previousSha) => `${comparePrefixF()}/${previousSha}...${currentSha}`
+ );
+};
export const itemizeVcs = (vcsInfo) => (obj) => {
const [branch, sha, author, commitMsg] = vcsInfo;
@@ -111,12 +124,23 @@ export const itemizeVcs = (vcsInfo) => (obj) => {
author,
vcsUrl: `https://github.com/elastic/kibana/commit/${sha}`,
};
- const res = fromNullable(commitMsg).fold(always({ ...obj, vcs }), (msg) => ({
- ...obj,
- vcs: { ...vcs, commitMsg: truncateMsg(msg) },
- }));
- return res;
+ const mutateVcs = (x) => (vcs.commitMsg = truncateMsg(x));
+ fromNullable(commitMsg).map(mutateVcs);
+
+ const vcsCompareUrl = process.env.FETCHED_PREVIOUS
+ ? `${comparePrefix()}/${process.env.FETCHED_PREVIOUS}...${sha}`
+ : 'PREVIOUS SHA NOT PROVIDED';
+
+ // const withoutPreviousL = always({ ...obj, vcs });
+ const withPreviousR = () => ({
+ ...obj,
+ vcs: {
+ ...vcs,
+ vcsCompareUrl,
+ },
+ });
+ return withPreviousR();
};
export const testRunner = (obj) => {
const { jsonSummaryPath } = obj;
diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh
index d3cf31fc0f427..0b67dac307473 100644
--- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh
+++ b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh
@@ -14,6 +14,10 @@ CI_RUN_URL=$3
export CI_RUN_URL
echo "### debug CI_RUN_URL: ${CI_RUN_URL}"
+FETCHED_PREVIOUS=$4
+export FETCHED_PREVIOUS
+echo "### debug FETCHED_PREVIOUS: ${FETCHED_PREVIOUS}"
+
ES_HOST="https://${USER_FROM_VAULT}:${PASS_FROM_VAULT}@${HOST_FROM_VAULT}"
export ES_HOST
diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy
index e511d7a8fc15e..66ebe3478fbec 100644
--- a/vars/kibanaCoverage.groovy
+++ b/vars/kibanaCoverage.groovy
@@ -1,3 +1,46 @@
+def downloadPrevious(title) {
+ def vaultSecret = 'secret/gce/elastic-bekitzur/service-account/kibana'
+
+ withGcpServiceAccount.fromVaultSecret(vaultSecret, 'value') {
+ kibanaPipeline.bash('''
+
+ gsutil -m cp -r gs://elastic-bekitzur-kibana-coverage-live/previous_pointer/previous.txt . || echo "### Previous Pointer NOT FOUND?"
+
+ if [ -e ./previous.txt ]; then
+ mv previous.txt downloaded_previous.txt
+ echo "### downloaded_previous.txt"
+ cat downloaded_previous.txt
+ fi
+
+ ''', title)
+
+ def previous = sh(script: 'cat downloaded_previous.txt', label: '### Capture Previous Sha', returnStdout: true).trim()
+
+ return previous
+ }
+}
+
+def uploadPrevious(title) {
+ def vaultSecret = 'secret/gce/elastic-bekitzur/service-account/kibana'
+
+ withGcpServiceAccount.fromVaultSecret(vaultSecret, 'value') {
+ kibanaPipeline.bash('''
+
+ collectPrevious() {
+ PREVIOUS=$(git log --pretty=format:%h -1)
+ echo "### PREVIOUS: ${PREVIOUS}"
+ echo $PREVIOUS > previous.txt
+ }
+ collectPrevious
+
+ gsutil cp previous.txt gs://elastic-bekitzur-kibana-coverage-live/previous_pointer/
+
+
+ ''', title)
+
+ }
+}
+
def uploadCoverageStaticSite(timestamp) {
def uploadPrefix = "gs://elastic-bekitzur-kibana-coverage-live/"
def uploadPrefixWithTimeStamp = "${uploadPrefix}${timestamp}/"
@@ -67,6 +110,7 @@ EOF
cat src/dev/code_coverage/www/index.html
''', "### Combine Index Partials")
}
+
def collectVcsInfo(title) {
kibanaPipeline.bash('''
predicate() {
@@ -125,31 +169,31 @@ def uploadCombinedReports() {
)
}
-def ingestData(jobName, buildNum, buildUrl, title) {
+def ingestData(jobName, buildNum, buildUrl, previousSha, title) {
kibanaPipeline.bash("""
source src/dev/ci_setup/setup_env.sh
yarn kbn bootstrap --prefer-offline
# Using existing target/kibana-coverage folder
- . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}'
+ . src/dev/code_coverage/shell_scripts/ingest_coverage.sh '${jobName}' ${buildNum} '${buildUrl}' ${previousSha}
""", title)
}
-def ingestWithVault(jobName, buildNum, buildUrl, title) {
+def ingestWithVault(jobName, buildNum, buildUrl, previousSha, title) {
def vaultSecret = 'secret/kibana-issues/prod/coverage/elasticsearch'
withVaultSecret(secret: vaultSecret, secret_field: 'host', variable_name: 'HOST_FROM_VAULT') {
withVaultSecret(secret: vaultSecret, secret_field: 'username', variable_name: 'USER_FROM_VAULT') {
withVaultSecret(secret: vaultSecret, secret_field: 'password', variable_name: 'PASS_FROM_VAULT') {
- ingestData(jobName, buildNum, buildUrl, title)
+ ingestData(jobName, buildNum, buildUrl, previousSha, title)
}
}
}
}
-def ingest(jobName, buildNumber, buildUrl, timestamp, title) {
+def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, title) {
withEnv([
"TIME_STAMP=${timestamp}",
]) {
- ingestWithVault(jobName, buildNumber, buildUrl, title)
+ ingestWithVault(jobName, buildNumber, buildUrl, previousSha, title)
}
}