From 45f0322fbcabf47883f7e571a41bd3846dfb82a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?=
Date: Thu, 2 Jul 2020 08:08:01 +0100
Subject: [PATCH 01/49] Reduce SavedObjects mappings for Application Usage
(#70475)
---
.../application_usage/saved_objects_types.ts | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts
index a0de79da565e6..8d6a2d110efe0 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts
@@ -35,10 +35,12 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe
hidden: false,
namespaceType: 'agnostic',
mappings: {
+ dynamic: false,
properties: {
- appId: { type: 'keyword' },
- numberOfClicks: { type: 'long' },
- minutesOnScreen: { type: 'float' },
+ // Disabled the mapping of these fields since they are not searched and we need to reduce the amount of indexed fields (#43673)
+ // appId: { type: 'keyword' },
+ // numberOfClicks: { type: 'long' },
+ // minutesOnScreen: { type: 'float' },
},
},
});
@@ -48,11 +50,13 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe
hidden: false,
namespaceType: 'agnostic',
mappings: {
+ dynamic: false,
properties: {
timestamp: { type: 'date' },
- appId: { type: 'keyword' },
- numberOfClicks: { type: 'long' },
- minutesOnScreen: { type: 'float' },
+ // Disabled the mapping of these fields since they are not searched and we need to reduce the amount of indexed fields (#43673)
+ // appId: { type: 'keyword' },
+ // numberOfClicks: { type: 'long' },
+ // minutesOnScreen: { type: 'float' },
},
},
});
From 6607bf7b49c710b0870367ceef1444c24b2c0d6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?=
Date: Thu, 2 Jul 2020 08:08:35 +0100
Subject: [PATCH 02/49] [Telemetry] Report data shippers (#64935)
Co-authored-by: Christiane (Tina) Heiligers
Co-authored-by: Elastic Machine
---
src/plugins/telemetry/server/index.ts | 4 +
.../__tests__/get_local_stats.js | 19 +-
.../get_data_telemetry/constants.ts | 136 ++++++++++
.../get_data_telemetry.test.ts | 251 +++++++++++++++++
.../get_data_telemetry/get_data_telemetry.ts | 253 +++++++++++++++++
.../get_data_telemetry/index.ts | 27 ++
.../telemetry_collection/get_local_stats.ts | 7 +-
.../server/telemetry_collection/index.ts | 6 +
.../apis/telemetry/telemetry_local.js | 20 ++
.../lib/elasticsearch/indices/get_indices.js | 40 ++-
.../server/lib/elasticsearch/indices/index.js | 2 +-
.../get_stats_with_xpack.test.ts.snap | 254 +++++++++---------
.../get_stats_with_xpack.test.ts | 12 +-
.../apis/telemetry/fixtures/basiccluster.json | 8 +-
.../apis/telemetry/fixtures/multicluster.json | 72 ++---
15 files changed, 903 insertions(+), 208 deletions(-)
create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts
create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts
create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts
create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts
index d048c8f5e9427..42259d2e5187c 100644
--- a/src/plugins/telemetry/server/index.ts
+++ b/src/plugins/telemetry/server/index.ts
@@ -47,4 +47,8 @@ export {
getLocalLicense,
getLocalStats,
TelemetryLocalStats,
+ DATA_TELEMETRY_ID,
+ DataTelemetryIndex,
+ DataTelemetryPayload,
+ buildDataTelemetryPayload,
} from './telemetry_collection';
diff --git a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js
index e78b92498e6e7..8541745faea3b 100644
--- a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js
+++ b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js
@@ -179,23 +179,36 @@ describe('get_local_stats', () => {
describe('handleLocalStats', () => {
it('returns expected object without xpack and kibana data', () => {
- const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context);
+ const result = handleLocalStats(
+ clusterInfo,
+ clusterStatsWithNodesUsage,
+ void 0,
+ void 0,
+ context
+ );
expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid);
expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name);
expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats);
expect(result.version).to.be('2.3.4');
expect(result.collection).to.be('local');
expect(result.license).to.be(undefined);
- expect(result.stack_stats).to.eql({ kibana: undefined });
+ expect(result.stack_stats).to.eql({ kibana: undefined, data: undefined });
});
it('returns expected object with xpack', () => {
- const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context);
+ const result = handleLocalStats(
+ clusterInfo,
+ clusterStatsWithNodesUsage,
+ void 0,
+ void 0,
+ context
+ );
const { stack_stats: stack, ...cluster } = result;
expect(cluster.collection).to.be(combinedStatsResult.collection);
expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid);
expect(cluster.cluster_name).to.be(combinedStatsResult.cluster_name);
expect(stack.kibana).to.be(undefined); // not mocked for this test
+ expect(stack.data).to.be(undefined); // not mocked for this test
expect(cluster.version).to.eql(combinedStatsResult.version);
expect(cluster.cluster_stats).to.eql(combinedStatsResult.cluster_stats);
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts
new file mode 100644
index 0000000000000..2d0864b1cb75f
--- /dev/null
+++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts
@@ -0,0 +1,136 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export const DATA_TELEMETRY_ID = 'data';
+
+export const DATA_KNOWN_TYPES = ['logs', 'traces', 'metrics'] as const;
+
+export type DataTelemetryType = typeof DATA_KNOWN_TYPES[number];
+
+export type DataPatternName = typeof DATA_DATASETS_INDEX_PATTERNS[number]['patternName'];
+
+// TODO: Ideally this list should be updated from an external public URL (similar to the newsfeed)
+// But it's good to have a minimum list shipped with the build.
+export const DATA_DATASETS_INDEX_PATTERNS = [
+ // Enterprise Search - Elastic
+ { pattern: '.ent-search-*', patternName: 'enterprise-search' },
+ { pattern: '.app-search-*', patternName: 'app-search' },
+ // Enterprise Search - 3rd party
+ { pattern: '*magento2*', patternName: 'magento2' },
+ { pattern: '*magento*', patternName: 'magento' },
+ { pattern: '*shopify*', patternName: 'shopify' },
+ { pattern: '*wordpress*', patternName: 'wordpress' },
+ // { pattern: '*wp*', patternName: 'wordpress' }, // TODO: Too vague?
+ { pattern: '*drupal*', patternName: 'drupal' },
+ { pattern: '*joomla*', patternName: 'joomla' },
+ { pattern: '*search*', patternName: 'search' }, // TODO: Too vague?
+ // { pattern: '*wix*', patternName: 'wix' }, // TODO: Too vague?
+ { pattern: '*sharepoint*', patternName: 'sharepoint' },
+ { pattern: '*squarespace*', patternName: 'squarespace' },
+ // { pattern: '*aem*', patternName: 'aem' }, // TODO: Too vague?
+ { pattern: '*sitecore*', patternName: 'sitecore' },
+ { pattern: '*weebly*', patternName: 'weebly' },
+ { pattern: '*acquia*', patternName: 'acquia' },
+
+ // Observability - Elastic
+ { pattern: 'filebeat-*', patternName: 'filebeat', shipper: 'filebeat' },
+ { pattern: 'metricbeat-*', patternName: 'metricbeat', shipper: 'metricbeat' },
+ { pattern: 'apm-*', patternName: 'apm', shipper: 'apm' },
+ { pattern: 'functionbeat-*', patternName: 'functionbeat', shipper: 'functionbeat' },
+ { pattern: 'heartbeat-*', patternName: 'heartbeat', shipper: 'heartbeat' },
+ { pattern: 'logstash-*', patternName: 'logstash', shipper: 'logstash' },
+ // Observability - 3rd party
+ { pattern: 'fluentd*', patternName: 'fluentd' },
+ { pattern: 'telegraf*', patternName: 'telegraf' },
+ { pattern: 'prometheusbeat*', patternName: 'prometheusbeat' },
+ { pattern: 'fluentbit*', patternName: 'fluentbit' },
+ { pattern: '*nginx*', patternName: 'nginx' },
+ { pattern: '*apache*', patternName: 'apache' }, // Already in Security (keeping it in here for documentation)
+ // { pattern: '*logs*', patternName: 'third-party-logs' }, Disabled for now
+
+ // Security - Elastic
+ { pattern: 'logstash-*', patternName: 'logstash', shipper: 'logstash' },
+ { pattern: 'endgame-*', patternName: 'endgame', shipper: 'endgame' },
+ { pattern: 'logs-endpoint.*', patternName: 'logs-endpoint', shipper: 'endpoint' }, // It should be caught by the `mappings` logic, but just in case
+ { pattern: 'metrics-endpoint.*', patternName: 'metrics-endpoint', shipper: 'endpoint' }, // It should be caught by the `mappings` logic, but just in case
+ { pattern: '.siem-signals-*', patternName: 'siem-signals' },
+ { pattern: 'auditbeat-*', patternName: 'auditbeat', shipper: 'auditbeat' },
+ { pattern: 'winlogbeat-*', patternName: 'winlogbeat', shipper: 'winlogbeat' },
+ { pattern: 'packetbeat-*', patternName: 'packetbeat', shipper: 'packetbeat' },
+ { pattern: 'filebeat-*', patternName: 'filebeat', shipper: 'filebeat' },
+ // Security - 3rd party
+ { pattern: '*apache*', patternName: 'apache' }, // Already in Observability (keeping it in here for documentation)
+ { pattern: '*tomcat*', patternName: 'tomcat' },
+ { pattern: '*artifactory*', patternName: 'artifactory' },
+ { pattern: '*aruba*', patternName: 'aruba' },
+ { pattern: '*barracuda*', patternName: 'barracuda' },
+ { pattern: '*bluecoat*', patternName: 'bluecoat' },
+ { pattern: 'arcsight-*', patternName: 'arcsight', shipper: 'arcsight' },
+ // { pattern: '*cef*', patternName: 'cef' }, // Disabled because it's too vague
+ { pattern: '*checkpoint*', patternName: 'checkpoint' },
+ { pattern: '*cisco*', patternName: 'cisco' },
+ { pattern: '*citrix*', patternName: 'citrix' },
+ { pattern: '*cyberark*', patternName: 'cyberark' },
+ { pattern: '*cylance*', patternName: 'cylance' },
+ { pattern: '*fireeye*', patternName: 'fireeye' },
+ { pattern: '*fortinet*', patternName: 'fortinet' },
+ { pattern: '*infoblox*', patternName: 'infoblox' },
+ { pattern: '*kaspersky*', patternName: 'kaspersky' },
+ { pattern: '*mcafee*', patternName: 'mcafee' },
+ // paloaltonetworks
+ { pattern: '*paloaltonetworks*', patternName: 'paloaltonetworks' },
+ { pattern: 'pan-*', patternName: 'paloaltonetworks' },
+ { pattern: 'pan_*', patternName: 'paloaltonetworks' },
+ { pattern: 'pan.*', patternName: 'paloaltonetworks' },
+
+ // rsa
+ { pattern: 'rsa.*', patternName: 'rsa' },
+ { pattern: 'rsa-*', patternName: 'rsa' },
+ { pattern: 'rsa_*', patternName: 'rsa' },
+
+ // snort
+ { pattern: 'snort-*', patternName: 'snort' },
+ { pattern: 'logstash-snort*', patternName: 'snort' },
+
+ { pattern: '*sonicwall*', patternName: 'sonicwall' },
+ { pattern: '*sophos*', patternName: 'sophos' },
+
+ // squid
+ { pattern: 'squid-*', patternName: 'squid' },
+ { pattern: 'squid_*', patternName: 'squid' },
+ { pattern: 'squid.*', patternName: 'squid' },
+
+ { pattern: '*symantec*', patternName: 'symantec' },
+ { pattern: '*tippingpoint*', patternName: 'tippingpoint' },
+ { pattern: '*trendmicro*', patternName: 'trendmicro' },
+ { pattern: '*tripwire*', patternName: 'tripwire' },
+ { pattern: '*zscaler*', patternName: 'zscaler' },
+ { pattern: '*zeek*', patternName: 'zeek' },
+ { pattern: '*sigma_doc*', patternName: 'sigma_doc' },
+ // { pattern: '*bro*', patternName: 'bro' }, // Disabled because it's too vague
+ { pattern: 'ecs-corelight*', patternName: 'ecs-corelight' },
+ { pattern: '*suricata*', patternName: 'suricata' },
+ // { pattern: '*fsf*', patternName: 'fsf' }, // Disabled because it's too vague
+ { pattern: '*wazuh*', patternName: 'wazuh' },
+] as const;
+
+// Get the unique list of index patterns (some are duplicated for documentation purposes)
+export const DATA_DATASETS_INDEX_PATTERNS_UNIQUE = DATA_DATASETS_INDEX_PATTERNS.filter(
+ (entry, index, array) => !array.slice(0, index).find(({ pattern }) => entry.pattern === pattern)
+);
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts
new file mode 100644
index 0000000000000..8bffc5d012a74
--- /dev/null
+++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts
@@ -0,0 +1,251 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { buildDataTelemetryPayload, getDataTelemetry } from './get_data_telemetry';
+import { DATA_DATASETS_INDEX_PATTERNS, DATA_DATASETS_INDEX_PATTERNS_UNIQUE } from './constants';
+
+describe('get_data_telemetry', () => {
+ describe('DATA_DATASETS_INDEX_PATTERNS', () => {
+ DATA_DATASETS_INDEX_PATTERNS.forEach((entry, index, array) => {
+ describe(`Pattern ${entry.pattern}`, () => {
+ test('there should only be one in DATA_DATASETS_INDEX_PATTERNS_UNIQUE', () => {
+ expect(
+ DATA_DATASETS_INDEX_PATTERNS_UNIQUE.filter(({ pattern }) => pattern === entry.pattern)
+ ).toHaveLength(1);
+ });
+
+ // This test is to make us sure that we don't update one of the duplicated entries and forget about any other repeated ones
+ test('when a document is duplicated, the duplicates should be identical', () => {
+ array.slice(0, index).forEach((previousEntry) => {
+ if (entry.pattern === previousEntry.pattern) {
+ expect(entry).toStrictEqual(previousEntry);
+ }
+ });
+ });
+ });
+ });
+ });
+
+ describe('buildDataTelemetryPayload', () => {
+ test('return the base object when no indices provided', () => {
+ expect(buildDataTelemetryPayload([])).toStrictEqual([]);
+ });
+
+ test('return the base object when no matching indices provided', () => {
+ expect(
+ buildDataTelemetryPayload([
+ { name: 'no__way__this__can_match_anything', sizeInBytes: 10 },
+ { name: '.kibana-event-log-8.0.0' },
+ ])
+ ).toStrictEqual([]);
+ });
+
+ test('matches some indices and puts them in their own category', () => {
+ expect(
+ buildDataTelemetryPayload([
+ // APM Indices have known shipper (so we can infer the datasetType from mapping constant)
+ { name: 'apm-7.7.0-error-000001', shipper: 'apm', isECS: true },
+ { name: 'apm-7.7.0-metric-000001', shipper: 'apm', isECS: true },
+ { name: 'apm-7.7.0-onboarding-2020.05.17', shipper: 'apm', isECS: true },
+ { name: 'apm-7.7.0-profile-000001', shipper: 'apm', isECS: true },
+ { name: 'apm-7.7.0-span-000001', shipper: 'apm', isECS: true },
+ { name: 'apm-7.7.0-transaction-000001', shipper: 'apm', isECS: true },
+ // Packetbeat indices with known shipper (we can infer datasetType from mapping constant)
+ { name: 'packetbeat-7.7.0-2020.06.11-000001', shipper: 'packetbeat', isECS: true },
+ // Matching patterns from the list => known datasetName but the rest is unknown
+ { name: 'filebeat-12314', docCount: 100, sizeInBytes: 10 },
+ { name: 'metricbeat-1234', docCount: 100, sizeInBytes: 10, isECS: false },
+ { name: '.app-search-1234', docCount: 0 },
+ { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name
+ // New Indexing strategy: everything can be inferred from the constant_keyword values
+ {
+ name: 'logs-nginx.access-default-000001',
+ datasetName: 'nginx.access',
+ datasetType: 'logs',
+ shipper: 'filebeat',
+ isECS: true,
+ docCount: 1000,
+ sizeInBytes: 1000,
+ },
+ {
+ name: 'logs-nginx.access-default-000002',
+ datasetName: 'nginx.access',
+ datasetType: 'logs',
+ shipper: 'filebeat',
+ isECS: true,
+ docCount: 1000,
+ sizeInBytes: 60,
+ },
+ ])
+ ).toStrictEqual([
+ {
+ shipper: 'apm',
+ index_count: 6,
+ ecs_index_count: 6,
+ },
+ {
+ shipper: 'packetbeat',
+ index_count: 1,
+ ecs_index_count: 1,
+ },
+ {
+ pattern_name: 'filebeat',
+ shipper: 'filebeat',
+ index_count: 1,
+ doc_count: 100,
+ size_in_bytes: 10,
+ },
+ {
+ pattern_name: 'metricbeat',
+ shipper: 'metricbeat',
+ index_count: 1,
+ ecs_index_count: 0,
+ doc_count: 100,
+ size_in_bytes: 10,
+ },
+ {
+ pattern_name: 'app-search',
+ index_count: 1,
+ doc_count: 0,
+ },
+ {
+ pattern_name: 'logs-endpoint',
+ shipper: 'endpoint',
+ index_count: 1,
+ doc_count: 0,
+ },
+ {
+ dataset: { name: 'nginx.access', type: 'logs' },
+ shipper: 'filebeat',
+ index_count: 2,
+ ecs_index_count: 2,
+ doc_count: 2000,
+ size_in_bytes: 1060,
+ },
+ ]);
+ });
+ });
+
+ describe('getDataTelemetry', () => {
+ test('it returns the base payload (all 0s) because no indices are found', async () => {
+ const callCluster = mockCallCluster();
+ await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([]);
+ });
+
+ test('can only see the index mappings, but not the stats', async () => {
+ const callCluster = mockCallCluster(['filebeat-12314']);
+ await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([
+ {
+ pattern_name: 'filebeat',
+ shipper: 'filebeat',
+ index_count: 1,
+ ecs_index_count: 0,
+ },
+ ]);
+ });
+
+ test('can see the mappings and the stats', async () => {
+ const callCluster = mockCallCluster(
+ ['filebeat-12314'],
+ { isECS: true },
+ {
+ indices: {
+ 'filebeat-12314': { total: { docs: { count: 100 }, store: { size_in_bytes: 10 } } },
+ },
+ }
+ );
+ await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([
+ {
+ pattern_name: 'filebeat',
+ shipper: 'filebeat',
+ index_count: 1,
+ ecs_index_count: 1,
+ doc_count: 100,
+ size_in_bytes: 10,
+ },
+ ]);
+ });
+
+ test('find an index that does not match any index pattern but has mappings metadata', async () => {
+ const callCluster = mockCallCluster(
+ ['cannot_match_anything'],
+ { isECS: true, datasetType: 'traces', shipper: 'my-beat' },
+ {
+ indices: {
+ cannot_match_anything: {
+ total: { docs: { count: 100 }, store: { size_in_bytes: 10 } },
+ },
+ },
+ }
+ );
+ await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([
+ {
+ dataset: { name: undefined, type: 'traces' },
+ shipper: 'my-beat',
+ index_count: 1,
+ ecs_index_count: 1,
+ doc_count: 100,
+ size_in_bytes: 10,
+ },
+ ]);
+ });
+
+ test('return empty array when there is an error', async () => {
+ const callCluster = jest.fn().mockRejectedValue(new Error('Something went terribly wrong'));
+ await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([]);
+ });
+ });
+});
+
+function mockCallCluster(
+ indicesMappings: string[] = [],
+ { isECS = false, datasetName = '', datasetType = '', shipper = '' } = {},
+ indexStats: any = {}
+) {
+ return jest.fn().mockImplementation(async (method: string, opts: any) => {
+ if (method === 'indices.getMapping') {
+ return Object.fromEntries(
+ indicesMappings.map((index) => [
+ index,
+ {
+ mappings: {
+ ...(shipper && { _meta: { beat: shipper } }),
+ properties: {
+ ...(isECS && { ecs: { properties: { version: { type: 'keyword' } } } }),
+ ...((datasetType || datasetName) && {
+ dataset: {
+ properties: {
+ ...(datasetName && {
+ name: { type: 'constant_keyword', value: datasetName },
+ }),
+ ...(datasetType && {
+ type: { type: 'constant_keyword', value: datasetType },
+ }),
+ },
+ },
+ }),
+ },
+ },
+ },
+ ])
+ );
+ }
+ return indexStats;
+ });
+}
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts
new file mode 100644
index 0000000000000..cf906bc5c86cf
--- /dev/null
+++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts
@@ -0,0 +1,253 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { LegacyAPICaller } from 'kibana/server';
+import {
+ DATA_DATASETS_INDEX_PATTERNS_UNIQUE,
+ DataPatternName,
+ DataTelemetryType,
+} from './constants';
+
+export interface DataTelemetryBasePayload {
+ index_count: number;
+ ecs_index_count?: number;
+ doc_count?: number;
+ size_in_bytes?: number;
+}
+
+export interface DataTelemetryDocument extends DataTelemetryBasePayload {
+ dataset?: {
+ name?: string;
+ type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s
+ };
+ shipper?: string;
+ pattern_name?: DataPatternName;
+}
+
+export type DataTelemetryPayload = DataTelemetryDocument[];
+
+export interface DataTelemetryIndex {
+ name: string;
+ datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword
+ datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword
+ shipper?: string; // To be obtained from `_meta.beat` if it's set
+ isECS?: boolean; // Optional because it can't be obtained via Monitoring.
+
+ // The fields below are optional because we might not be able to obtain them if the user does not
+ // have access to the index.
+ docCount?: number;
+ sizeInBytes?: number;
+}
+
+type AtLeastOne }> = Partial & U[keyof U];
+
+type DataDescriptor = AtLeastOne<{
+ datasetName: string;
+ datasetType: string;
+ shipper: string;
+ patternName: DataPatternName; // When found from the list of the index patterns
+}>;
+
+function findMatchingDescriptors({
+ name,
+ shipper,
+ datasetName,
+ datasetType,
+}: DataTelemetryIndex): DataDescriptor[] {
+ // If we already have the data from the indices' mappings...
+ if ([shipper, datasetName, datasetType].some(Boolean)) {
+ return [
+ {
+ ...(shipper && { shipper }),
+ ...(datasetName && { datasetName }),
+ ...(datasetType && { datasetType }),
+ } as AtLeastOne<{ datasetName: string; datasetType: string; shipper: string }>, // Using casting here because TS doesn't infer at least one exists from the if clause
+ ];
+ }
+
+ // Otherwise, try with the list of known index patterns
+ return DATA_DATASETS_INDEX_PATTERNS_UNIQUE.filter(({ pattern }) => {
+ if (!pattern.startsWith('.') && name.startsWith('.')) {
+ // avoid system indices caught by very fuzzy index patterns (i.e.: *log* would catch `.kibana-log-...`)
+ return false;
+ }
+ return new RegExp(`^${pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`).test(name);
+ });
+}
+
+function increaseCounters(
+ previousValue: DataTelemetryBasePayload = { index_count: 0 },
+ { isECS, docCount, sizeInBytes }: DataTelemetryIndex
+) {
+ return {
+ ...previousValue,
+ index_count: previousValue.index_count + 1,
+ ...(typeof isECS === 'boolean'
+ ? {
+ ecs_index_count: (previousValue.ecs_index_count || 0) + (isECS ? 1 : 0),
+ }
+ : {}),
+ ...(typeof docCount === 'number'
+ ? { doc_count: (previousValue.doc_count || 0) + docCount }
+ : {}),
+ ...(typeof sizeInBytes === 'number'
+ ? { size_in_bytes: (previousValue.size_in_bytes || 0) + sizeInBytes }
+ : {}),
+ };
+}
+
+export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTelemetryPayload {
+ const startingDotPatternsUntilTheFirstAsterisk = DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(
+ ({ pattern }) => pattern.replace(/^\.(.+)\*.*$/g, '.$1')
+ ).filter(Boolean);
+
+ // Filter out the system indices unless they are required by the patterns
+ const indexCandidates = indices.filter(
+ ({ name }) =>
+ !(
+ name.startsWith('.') &&
+ !startingDotPatternsUntilTheFirstAsterisk.find((pattern) => name.startsWith(pattern))
+ )
+ );
+
+ const acc = new Map();
+
+ for (const indexCandidate of indexCandidates) {
+ const matchingDescriptors = findMatchingDescriptors(indexCandidate);
+ for (const { datasetName, datasetType, shipper, patternName } of matchingDescriptors) {
+ const key = `${datasetName}-${datasetType}-${shipper}-${patternName}`;
+ acc.set(key, {
+ ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }),
+ ...(shipper && { shipper }),
+ ...(patternName && { pattern_name: patternName }),
+ ...increaseCounters(acc.get(key), indexCandidate),
+ });
+ }
+ }
+
+ return [...acc.values()];
+}
+
+interface IndexStats {
+ indices: {
+ [indexName: string]: {
+ total: {
+ docs: {
+ count: number;
+ deleted: number;
+ };
+ store: {
+ size_in_bytes: number;
+ };
+ };
+ };
+ };
+}
+
+interface IndexMappings {
+ [indexName: string]: {
+ mappings: {
+ _meta?: {
+ beat?: string;
+ };
+ properties: {
+ dataset?: {
+ properties: {
+ name?: {
+ type: string;
+ value?: string;
+ };
+ type?: {
+ type: string;
+ value?: string;
+ };
+ };
+ };
+ ecs?: {
+ properties: {
+ version?: {
+ type: string;
+ };
+ };
+ };
+ };
+ };
+ };
+}
+
+export async function getDataTelemetry(callCluster: LegacyAPICaller) {
+ try {
+ const index = [
+ ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern),
+ '*-*-*-*', // Include new indexing strategy indices {type}-{dataset}-{namespace}-{rollover_counter}
+ ];
+ const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([
+ // GET */_mapping?filter_path=*.mappings._meta.beat,*.mappings.properties.ecs.properties.version.type,*.mappings.properties.dataset.properties.type.value,*.mappings.properties.dataset.properties.name.value
+ callCluster('indices.getMapping', {
+ index: '*', // Request all indices because filter_path already filters out the indices without any of those fields
+ filterPath: [
+ // _meta.beat tells the shipper
+ '*.mappings._meta.beat',
+ // Does it have `ecs.version` in the mappings? => It follows the ECS conventions
+ '*.mappings.properties.ecs.properties.version.type',
+
+ // Disable the fields below because they are still pending to be confirmed:
+ // https://github.com/elastic/ecs/pull/845
+ // TODO: Re-enable when the final fields are confirmed
+ // // If `dataset.type` is a `constant_keyword`, it can be reported as a type
+ // '*.mappings.properties.dataset.properties.type.value',
+ // // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset
+ // '*.mappings.properties.dataset.properties.name.value',
+ ],
+ }),
+ // GET /_stats/docs,store?level=indices&filter_path=indices.*.total
+ callCluster('indices.stats', {
+ index,
+ level: 'indices',
+ metric: ['docs', 'store'],
+ filterPath: ['indices.*.total'],
+ }),
+ ]);
+
+ const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices });
+ const indices = indexNames.map((name) => {
+ const isECS = !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type;
+ const shipper = indexMappings[name]?.mappings?._meta?.beat;
+ const datasetName = indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value;
+ const datasetType = indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value;
+
+ const stats = (indexStats?.indices || {})[name];
+ if (stats) {
+ return {
+ name,
+ datasetName,
+ datasetType,
+ shipper,
+ isECS,
+ docCount: stats.total?.docs?.count,
+ sizeInBytes: stats.total?.store?.size_in_bytes,
+ };
+ }
+ return { name, datasetName, datasetType, shipper, isECS };
+ });
+ return buildDataTelemetryPayload(indices);
+ } catch (e) {
+ return [];
+ }
+}
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
new file mode 100644
index 0000000000000..d056d1c9f299f
--- /dev/null
+++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { DATA_TELEMETRY_ID } from './constants';
+
+export {
+ DataTelemetryIndex,
+ DataTelemetryPayload,
+ getDataTelemetry,
+ buildDataTelemetryPayload,
+} from './get_data_telemetry';
diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts
index b42edde2f55ca..4d4031bb428ba 100644
--- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts
@@ -25,6 +25,7 @@ import { getClusterInfo, ESClusterInfo } from './get_cluster_info';
import { getClusterStats } from './get_cluster_stats';
import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana';
import { getNodesUsage } from './get_nodes_usage';
+import { getDataTelemetry, DATA_TELEMETRY_ID, DataTelemetryPayload } from './get_data_telemetry';
/**
* Handle the separate local calls by combining them into a single object response that looks like the
@@ -39,6 +40,7 @@ export function handleLocalStats(
{ cluster_name, cluster_uuid, version }: ESClusterInfo,
{ _nodes, cluster_name: clusterName, ...clusterStats }: any,
kibana: KibanaUsageStats,
+ dataTelemetry: DataTelemetryPayload,
context: StatsCollectionContext
) {
return {
@@ -49,6 +51,7 @@ export function handleLocalStats(
cluster_stats: clusterStats,
collection: 'local',
stack_stats: {
+ [DATA_TELEMETRY_ID]: dataTelemetry,
kibana: handleKibanaStats(context, kibana),
},
};
@@ -68,11 +71,12 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async (
return await Promise.all(
clustersDetails.map(async (clustersDetail) => {
- const [clusterInfo, clusterStats, nodesUsage, kibana] = await Promise.all([
+ const [clusterInfo, clusterStats, nodesUsage, kibana, dataTelemetry] = await Promise.all([
getClusterInfo(callCluster), // cluster info
getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_)
getNodesUsage(callCluster), // nodes_usage info
getKibana(usageCollection, callCluster),
+ getDataTelemetry(callCluster),
]);
return handleLocalStats(
clusterInfo,
@@ -81,6 +85,7 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async (
nodes: { ...clusterStats.nodes, usage: nodesUsage },
},
kibana,
+ dataTelemetry,
context
);
})
diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts
index 377ddab7b877c..40cbf0e4caa1d 100644
--- a/src/plugins/telemetry/server/telemetry_collection/index.ts
+++ b/src/plugins/telemetry/server/telemetry_collection/index.ts
@@ -17,6 +17,12 @@
* under the License.
*/
+export {
+ DATA_TELEMETRY_ID,
+ DataTelemetryIndex,
+ DataTelemetryPayload,
+ buildDataTelemetryPayload,
+} from './get_data_telemetry';
export { getLocalStats, TelemetryLocalStats } from './get_local_stats';
export { getLocalLicense } from './get_local_license';
export { getClusterUuids } from './get_cluster_stats';
diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js
index e74cd180185ab..88e6b3a29052e 100644
--- a/test/api_integration/apis/telemetry/telemetry_local.js
+++ b/test/api_integration/apis/telemetry/telemetry_local.js
@@ -37,8 +37,17 @@ function flatKeys(source) {
export default function ({ getService }) {
const supertest = getService('supertest');
+ const es = getService('es');
describe('/api/telemetry/v2/clusters/_stats', () => {
+ before('create some telemetry-data tracked indices', async () => {
+ return es.indices.create({ index: 'filebeat-telemetry_tests_logs' });
+ });
+
+ after('cleanup telemetry-data tracked indices', () => {
+ return es.indices.delete({ index: 'filebeat-telemetry_tests_logs' });
+ });
+
it('should pull local stats and validate data types', async () => {
const timeRange = {
min: '2018-07-23T22:07:00Z',
@@ -71,6 +80,17 @@ export default function ({ getService }) {
expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true);
expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true);
expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false);
+
+ // Testing stack_stats.data
+ expect(stats.stack_stats.data).to.be.an('object');
+ expect(stats.stack_stats.data).to.be.an('array');
+ expect(stats.stack_stats.data[0]).to.be.an('object');
+ expect(stats.stack_stats.data[0].pattern_name).to.be('filebeat');
+ expect(stats.stack_stats.data[0].shipper).to.be('filebeat');
+ expect(stats.stack_stats.data[0].index_count).to.be(1);
+ expect(stats.stack_stats.data[0].doc_count).to.be(0);
+ expect(stats.stack_stats.data[0].ecs_index_count).to.be(0);
+ expect(stats.stack_stats.data[0].size_in_bytes).to.be.greaterThan(0);
});
it('should pull local stats and validate fields', async () => {
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js
index c087d20a97db1..ba6d0cb926f06 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js
@@ -77,11 +77,11 @@ export function handleResponse(resp, min, max, shardStats) {
});
}
-export function getIndices(req, esIndexPattern, showSystemIndices = false, shardStats) {
- checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices');
-
- const { min, max } = req.payload.timeRange;
-
+export function buildGetIndicesQuery(
+ esIndexPattern,
+ clusterUuid,
+ { start, end, size, showSystemIndices = false }
+) {
const filters = [];
if (!showSystemIndices) {
filters.push({
@@ -90,14 +90,11 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard
},
});
}
-
- const clusterUuid = req.params.clusterUuid;
const metricFields = ElasticsearchMetric.getMetricFields();
- const config = req.server.config();
- const params = {
+
+ return {
index: esIndexPattern,
- // TODO: composite aggregation
- size: config.get('monitoring.ui.max_bucket_size'),
+ size,
ignoreUnavailable: true,
filterPath: [
// only filter path can filter for inner_hits
@@ -118,8 +115,8 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard
body: {
query: createQuery({
type: 'index_stats',
- start: min,
- end: max,
+ start,
+ end,
clusterUuid,
metric: metricFields,
filters,
@@ -135,9 +132,24 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard
sort: [{ timestamp: { order: 'desc' } }],
},
};
+}
+
+export function getIndices(req, esIndexPattern, showSystemIndices = false, shardStats) {
+ checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices');
+
+ const { min: start, max: end } = req.payload.timeRange;
+
+ const clusterUuid = req.params.clusterUuid;
+ const config = req.server.config();
+ const params = buildGetIndicesQuery(esIndexPattern, clusterUuid, {
+ start,
+ end,
+ showSystemIndices,
+ size: config.get('monitoring.ui.max_bucket_size'),
+ });
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring');
return callWithRequest(req, 'search', params).then((resp) =>
- handleResponse(resp, min, max, shardStats)
+ handleResponse(resp, start, end, shardStats)
);
}
diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js
index 0ac2610bbba62..b07e3511d4804 100644
--- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js
+++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js
@@ -4,5 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { getIndices } from './get_indices';
+export { getIndices, buildGetIndicesQuery } from './get_indices';
export { getIndexSummary } from './get_index_summary';
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap
index ed82dc65eb410..b9bb206b8056f 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap
+++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap
@@ -1,158 +1,158 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Telemetry Collection: Get Aggregated Stats OSS-like telemetry (no license nor X-Pack telemetry) 1`] = `
-Array [
- Object {
- "cluster_name": "test",
- "cluster_stats": Object {
- "nodes": Object {
- "usage": Object {
- "nodes": Array [
- Object {
- "aggregations": Object {
- "terms": Object {
- "bytes": 2,
- },
- },
- "node_id": "some_node_id",
- "rest_actions": Object {
- "nodes_usage_action": 1,
+Object {
+ "cluster_name": "test",
+ "cluster_stats": Object {
+ "nodes": Object {
+ "usage": Object {
+ "nodes": Array [
+ Object {
+ "aggregations": Object {
+ "terms": Object {
+ "bytes": 2,
},
- "since": 1588616945163,
- "timestamp": 1588617023177,
},
- ],
- },
- },
- },
- "cluster_uuid": "test",
- "collection": "local",
- "stack_stats": Object {
- "kibana": Object {
- "count": 1,
- "great": "googlymoogly",
- "indices": 1,
- "os": Object {
- "platformReleases": Array [
- Object {
- "count": 1,
- "platformRelease": "iv",
- },
- ],
- "platforms": Array [
- Object {
- "count": 1,
- "platform": "rocky",
+ "node_id": "some_node_id",
+ "rest_actions": Object {
+ "nodes_usage_action": 1,
},
- ],
- },
- "plugins": Object {
- "clouds": Object {
- "chances": 95,
+ "since": 1588616945163,
+ "timestamp": 1588617023177,
},
- "localization": Object {
- "integrities": Object {},
- "labelsCount": 0,
- "locale": "en",
- },
- "rain": Object {
- "chances": 2,
- },
- "snow": Object {
- "chances": 0,
- },
- "sun": Object {
- "chances": 5,
+ ],
+ },
+ },
+ },
+ "cluster_uuid": "test",
+ "collection": "local",
+ "stack_stats": Object {
+ "data": Array [],
+ "kibana": Object {
+ "count": 1,
+ "great": "googlymoogly",
+ "indices": 1,
+ "os": Object {
+ "platformReleases": Array [
+ Object {
+ "count": 1,
+ "platformRelease": "iv",
},
- },
- "versions": Array [
+ ],
+ "platforms": Array [
Object {
"count": 1,
- "version": "8675309",
+ "platform": "rocky",
},
],
},
+ "plugins": Object {
+ "clouds": Object {
+ "chances": 95,
+ },
+ "localization": Object {
+ "integrities": Object {},
+ "labelsCount": 0,
+ "locale": "en",
+ },
+ "rain": Object {
+ "chances": 2,
+ },
+ "snow": Object {
+ "chances": 0,
+ },
+ "sun": Object {
+ "chances": 5,
+ },
+ },
+ "versions": Array [
+ Object {
+ "count": 1,
+ "version": "8675309",
+ },
+ ],
},
- "version": "8.0.0",
},
-]
+ "timestamp": Any,
+ "version": "8.0.0",
+}
`;
exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry (license + X-Pack) 1`] = `
-Array [
- Object {
- "cluster_name": "test",
- "cluster_stats": Object {
- "nodes": Object {
- "usage": Object {
- "nodes": Array [
- Object {
- "aggregations": Object {
- "terms": Object {
- "bytes": 2,
- },
- },
- "node_id": "some_node_id",
- "rest_actions": Object {
- "nodes_usage_action": 1,
+Object {
+ "cluster_name": "test",
+ "cluster_stats": Object {
+ "nodes": Object {
+ "usage": Object {
+ "nodes": Array [
+ Object {
+ "aggregations": Object {
+ "terms": Object {
+ "bytes": 2,
},
- "since": 1588616945163,
- "timestamp": 1588617023177,
},
- ],
- },
- },
- },
- "cluster_uuid": "test",
- "collection": "local",
- "stack_stats": Object {
- "kibana": Object {
- "count": 1,
- "great": "googlymoogly",
- "indices": 1,
- "os": Object {
- "platformReleases": Array [
- Object {
- "count": 1,
- "platformRelease": "iv",
- },
- ],
- "platforms": Array [
- Object {
- "count": 1,
- "platform": "rocky",
+ "node_id": "some_node_id",
+ "rest_actions": Object {
+ "nodes_usage_action": 1,
},
- ],
- },
- "plugins": Object {
- "clouds": Object {
- "chances": 95,
+ "since": 1588616945163,
+ "timestamp": 1588617023177,
},
- "localization": Object {
- "integrities": Object {},
- "labelsCount": 0,
- "locale": "en",
- },
- "rain": Object {
- "chances": 2,
- },
- "snow": Object {
- "chances": 0,
- },
- "sun": Object {
- "chances": 5,
+ ],
+ },
+ },
+ },
+ "cluster_uuid": "test",
+ "collection": "local",
+ "stack_stats": Object {
+ "data": Array [],
+ "kibana": Object {
+ "count": 1,
+ "great": "googlymoogly",
+ "indices": 1,
+ "os": Object {
+ "platformReleases": Array [
+ Object {
+ "count": 1,
+ "platformRelease": "iv",
},
- },
- "versions": Array [
+ ],
+ "platforms": Array [
Object {
"count": 1,
- "version": "8675309",
+ "platform": "rocky",
},
],
},
- "xpack": Object {},
+ "plugins": Object {
+ "clouds": Object {
+ "chances": 95,
+ },
+ "localization": Object {
+ "integrities": Object {},
+ "labelsCount": 0,
+ "locale": "en",
+ },
+ "rain": Object {
+ "chances": 2,
+ },
+ "snow": Object {
+ "chances": 0,
+ },
+ "sun": Object {
+ "chances": 5,
+ },
+ },
+ "versions": Array [
+ Object {
+ "count": 1,
+ "version": "8675309",
+ },
+ ],
},
- "version": "8.0.0",
+ "xpack": Object {},
},
-]
+ "timestamp": Any,
+ "version": "8.0.0",
+}
`;
diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts
index a8311933f0531..24382fb89d337 100644
--- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts
+++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts
@@ -85,7 +85,11 @@ describe('Telemetry Collection: Get Aggregated Stats', () => {
} as any,
context
);
- expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot();
+ stats.forEach((entry) => {
+ expect(entry).toMatchSnapshot({
+ timestamp: expect.any(String),
+ });
+ });
});
test('X-Pack telemetry (license + X-Pack)', async () => {
@@ -123,6 +127,10 @@ describe('Telemetry Collection: Get Aggregated Stats', () => {
} as any,
context
);
- expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot();
+ stats.forEach((entry) => {
+ expect(entry).toMatchSnapshot({
+ timestamp: expect.any(String),
+ });
+ });
});
});
diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json
index a0097f53ac93b..74d91a6215c79 100644
--- a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json
+++ b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json
@@ -153,9 +153,7 @@
"min": 223
}
},
- "versions": [
- "6.3.1"
- ]
+ "versions": ["6.3.1"]
},
"status": "yellow",
"timestamp": 1532386499084
@@ -297,9 +295,7 @@
},
"audit": {
"enabled": false,
- "outputs": [
- "logfile"
- ]
+ "outputs": ["logfile"]
},
"available": false,
"enabled": true,
diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json
index 6cc9c55157b28..7d408e39247ee 100644
--- a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json
+++ b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json
@@ -92,9 +92,7 @@
"master": 1,
"ingest": 1
},
- "versions": [
- "7.0.0-alpha1"
- ],
+ "versions": ["7.0.0-alpha1"],
"os": {
"available_processors": 4,
"allocated_processors": 1,
@@ -214,9 +212,7 @@
}
},
"audit": {
- "outputs": [
- "logfile"
- ],
+ "outputs": ["logfile"],
"enabled": false
},
"ipfilter": {
@@ -383,9 +379,7 @@
"master": 1,
"ingest": 1
},
- "versions": [
- "7.0.0-alpha1"
- ],
+ "versions": ["7.0.0-alpha1"],
"os": {
"available_processors": 4,
"allocated_processors": 1,
@@ -461,34 +455,22 @@
"enabled": true,
"realms": {
"file": {
- "name": [
- "default_file"
- ],
+ "name": ["default_file"],
"available": true,
- "size": [
- 0
- ],
+ "size": [0],
"enabled": true,
- "order": [
- 2147483647
- ]
+ "order": [2147483647]
},
"ldap": {
"available": true,
"enabled": false
},
"native": {
- "name": [
- "default_native"
- ],
+ "name": ["default_native"],
"available": true,
- "size": [
- 2
- ],
+ "size": [2],
"enabled": true,
- "order": [
- 2147483647
- ]
+ "order": [2147483647]
},
"active_directory": {
"available": true,
@@ -523,9 +505,7 @@
}
},
"audit": {
- "outputs": [
- "logfile"
- ],
+ "outputs": ["logfile"],
"enabled": false
},
"ipfilter": {
@@ -700,9 +680,7 @@
"master": 2,
"ingest": 2
},
- "versions": [
- "7.0.0-alpha1"
- ],
+ "versions": ["7.0.0-alpha1"],
"os": {
"available_processors": 8,
"allocated_processors": 2,
@@ -778,34 +756,22 @@
"enabled": true,
"realms": {
"file": {
- "name": [
- "default_file"
- ],
+ "name": ["default_file"],
"available": true,
- "size": [
- 0
- ],
+ "size": [0],
"enabled": true,
- "order": [
- 2147483647
- ]
+ "order": [2147483647]
},
"ldap": {
"available": true,
"enabled": false
},
"native": {
- "name": [
- "default_native"
- ],
+ "name": ["default_native"],
"available": true,
- "size": [
- 1
- ],
+ "size": [1],
"enabled": true,
- "order": [
- 2147483647
- ]
+ "order": [2147483647]
},
"active_directory": {
"available": true,
@@ -840,9 +806,7 @@
}
},
"audit": {
- "outputs": [
- "logfile"
- ],
+ "outputs": ["logfile"],
"enabled": false
},
"ipfilter": {
From f86afd4b070b219595dc444b43e86f24364ae30d Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 2 Jul 2020 11:02:35 +0300
Subject: [PATCH 03/49] [Visualizations] Each visType returns its supported
triggers (#70177)
* Refactor hardcoded supportedTriggers function with a callback function of visType
* use typescript to check if function exists
* Remove brush event from heatmap
Co-authored-by: Elastic Machine
---
.../vis_type_table/public/table_vis_type.ts | 4 +++
.../public/tag_cloud_type.ts | 4 +++
src/plugins/vis_type_vislib/public/area.ts | 4 +++
src/plugins/vis_type_vislib/public/heatmap.ts | 4 +++
.../vis_type_vislib/public/histogram.ts | 4 +++
.../vis_type_vislib/public/horizontal_bar.ts | 4 +++
src/plugins/vis_type_vislib/public/line.ts | 4 +++
src/plugins/vis_type_vislib/public/pie.ts | 4 +++
.../public/embeddable/visualize_embeddable.ts | 25 +------------------
.../public/vis_types/base_vis_type.ts | 4 +++
.../public/vis_types/types_service.ts | 2 ++
.../vis_types/vis_type_alias_registry.ts | 3 +++
12 files changed, 42 insertions(+), 24 deletions(-)
diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts
index c3bc72497007e..80d53021b7866 100644
--- a/src/plugins/vis_type_table/public/table_vis_type.ts
+++ b/src/plugins/vis_type_table/public/table_vis_type.ts
@@ -26,6 +26,7 @@ import { tableVisResponseHandler } from './table_vis_response_handler';
import tableVisTemplate from './table_vis.html';
import { TableOptions } from './components/table_vis_options_lazy';
import { getTableVisualizationControllerClass } from './vis_controller';
+import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) {
return {
@@ -39,6 +40,9 @@ export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitia
defaultMessage: 'Display values in a table',
}),
visualization: getTableVisualizationControllerClass(core, context),
+ getSupportedTriggers: () => {
+ return [VIS_EVENT_TO_TRIGGER.filter];
+ },
visConfig: {
defaults: {
perPage: 10,
diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
index 5a8cc3004a315..023489c6d2e87 100644
--- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
+++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts
@@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n';
import { Schemas } from '../../vis_default_editor/public';
import { TagCloudOptions } from './components/tag_cloud_options';
+import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
// @ts-ignore
import { createTagCloudVisualization } from './components/tag_cloud_visualization';
@@ -31,6 +32,9 @@ export const createTagCloudVisTypeDefinition = (deps: TagCloudVisDependencies) =
name: 'tagcloud',
title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }),
icon: 'visTagCloud',
+ getSupportedTriggers: () => {
+ return [VIS_EVENT_TO_TRIGGER.filter];
+ },
description: i18n.translate('visTypeTagCloud.vis.tagCloudDescription', {
defaultMessage: 'A group of words, sized according to their importance',
}),
diff --git a/src/plugins/vis_type_vislib/public/area.ts b/src/plugins/vis_type_vislib/public/area.ts
index c42962ad50a4b..ec90fbd1746a1 100644
--- a/src/plugins/vis_type_vislib/public/area.ts
+++ b/src/plugins/vis_type_vislib/public/area.ts
@@ -40,6 +40,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { createVislibVisController } from './vis_controller';
import { VisTypeVislibDependencies } from './plugin';
import { Rotates } from '../../charts/public';
+import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'area',
@@ -49,6 +50,9 @@ export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) =>
defaultMessage: 'Emphasize the quantity beneath a line chart',
}),
visualization: createVislibVisController(deps),
+ getSupportedTriggers: () => {
+ return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
+ },
visConfig: {
defaults: {
type: 'area',
diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts
index ced7a38568ffd..bd3d02029cb23 100644
--- a/src/plugins/vis_type_vislib/public/heatmap.ts
+++ b/src/plugins/vis_type_vislib/public/heatmap.ts
@@ -28,6 +28,7 @@ import { TimeMarker } from './vislib/visualizations/time_marker';
import { CommonVislibParams, ValueAxis } from './types';
import { VisTypeVislibDependencies } from './plugin';
import { ColorSchemas, ColorSchemaParams } from '../../charts/public';
+import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams {
type: 'heatmap';
@@ -48,6 +49,9 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies)
description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', {
defaultMessage: 'Shade cells within a matrix',
}),
+ getSupportedTriggers: () => {
+ return [VIS_EVENT_TO_TRIGGER.filter];
+ },
visualization: createVislibVisController(deps),
visConfig: {
defaults: {
diff --git a/src/plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts
index 52242ad11e8f5..8aeeb4ec533ab 100644
--- a/src/plugins/vis_type_vislib/public/histogram.ts
+++ b/src/plugins/vis_type_vislib/public/histogram.ts
@@ -39,6 +39,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { createVislibVisController } from './vis_controller';
import { VisTypeVislibDependencies } from './plugin';
import { Rotates } from '../../charts/public';
+import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'histogram',
@@ -50,6 +51,9 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies
defaultMessage: 'Assign a continuous variable to each axis',
}),
visualization: createVislibVisController(deps),
+ getSupportedTriggers: () => {
+ return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
+ },
visConfig: {
defaults: {
type: 'histogram',
diff --git a/src/plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts
index a58c15f136431..702581828e60d 100644
--- a/src/plugins/vis_type_vislib/public/horizontal_bar.ts
+++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts
@@ -37,6 +37,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { createVislibVisController } from './vis_controller';
import { VisTypeVislibDependencies } from './plugin';
import { Rotates } from '../../charts/public';
+import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'horizontal_bar',
@@ -48,6 +49,9 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen
defaultMessage: 'Assign a continuous variable to each axis',
}),
visualization: createVislibVisController(deps),
+ getSupportedTriggers: () => {
+ return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
+ },
visConfig: {
defaults: {
type: 'histogram',
diff --git a/src/plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts
index a94fd3f3945ab..6e9190229114b 100644
--- a/src/plugins/vis_type_vislib/public/line.ts
+++ b/src/plugins/vis_type_vislib/public/line.ts
@@ -38,6 +38,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config';
import { createVislibVisController } from './vis_controller';
import { VisTypeVislibDependencies } from './plugin';
import { Rotates } from '../../charts/public';
+import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({
name: 'line',
@@ -47,6 +48,9 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) =>
defaultMessage: 'Emphasize trends',
}),
visualization: createVislibVisController(deps),
+ getSupportedTriggers: () => {
+ return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
+ },
visConfig: {
defaults: {
type: 'line',
diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts
index a68bc5893406f..1e81dbdde3f68 100644
--- a/src/plugins/vis_type_vislib/public/pie.ts
+++ b/src/plugins/vis_type_vislib/public/pie.ts
@@ -26,6 +26,7 @@ import { getPositions, Positions } from './utils/collections';
import { createVislibVisController } from './vis_controller';
import { CommonVislibParams } from './types';
import { VisTypeVislibDependencies } from './plugin';
+import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public';
export interface PieVisParams extends CommonVislibParams {
type: 'pie';
@@ -47,6 +48,9 @@ export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => (
defaultMessage: 'Compare parts of a whole',
}),
visualization: createVislibVisController(deps),
+ getSupportedTriggers: () => {
+ return [VIS_EVENT_TO_TRIGGER.filter];
+ },
visConfig: {
defaults: {
type: 'pie',
diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
index 26fdd665192a6..2f9cda32fccdc 100644
--- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
+++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts
@@ -377,29 +377,6 @@ export class VisualizeEmbeddable extends Embeddable Array;
icon?: string;
image?: string;
stage?: 'experimental' | 'beta' | 'production';
@@ -44,6 +46,7 @@ export class BaseVisType {
name: string;
title: string;
description: string;
+ getSupportedTriggers?: () => Array;
icon?: string;
image?: string;
stage: 'experimental' | 'beta' | 'production';
@@ -77,6 +80,7 @@ export class BaseVisType {
this.name = opts.name;
this.description = opts.description || '';
+ this.getSupportedTriggers = opts.getSupportedTriggers;
this.title = opts.title;
this.icon = opts.icon;
this.image = opts.image;
diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts
index 321f96180fd68..14c2a9c50ab0e 100644
--- a/src/plugins/visualizations/public/vis_types/types_service.ts
+++ b/src/plugins/visualizations/public/vis_types/types_service.ts
@@ -23,11 +23,13 @@ import { visTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry';
import { BaseVisType } from './base_vis_type';
// @ts-ignore
import { ReactVisType } from './react_vis_type';
+import { TriggerContextMapping } from '../../../ui_actions/public';
export interface VisType {
name: string;
title: string;
description?: string;
+ getSupportedTriggers?: () => Array;
visualization: any;
isAccessible?: boolean;
requestHandler: string | unknown;
diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts
index bc80d549c81e6..f6d27b54c7c64 100644
--- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts
+++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { TriggerContextMapping } from '../../../ui_actions/public';
export interface VisualizationListItem {
editUrl: string;
@@ -26,6 +27,7 @@ export interface VisualizationListItem {
savedObjectType: string;
title: string;
description?: string;
+ getSupportedTriggers?: () => Array;
typeTitle: string;
image?: string;
}
@@ -53,6 +55,7 @@ export interface VisTypeAlias {
icon: string;
promotion?: VisTypeAliasPromotion;
description: string;
+ getSupportedTriggers?: () => Array;
stage: 'experimental' | 'beta' | 'production';
appExtensions?: {
From dc2737b8686f6f96b357fcea91d3e1060683fb9a Mon Sep 17 00:00:00 2001
From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com>
Date: Thu, 2 Jul 2020 11:38:46 +0300
Subject: [PATCH 04/49] Filter out error when calculating a label (#69934)
Co-authored-by: Elastic Machine
---
.../public/search/tabify/get_columns.test.ts | 16 ++++++++++++++++
.../data/public/search/tabify/get_columns.ts | 9 ++++++++-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/plugins/data/public/search/tabify/get_columns.test.ts b/src/plugins/data/public/search/tabify/get_columns.test.ts
index 0c5551d95690f..35f0181f63302 100644
--- a/src/plugins/data/public/search/tabify/get_columns.test.ts
+++ b/src/plugins/data/public/search/tabify/get_columns.test.ts
@@ -161,4 +161,20 @@ describe('get columns', () => {
'Sum of @timestamp',
]);
});
+
+ test('should not fail if there is no field for date histogram agg', () => {
+ const columns = tabifyGetColumns(
+ createAggConfigs([
+ {
+ type: 'date_histogram',
+ schema: 'segment',
+ params: {},
+ },
+ { type: 'sum', schema: 'metric', params: { field: '@timestamp' } },
+ ]).aggs,
+ false
+ );
+
+ expect(columns.map((c) => c.name)).toEqual(['', 'Sum of @timestamp']);
+ });
});
diff --git a/src/plugins/data/public/search/tabify/get_columns.ts b/src/plugins/data/public/search/tabify/get_columns.ts
index 8c538288d2fea..8e907d4b0cb88 100644
--- a/src/plugins/data/public/search/tabify/get_columns.ts
+++ b/src/plugins/data/public/search/tabify/get_columns.ts
@@ -22,10 +22,17 @@ import { IAggConfig } from '../aggs';
import { TabbedAggColumn } from './types';
const getColumn = (agg: IAggConfig, i: number): TabbedAggColumn => {
+ let name = '';
+ try {
+ name = agg.makeLabel();
+ } catch (e) {
+ // skip the case when makeLabel throws an error (e.x. no appropriate field for an aggregation)
+ }
+
return {
aggConfig: agg,
id: `col-${i}-${agg.id}`,
- name: agg.makeLabel(),
+ name,
};
};
From 6aeda644c8a31fdd58ae43f1f0ea3f1e569476b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Thu, 2 Jul 2020 10:01:10 +0100
Subject: [PATCH 05/49] [APM] Show transaction rate per minute on Observability
Overview page (#70336)
* changing transaction count to transaction rate per second
* sanity check coordinates before calculate the mean
* sanity check coordinates before calculate the mean
* removing extend_bounds to return empty when no data is available
---
.../rest/observability.dashboard.test.ts | 42 ++++++++++++++++++-
.../services/rest/observability_dashboard.ts | 9 +++-
.../get_transaction_coordinates.ts | 5 ++-
3 files changed, 51 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
index dbb5d6029d0f1..a14d827eeaec5 100644
--- a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
+++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
@@ -58,7 +58,7 @@ describe('Observability dashboard data', () => {
transactions: {
type: 'number',
label: 'Transactions',
- value: 6,
+ value: 2,
color: '#6092c0',
},
},
@@ -115,5 +115,45 @@ describe('Observability dashboard data', () => {
},
});
});
+ it('returns transaction stat as 0 when y is undefined', async () => {
+ callApmApiMock.mockImplementation(() =>
+ Promise.resolve({
+ serviceCount: 0,
+ transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }],
+ })
+ );
+ const response = await fetchLandingPageData(
+ {
+ startTime: '1',
+ endTime: '2',
+ bucketSize: '3',
+ },
+ { theme }
+ );
+ expect(response).toEqual({
+ title: 'APM',
+ appLink: '/app/apm',
+ stats: {
+ services: {
+ type: 'number',
+ label: 'Services',
+ value: 0,
+ },
+ transactions: {
+ type: 'number',
+ label: 'Transactions',
+ value: 0,
+ color: '#6092c0',
+ },
+ },
+ series: {
+ transactions: {
+ label: 'Transactions',
+ coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }],
+ color: '#6092c0',
+ },
+ },
+ });
+ });
});
});
diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
index 2107565c5facf..589199221d7a9 100644
--- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
+++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
@@ -5,7 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { sum } from 'lodash';
+import mean from 'lodash.mean';
import { Theme } from '@kbn/ui-shared-deps/theme';
import {
ApmFetchDataResponse,
@@ -48,7 +48,12 @@ export const fetchLandingPageData = async (
'xpack.apm.observabilityDashboard.stats.transactions',
{ defaultMessage: 'Transactions' }
),
- value: sum(transactionCoordinates.map((coordinates) => coordinates.y)),
+ value:
+ mean(
+ transactionCoordinates
+ .map(({ y }) => y)
+ .filter((y) => y && isFinite(y))
+ ) || 0,
color: theme.euiColorVis1,
},
},
diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts
index e78a3c1cec24a..0d1a4274c16dc 100644
--- a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts
+++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts
@@ -41,17 +41,18 @@ export async function getTransactionCoordinates({
field: '@timestamp',
fixed_interval: bucketSize,
min_doc_count: 0,
- extended_bounds: { min: start, max: end },
},
},
},
},
});
+ const deltaAsMinutes = (end - start) / 1000 / 60;
+
return (
aggregations?.distribution.buckets.map((bucket) => ({
x: bucket.key,
- y: bucket.doc_count,
+ y: bucket.doc_count / deltaAsMinutes,
})) || []
);
}
From 83beede50cb57f012411cc31952692d8cd888d52 Mon Sep 17 00:00:00 2001
From: Jean-Louis Leysens
Date: Thu, 2 Jul 2020 11:02:52 +0200
Subject: [PATCH 06/49] [Ingest Pipelines] Error messages (#70167)
* improved error messages
* traverse recursive error struct
* add check for object with keys
* update button position and copy
* size adjustments
* Refactor i18n texts and change wording
Also added missing translation and refactored maximum errors in
collapsed state to external constant
* use io-ts, add CIT and unit tests
* refactor error utilities to separate file
Co-authored-by: Elastic Machine
---
.../__jest__/client_integration/fixtures.ts | 117 ++++++++++++++++++
.../helpers/pipeline_form.helpers.ts | 2 +
.../ingest_pipelines_create.test.tsx | 21 ++++
.../pipeline_form/pipeline_form.tsx | 13 +-
.../pipeline_form/pipeline_form_error.tsx | 34 -----
.../pipeline_form_error/error_utils.test.ts | 67 ++++++++++
.../pipeline_form_error/error_utils.ts | 85 +++++++++++++
.../pipeline_form_error/i18n_texts.ts | 38 ++++++
.../pipeline_form_error/index.ts | 7 ++
.../pipeline_form_error.tsx | 99 +++++++++++++++
.../server/routes/api/create.ts | 8 +-
.../server/routes/api/shared/index.ts | 7 ++
.../routes/api/shared/is_object_with_keys.ts | 9 ++
.../server/routes/api/update.ts | 8 +-
14 files changed, 473 insertions(+), 42 deletions(-)
create mode 100644 x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts
delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx
create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts
create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts
create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts
create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts
create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx
create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts
create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts
new file mode 100644
index 0000000000000..8dddb2421f03d
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts
@@ -0,0 +1,117 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const nestedProcessorsErrorFixture = {
+ attributes: {
+ error: {
+ root_cause: [
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ suppressed: [
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ suppressed: [
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'csv',
+ },
+ ],
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ ],
+ },
+ ],
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ suppressed: [
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ suppressed: [
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'csv',
+ },
+ ],
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ {
+ type: 'parse_exception',
+ reason: '[field] required property is missing',
+ property_name: 'field',
+ processor_type: 'circle',
+ },
+ ],
+ },
+ status: 400,
+ },
+};
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts
index 8a14ed13f2022..85848b3d2f73c 100644
--- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts
@@ -42,6 +42,8 @@ export type PipelineFormTestSubjects =
| 'submitButton'
| 'pageTitle'
| 'savePipelineError'
+ | 'savePipelineError.showErrorsButton'
+ | 'savePipelineError.hideErrorsButton'
| 'pipelineForm'
| 'versionToggle'
| 'versionField'
diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx
index 2cfccbdc6d578..813057813f139 100644
--- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx
+++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx
@@ -9,6 +9,8 @@ import { act } from 'react-dom/test-utils';
import { setupEnvironment, pageHelpers, nextTick } from './helpers';
import { PipelinesCreateTestBed } from './helpers/pipelines_create.helpers';
+import { nestedProcessorsErrorFixture } from './fixtures';
+
const { setup } = pageHelpers.pipelinesCreate;
jest.mock('@elastic/eui', () => {
@@ -163,6 +165,25 @@ describe('', () => {
expect(exists('savePipelineError')).toBe(true);
expect(find('savePipelineError').text()).toContain(error.message);
});
+
+ test('displays nested pipeline errors as a flat list', async () => {
+ const { actions, find, exists, waitFor } = testBed;
+ httpRequestsMockHelpers.setCreatePipelineResponse(undefined, {
+ body: nestedProcessorsErrorFixture,
+ });
+
+ await act(async () => {
+ actions.clickSubmitButton();
+ await waitFor('savePipelineError');
+ });
+
+ expect(exists('savePipelineError')).toBe(true);
+ expect(exists('savePipelineError.showErrorsButton')).toBe(true);
+ find('savePipelineError.showErrorsButton').simulate('click');
+ expect(exists('savePipelineError.hideErrorsButton')).toBe(true);
+ expect(exists('savePipelineError.showErrorsButton')).toBe(false);
+ expect(find('savePipelineError').find('li').length).toBe(8);
+ });
});
describe('test pipeline', () => {
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx
index 05c9f0a08b0c7..a68e667f4ab43 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx
@@ -11,17 +11,18 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from
import { useForm, Form, FormConfig } from '../../../shared_imports';
import { Pipeline } from '../../../../common/types';
-import { PipelineRequestFlyout } from './pipeline_request_flyout';
-import { PipelineTestFlyout } from './pipeline_test_flyout';
-import { PipelineFormFields } from './pipeline_form_fields';
-import { PipelineFormError } from './pipeline_form_error';
-import { pipelineFormSchema } from './schema';
import {
OnUpdateHandlerArg,
OnUpdateHandler,
SerializeResult,
} from '../pipeline_processors_editor';
+import { PipelineRequestFlyout } from './pipeline_request_flyout';
+import { PipelineTestFlyout } from './pipeline_test_flyout';
+import { PipelineFormFields } from './pipeline_form_fields';
+import { PipelineFormError } from './pipeline_form_error';
+import { pipelineFormSchema } from './schema';
+
export interface PipelineFormProps {
onSave: (pipeline: Pipeline) => void;
onCancel: () => void;
@@ -116,7 +117,7 @@ export const PipelineForm: React.FunctionComponent = ({
error={form.getErrors()}
>
{/* Request error */}
- {saveError && }
+ {saveError && }
{/* All form fields */}
= ({ errorMessage }) => {
- return (
- <>
-
- }
- color="danger"
- iconType="alert"
- data-test-subj="savePipelineError"
- >
- {errorMessage}
-
-
- >
- );
-};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts
new file mode 100644
index 0000000000000..1739365eb197d
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts
@@ -0,0 +1,67 @@
+/*
+ * 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 { toKnownError } from './error_utils';
+import { nestedProcessorsErrorFixture } from '../../../../../__jest__/client_integration/fixtures';
+
+describe('toKnownError', () => {
+ test('undefined, null, numbers, arrays and bad objects', () => {
+ expect(toKnownError(undefined)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] });
+ expect(toKnownError(null)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] });
+ expect(toKnownError(123)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] });
+ expect(toKnownError([])).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] });
+ expect(toKnownError({})).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] });
+ expect(toKnownError({ attributes: {} })).toEqual({
+ errors: [{ reason: 'An unknown error occurred.' }],
+ });
+ });
+
+ test('non-processors errors', () => {
+ expect(toKnownError(new Error('my error'))).toEqual({ errors: [{ reason: 'my error' }] });
+ expect(toKnownError({ message: 'my error' })).toEqual({ errors: [{ reason: 'my error' }] });
+ });
+
+ test('processors errors', () => {
+ expect(toKnownError(nestedProcessorsErrorFixture)).toMatchInlineSnapshot(`
+ Object {
+ "errors": Array [
+ Object {
+ "processorType": "circle",
+ "reason": "[field] required property is missing",
+ },
+ Object {
+ "processorType": "circle",
+ "reason": "[field] required property is missing",
+ },
+ Object {
+ "processorType": "circle",
+ "reason": "[field] required property is missing",
+ },
+ Object {
+ "processorType": "csv",
+ "reason": "[field] required property is missing",
+ },
+ Object {
+ "processorType": "circle",
+ "reason": "[field] required property is missing",
+ },
+ Object {
+ "processorType": "circle",
+ "reason": "[field] required property is missing",
+ },
+ Object {
+ "processorType": "circle",
+ "reason": "[field] required property is missing",
+ },
+ Object {
+ "processorType": "circle",
+ "reason": "[field] required property is missing",
+ },
+ ],
+ }
+ `);
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts
new file mode 100644
index 0000000000000..7f32f962f657c
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as t from 'io-ts';
+import { flow } from 'fp-ts/lib/function';
+import { isRight } from 'fp-ts/lib/Either';
+
+import { i18nTexts } from './i18n_texts';
+
+export interface PipelineError {
+ reason: string;
+ processorType?: string;
+}
+interface PipelineErrors {
+ errors: PipelineError[];
+}
+
+interface ErrorNode {
+ reason: string;
+ processor_type?: string;
+ suppressed?: ErrorNode[];
+}
+
+// This is a runtime type (RT) for an error node which is a recursive type
+const errorNodeRT = t.recursion('ErrorNode', (ErrorNode) =>
+ t.intersection([
+ t.interface({
+ reason: t.string,
+ }),
+ t.partial({
+ processor_type: t.string,
+ suppressed: t.array(ErrorNode),
+ }),
+ ])
+);
+
+// This is a runtime type for the attributes object we expect to receive from the server
+// for processor errors
+const errorAttributesObjectRT = t.interface({
+ attributes: t.interface({
+ error: t.interface({
+ root_cause: t.array(errorNodeRT),
+ }),
+ }),
+});
+
+const isProcessorsError = flow(errorAttributesObjectRT.decode, isRight);
+
+type ErrorAttributesObject = t.TypeOf;
+
+const flattenErrorsTree = (node: ErrorNode): PipelineError[] => {
+ const result: PipelineError[] = [];
+ const recurse = (_node: ErrorNode) => {
+ result.push({ reason: _node.reason, processorType: _node.processor_type });
+ if (_node.suppressed && Array.isArray(_node.suppressed)) {
+ _node.suppressed.forEach(recurse);
+ }
+ };
+ recurse(node);
+ return result;
+};
+
+export const toKnownError = (error: unknown): PipelineErrors => {
+ if (typeof error === 'object' && error != null && isProcessorsError(error)) {
+ const errorAttributes = error as ErrorAttributesObject;
+ const rootCause = errorAttributes.attributes.error.root_cause[0];
+ return { errors: flattenErrorsTree(rootCause) };
+ }
+
+ if (typeof error === 'string') {
+ return { errors: [{ reason: error }] };
+ }
+
+ if (
+ error instanceof Error ||
+ (typeof error === 'object' && error != null && (error as any).message)
+ ) {
+ return { errors: [{ reason: (error as any).message }] };
+ }
+
+ return { errors: [{ reason: i18nTexts.errors.unknownError }] };
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts
new file mode 100644
index 0000000000000..e354541db8e7b
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const i18nTexts = {
+ title: i18n.translate('xpack.ingestPipelines.form.savePipelineError', {
+ defaultMessage: 'Unable to create pipeline',
+ }),
+ errors: {
+ processor: (processorType: string) =>
+ i18n.translate('xpack.ingestPipelines.form.savePipelineError.processorLabel', {
+ defaultMessage: '{type} processor',
+ values: { type: processorType },
+ }),
+ showErrors: (hiddenErrorsCount: number) =>
+ i18n.translate('xpack.ingestPipelines.form.savePipelineError.showAllButton', {
+ defaultMessage:
+ 'Show {hiddenErrorsCount, plural, one {# more error} other {# more errors}}',
+ values: {
+ hiddenErrorsCount,
+ },
+ }),
+ hideErrors: (hiddenErrorsCount: number) =>
+ i18n.translate('xpack.ingestPipelines.form.savePip10mbelineError.showFewerButton', {
+ defaultMessage: 'Hide {hiddenErrorsCount, plural, one {# error} other {# errors}}',
+ values: {
+ hiddenErrorsCount,
+ },
+ }),
+ unknownError: i18n.translate('xpack.ingestPipelines.form.unknownError', {
+ defaultMessage: 'An unknown error occurred.',
+ }),
+ },
+};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts
new file mode 100644
index 0000000000000..656691f639498
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/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 { PipelineFormError } from './pipeline_form_error';
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx
new file mode 100644
index 0000000000000..23fb9a1648434
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx
@@ -0,0 +1,99 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, useEffect } from 'react';
+
+import { EuiSpacer, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
+import { useKibana } from '../../../../shared_imports';
+
+import { i18nTexts } from './i18n_texts';
+import { toKnownError, PipelineError } from './error_utils';
+
+interface Props {
+ error: unknown;
+}
+
+const numberOfErrorsToDisplay = 5;
+
+export const PipelineFormError: React.FunctionComponent = ({ error }) => {
+ const { services } = useKibana();
+ const [isShowingAllErrors, setIsShowingAllErrors] = useState(false);
+ const safeErrorResult = toKnownError(error);
+ const hasMoreErrors = safeErrorResult.errors.length > numberOfErrorsToDisplay;
+ const hiddenErrorsCount = safeErrorResult.errors.length - numberOfErrorsToDisplay;
+ const results = isShowingAllErrors
+ ? safeErrorResult.errors
+ : safeErrorResult.errors.slice(0, numberOfErrorsToDisplay);
+
+ const renderErrorListItem = ({ processorType, reason }: PipelineError) => {
+ return (
+ <>
+ {processorType ? <>{i18nTexts.errors.processor(processorType) + ':'} > : undefined}
+ {reason}
+ >
+ );
+ };
+
+ useEffect(() => {
+ services.notifications.toasts.addDanger({ title: i18nTexts.title });
+ }, [services, error]);
+ return (
+ <>
+
+ {results.length > 1 ? (
+
+ {results.map((e, idx) => (
+ - {renderErrorListItem(e)}
+ ))}
+
+ ) : (
+ renderErrorListItem(results[0])
+ )}
+ {hasMoreErrors ? (
+
+
+ {isShowingAllErrors ? (
+ setIsShowingAllErrors(false)}
+ color="danger"
+ iconSide="right"
+ iconType="arrowUp"
+ data-test-subj="hideErrorsButton"
+ >
+ {i18nTexts.errors.hideErrors(hiddenErrorsCount)}
+
+ ) : (
+ setIsShowingAllErrors(true)}
+ color="danger"
+ iconSide="right"
+ iconType="arrowDown"
+ data-test-subj="showErrorsButton"
+ >
+ {i18nTexts.errors.showErrors(hiddenErrorsCount)}
+
+ )}
+
+
+ ) : undefined}
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts
index c1ab3852ee784..c2328bcc9d0ab 100644
--- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts
+++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts
@@ -10,6 +10,7 @@ import { Pipeline } from '../../../common/types';
import { API_BASE_PATH } from '../../../common/constants';
import { RouteDependencies } from '../../types';
import { pipelineSchema } from './pipeline_schema';
+import { isObjectWithKeys } from './shared';
const bodySchema = schema.object({
name: schema.string(),
@@ -70,7 +71,12 @@ export const registerCreateRoute = ({
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
- body: error,
+ body: isObjectWithKeys(error.body)
+ ? {
+ message: error.message,
+ attributes: error.body,
+ }
+ : error,
});
}
diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts
new file mode 100644
index 0000000000000..1fa794a4fb996
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/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 { isObjectWithKeys } from './is_object_with_keys';
diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts
new file mode 100644
index 0000000000000..0617bde26cfb6
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const isObjectWithKeys = (value: unknown) => {
+ return typeof value === 'object' && !!value && Object.keys(value).length > 0;
+};
diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts
index 214b293a43c6c..cd0e3568f0f60 100644
--- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts
+++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts
@@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema';
import { API_BASE_PATH } from '../../../common/constants';
import { RouteDependencies } from '../../types';
import { pipelineSchema } from './pipeline_schema';
+import { isObjectWithKeys } from './shared';
const bodySchema = schema.object(pipelineSchema);
@@ -52,7 +53,12 @@ export const registerUpdateRoute = ({
if (isEsError(error)) {
return res.customError({
statusCode: error.statusCode,
- body: error,
+ body: isObjectWithKeys(error.body)
+ ? {
+ message: error.message,
+ attributes: error.body,
+ }
+ : error,
});
}
From a0e263038c6c7cf077386a1a3da18d6fc6b92def Mon Sep 17 00:00:00 2001
From: Rudolf Meijering
Date: Thu, 2 Jul 2020 11:24:39 +0200
Subject: [PATCH 07/49] Use dynamic: false for config saved object mappings
(#70436)
---
src/core/server/ui_settings/saved_objects/ui_settings.ts | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts
index 26704f46a509c..452d1954b6e23 100644
--- a/src/core/server/ui_settings/saved_objects/ui_settings.ts
+++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts
@@ -25,10 +25,7 @@ export const uiSettingsType: SavedObjectsType = {
hidden: false,
namespaceType: 'single',
mappings: {
- // we don't want to allow `true` in the public `SavedObjectsTypeMappingDefinition` type, however
- // this is needed for the config that is kinda a special type. To avoid adding additional internal types
- // just for this, we hardcast to any here.
- dynamic: true as any,
+ dynamic: false,
properties: {
buildNum: {
type: 'keyword',
From a8347fad1c9c6ef47436cb947154f935615ea1d5 Mon Sep 17 00:00:00 2001
From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com>
Date: Thu, 2 Jul 2020 13:24:00 +0300
Subject: [PATCH 08/49] [Visualize] Add missing advanced settings and custom
label for pipeline aggs (#69688)
* Show advanced settings in pipeline aggs
* Add functional tests
* Make sub metric in sibling pipeline to have custom label
Co-authored-by: Elastic Machine
---
.../public/components/agg_group.tsx | 2 +-
.../public/components/agg_params_helper.ts | 6 +-
.../public/components/controls/sub_metric.tsx | 7 +-
test/functional/apps/visualize/_line_chart.js | 74 +++++++++++++++++++
4 files changed, 84 insertions(+), 5 deletions(-)
diff --git a/src/plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx
index 3030601236687..4cde33b8fbc31 100644
--- a/src/plugins/vis_default_editor/public/components/agg_group.tsx
+++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx
@@ -152,7 +152,7 @@ function DefaultEditorAggGroup({
{bucketsError && (
<>
- {bucketsError}
+ {bucketsError}
>
)}
diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts
index 45abbf8d2b2dd..39abddb3de853 100644
--- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts
+++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts
@@ -111,7 +111,11 @@ function getAggParamsToRender({
const aggType = agg.type.type;
const aggName = agg.type.name;
const aggParams = get(aggParamsMap, [aggType, aggName], {});
- paramEditor = get(aggParams, param.name) || get(aggParamsMap, ['common', param.type]);
+ paramEditor = get(aggParams, param.name);
+ }
+
+ if (!paramEditor) {
+ paramEditor = get(aggParamsMap, ['common', param.type]);
}
// show params with an editor component
diff --git a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx
index 361eeba9abdbf..fc79ba703c2b4 100644
--- a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx
+++ b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx
@@ -45,9 +45,10 @@ function SubMetricParamEditor({
defaultMessage: 'Bucket',
});
const type = aggParam.name;
+ const isCustomMetric = type === 'customMetric';
- const aggTitle = type === 'customMetric' ? metricTitle : bucketTitle;
- const aggGroup = type === 'customMetric' ? AggGroupNames.Metrics : AggGroupNames.Buckets;
+ const aggTitle = isCustomMetric ? metricTitle : bucketTitle;
+ const aggGroup = isCustomMetric ? AggGroupNames.Metrics : AggGroupNames.Buckets;
useMount(() => {
if (agg.params[type]) {
@@ -87,7 +88,7 @@ function SubMetricParamEditor({
setValidity={setValidity}
setTouched={setTouched}
schemas={schemas}
- hideCustomLabel={true}
+ hideCustomLabel={!isCustomMetric}
/>
>
);
diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js
index 5c510617fbb01..a492f3858b524 100644
--- a/test/functional/apps/visualize/_line_chart.js
+++ b/test/functional/apps/visualize/_line_chart.js
@@ -279,5 +279,79 @@ export default function ({ getService, getPageObjects }) {
expect(labels).to.eql(expectedLabels);
});
});
+
+ describe('pipeline aggregations', () => {
+ before(async () => {
+ log.debug('navigateToApp visualize');
+ await PageObjects.visualize.navigateToNewVisualization();
+ log.debug('clickLineChart');
+ await PageObjects.visualize.clickLineChart();
+ await PageObjects.visualize.clickNewSearch();
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ });
+
+ describe('parent pipeline', () => {
+ it('should have an error if bucket is not selected', async () => {
+ await PageObjects.visEditor.clickMetricEditor();
+ log.debug('Metrics agg = Serial diff');
+ await PageObjects.visEditor.selectAggregation('Serial diff', 'metrics');
+ await testSubjects.existOrFail('bucketsError');
+ });
+
+ it('should apply with selected bucket', async () => {
+ log.debug('Bucket = X-axis');
+ await PageObjects.visEditor.clickBucket('X-axis');
+ log.debug('Aggregation = Date Histogram');
+ await PageObjects.visEditor.selectAggregation('Date Histogram');
+ await PageObjects.visEditor.clickGo();
+ const title = await PageObjects.visChart.getYAxisTitle();
+ expect(title).to.be('Serial Diff of Count');
+ });
+
+ it('should change y-axis label to custom', async () => {
+ log.debug('set custom label of y-axis to "Custom"');
+ await PageObjects.visEditor.setCustomLabel('Custom', 1);
+ await PageObjects.visEditor.clickGo();
+ const title = await PageObjects.visChart.getYAxisTitle();
+ expect(title).to.be('Custom');
+ });
+
+ it('should have advanced accordion and json input', async () => {
+ await testSubjects.click('advancedParams-1');
+ await testSubjects.existOrFail('advancedParams-1 > codeEditorContainer');
+ });
+ });
+
+ describe('sibling pipeline', () => {
+ it('should apply with selected bucket', async () => {
+ log.debug('Metrics agg = Average Bucket');
+ await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics');
+ await PageObjects.visEditor.clickGo();
+ const title = await PageObjects.visChart.getYAxisTitle();
+ expect(title).to.be('Overall Average of Count');
+ });
+
+ it('should change sub metric custom label and calculate y-axis title', async () => {
+ log.debug('set custom label of sub metric to "Cats"');
+ await PageObjects.visEditor.setCustomLabel('Cats', '1-metric');
+ await PageObjects.visEditor.clickGo();
+ const title = await PageObjects.visChart.getYAxisTitle();
+ expect(title).to.be('Overall Average of Cats');
+ });
+
+ it('should outer custom label', async () => {
+ log.debug('set custom label to "Custom"');
+ await PageObjects.visEditor.setCustomLabel('Custom', 1);
+ await PageObjects.visEditor.clickGo();
+ const title = await PageObjects.visChart.getYAxisTitle();
+ expect(title).to.be('Custom');
+ });
+
+ it('should have advanced accordion and json input', async () => {
+ await testSubjects.click('advancedParams-1');
+ await testSubjects.existOrFail('advancedParams-1 > codeEditorContainer');
+ });
+ });
+ });
});
}
From 1cfc9356bd36c54b8005089decc36970c11c101d Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Thu, 2 Jul 2020 13:17:33 +0200
Subject: [PATCH 09/49] 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 10/49] 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 11/49] 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 12/49] [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 17/49] 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 18/49] 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 19/49] [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 20/49] [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 21/49] 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 22/49] 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 23/49] [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 24/49] [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 25/49] 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 26/49] [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)
}
}
From 67067ec9a9ace63632a36dbf91b7089b46852f5f Mon Sep 17 00:00:00 2001
From: Melissa Alvarez
Date: Thu, 2 Jul 2020 12:23:40 -0400
Subject: [PATCH 27/49] update reg evaluate property names (#70604)
---
.../application/data_frame_analytics/common/analytics.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index a423722d1447a..5715687402bcb 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -128,7 +128,7 @@ export interface Eval {
export interface RegressionEvaluateResponse {
regression: {
- mean_squared_error: {
+ mse: {
value: number;
};
r_squared: {
@@ -311,7 +311,7 @@ export const isRegressionEvaluateResponse = (arg: any): arg is RegressionEvaluat
return (
keys.length === 1 &&
keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION &&
- arg?.regression?.mean_squared_error !== undefined &&
+ arg?.regression?.mse !== undefined &&
arg?.regression?.r_squared !== undefined
);
};
@@ -410,7 +410,7 @@ export const useRefreshAnalyticsList = (
const DEFAULT_SIG_FIGS = 3;
export function getValuesFromResponse(response: RegressionEvaluateResponse) {
- let meanSquaredError = response?.regression?.mean_squared_error?.value;
+ let meanSquaredError = response?.regression?.mse?.value;
if (meanSquaredError) {
meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS));
From d3c35c751895e997e41ab02d455385e7df1fd391 Mon Sep 17 00:00:00 2001
From: Luca Belluccini
Date: Thu, 2 Jul 2020 19:07:48 +0200
Subject: [PATCH 28/49] [DOC] CSV Reporting settings (#67742)
* [DOC] CSV Reporting settings
We have some undocumented CSV reporting settings.
Those should be backported to 7.3+ probably.
* Update docs/settings/reporting-settings.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/settings/reporting-settings.asciidoc
Co-authored-by: Kaarina Tungseth
* Update docs/settings/reporting-settings.asciidoc
Co-authored-by: Kaarina Tungseth
* Apply suggestions from code review
Co-authored-by: Kaarina Tungseth
Co-authored-by: Kaarina Tungseth
Co-authored-by: Tim Sullivan
---
docs/settings/reporting-settings.asciidoc | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc
index 928878fdcdb03..c83cd068eff5b 100644
--- a/docs/settings/reporting-settings.asciidoc
+++ b/docs/settings/reporting-settings.asciidoc
@@ -210,6 +210,25 @@ When `xpack.reporting.capture.browser.type` is set to `chromium` (default) you c
large exports from causing performance and storage issues.
Defaults to `10485760` (10mB).
+| `xpack.reporting.csv.scroll.size`
+ | Number of documents retrieved from {es} for each scroll iteration during a CSV
+ export.
+ Defaults to `500`.
+
+| `xpack.reporting.csv.scroll.duration`
+ | Amount of time allowed before {kib} cleans the scroll context during a CSV export.
+ Defaults to `30s`.
+
+| `xpack.reporting.csv.checkForFormulas`
+ | Enables a check that warns you when there's a potential formula involved in the output (=, -, +, and @ chars).
+ See OWASP: https://www.owasp.org/index.php/CSV_Injection
+ Defaults to `true`.
+
+| `xpack.reporting.csv.enablePanelActionDownload`
+ | Enables CSV export from a saved search on a dashboard. This action is available in the dashboard
+ panel menu for the saved search.
+ Defaults to `true`.
+
|===
[float]
From 0e008e30e98f760c5e08db0e5847e103eff49ca6 Mon Sep 17 00:00:00 2001
From: Jonathan Budzenski
Date: Thu, 2 Jul 2020 12:46:40 -0500
Subject: [PATCH 29/49] skip fleet agent flow tests
---
x-pack/test/api_integration/apis/fleet/agent_flow.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/test/api_integration/apis/fleet/agent_flow.ts b/x-pack/test/api_integration/apis/fleet/agent_flow.ts
index 71057b81d1b09..a6a4003a554fc 100644
--- a/x-pack/test/api_integration/apis/fleet/agent_flow.ts
+++ b/x-pack/test/api_integration/apis/fleet/agent_flow.ts
@@ -18,7 +18,7 @@ export default function (providerContext: FtrProviderContext) {
const supertestWithoutAuth = getSupertestWithoutAuth(providerContext);
const esClient = getService('es');
- describe('fleet_agent_flow', () => {
+ describe.skip('fleet_agent_flow', () => {
before(async () => {
await esArchiver.load('empty_kibana');
});
From 52f8d3a58b4989541e532f6b9e5dde067f7ff303 Mon Sep 17 00:00:00 2001
From: liza-mae
Date: Thu, 2 Jul 2020 13:41:23 -0600
Subject: [PATCH 30/49] Build docker image for elasticsearch snapshot (#70482)
* Build docker image for elasticsearch snapshot
* Consolidate statements
* Update .ci/es-snapshots/Jenkinsfile_build_es
Co-authored-by: Brian Seeders
* Update find
* Use larger worker
Co-authored-by: Elastic Machine
Co-authored-by: Brian Seeders
---
.ci/es-snapshots/Jenkinsfile_build_es | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es
index a3470cd750738..aafdf06433c6d 100644
--- a/.ci/es-snapshots/Jenkinsfile_build_es
+++ b/.ci/es-snapshots/Jenkinsfile_build_es
@@ -25,7 +25,7 @@ def PROMOTE_WITHOUT_VERIFY = !!params.PROMOTE_WITHOUT_VERIFICATION
timeout(time: 120, unit: 'MINUTES') {
timestamps {
ansiColor('xterm') {
- node(workers.label('s')) {
+ node(workers.label('l')) {
catchErrors {
def VERSION
def SNAPSHOT_ID
@@ -154,9 +154,10 @@ def buildArchives(destination) {
"NODE_NAME=",
]) {
sh """
- ./gradlew -p distribution/archives assemble --parallel
+ ./gradlew -Dbuild.docker=true assemble --parallel
mkdir -p ${destination}
- find distribution/archives -type f \\( -name 'elasticsearch-*-*-*-*.tar.gz' -o -name 'elasticsearch-*-*-*-*.zip' \\) -not -path *no-jdk* -exec cp {} ${destination} \\;
+ find distribution -type f \\( -name 'elasticsearch-*-*-*-*.tar.gz' -o -name 'elasticsearch-*-*-*-*.zip' \\) -not -path *no-jdk* -not -path *build-context* -exec cp {} ${destination} \\;
+ docker images "docker.elastic.co/elasticsearch/elasticsearch" --format "{{.Tag}}" | xargs -n1 bash -c 'docker save docker.elastic.co/elasticsearch/elasticsearch:\${0} | gzip > ${destination}/elasticsearch-\${0}-docker-image.tar.gz'
"""
}
}
From 5b8fb95d0015ed98877edbbbe7b7b03229d5f497 Mon Sep 17 00:00:00 2001
From: Nicolas Ruflin
Date: Thu, 2 Jul 2020 21:46:57 +0200
Subject: [PATCH 31/49] Remove the base package from the default packages
(#70437)
As the base assets will be shipped by ES directly, the base package is not needed anymore. https://github.com/elastic/elasticsearch/pull/57629
In the future we might reintroduce it to update the installed assets.
---
x-pack/plugins/ingest_manager/common/types/models/epm.ts | 1 -
1 file changed, 1 deletion(-)
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 bf6a8de15182d..3ee3039e9e1c4 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -250,7 +250,6 @@ export enum IngestAssetType {
}
export enum DefaultPackages {
- base = 'base',
system = 'system',
endpoint = 'endpoint',
}
From a3e9f39aff523de911a7d6f9eaa2932a6b6077f9 Mon Sep 17 00:00:00 2001
From: Chris Cowan
Date: Thu, 2 Jul 2020 13:01:18 -0700
Subject: [PATCH 32/49] [Metrics UI] Register function for Observability
homepage (#70529)
* [Metrics UI] Register function for Observability homepage
* Updating types; removing relative path from appLink
Co-authored-by: Elastic Machine
---
.../metrics_overview_fetchers.test.ts.snap | 215 ++++++++++++
.../public/metrics_overview_fetchers.test.ts | 91 ++++++
.../infra/public/metrics_overview_fetchers.ts | 161 +++++++++
x-pack/plugins/infra/public/plugin.ts | 7 +
.../plugins/infra/public/test_utils/index.ts | 309 ++++++++++++++++++
.../typings/fetch_overview_data/index.ts | 1 -
6 files changed, 783 insertions(+), 1 deletion(-)
create mode 100644 x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
create mode 100644 x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts
create mode 100644 x-pack/plugins/infra/public/metrics_overview_fetchers.ts
create mode 100644 x-pack/plugins/infra/public/test_utils/index.ts
diff --git a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
new file mode 100644
index 0000000000000..99ab129fc36e3
--- /dev/null
+++ b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
@@ -0,0 +1,215 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Metrics UI Observability Homepage Functions createMetricsFetchData() should just work 1`] = `
+Object {
+ "appLink": "/app/metrics",
+ "series": Object {
+ "inboundTraffic": Object {
+ "coordinates": Array [
+ Object {
+ "x": 1593630455000,
+ "y": 0,
+ },
+ Object {
+ "x": 1593630755000,
+ "y": 3.5,
+ },
+ Object {
+ "x": 1593631055000,
+ "y": 3.5,
+ },
+ Object {
+ "x": 1593631355000,
+ "y": 8.5,
+ },
+ Object {
+ "x": 1593631655000,
+ "y": 3.5,
+ },
+ Object {
+ "x": 1593631955000,
+ "y": 2.5,
+ },
+ Object {
+ "x": 1593632255000,
+ "y": 1.5,
+ },
+ Object {
+ "x": 1593632555000,
+ "y": 1.5,
+ },
+ Object {
+ "x": 1593632855000,
+ "y": 3.5,
+ },
+ Object {
+ "x": 1593633155000,
+ "y": 2.5,
+ },
+ Object {
+ "x": 1593633455000,
+ "y": 1.5,
+ },
+ Object {
+ "x": 1593633755000,
+ "y": 1.5,
+ },
+ Object {
+ "x": 1593634055000,
+ "y": 2.5,
+ },
+ Object {
+ "x": 1593634355000,
+ "y": 0,
+ },
+ Object {
+ "x": 1593634655000,
+ "y": 10.5,
+ },
+ Object {
+ "x": 1593634955000,
+ "y": 5.5,
+ },
+ Object {
+ "x": 1593635255000,
+ "y": 13.5,
+ },
+ Object {
+ "x": 1593635555000,
+ "y": 9.5,
+ },
+ Object {
+ "x": 1593635855000,
+ "y": 7.5,
+ },
+ Object {
+ "x": 1593636155000,
+ "y": 3,
+ },
+ Object {
+ "x": 1593636455000,
+ "y": 3.5,
+ },
+ ],
+ "label": "Inbound traffic",
+ },
+ "outboundTraffic": Object {
+ "coordinates": Array [
+ Object {
+ "x": 1593630455000,
+ "y": 0,
+ },
+ Object {
+ "x": 1593630755000,
+ "y": 4,
+ },
+ Object {
+ "x": 1593631055000,
+ "y": 4,
+ },
+ Object {
+ "x": 1593631355000,
+ "y": 9,
+ },
+ Object {
+ "x": 1593631655000,
+ "y": 4,
+ },
+ Object {
+ "x": 1593631955000,
+ "y": 2.5,
+ },
+ Object {
+ "x": 1593632255000,
+ "y": 2,
+ },
+ Object {
+ "x": 1593632555000,
+ "y": 2,
+ },
+ Object {
+ "x": 1593632855000,
+ "y": 4,
+ },
+ Object {
+ "x": 1593633155000,
+ "y": 3,
+ },
+ Object {
+ "x": 1593633455000,
+ "y": 2,
+ },
+ Object {
+ "x": 1593633755000,
+ "y": 2,
+ },
+ Object {
+ "x": 1593634055000,
+ "y": 2.5,
+ },
+ Object {
+ "x": 1593634355000,
+ "y": 1,
+ },
+ Object {
+ "x": 1593634655000,
+ "y": 11,
+ },
+ Object {
+ "x": 1593634955000,
+ "y": 6,
+ },
+ Object {
+ "x": 1593635255000,
+ "y": 14,
+ },
+ Object {
+ "x": 1593635555000,
+ "y": 10,
+ },
+ Object {
+ "x": 1593635855000,
+ "y": 8,
+ },
+ Object {
+ "x": 1593636155000,
+ "y": 3,
+ },
+ Object {
+ "x": 1593636455000,
+ "y": 4,
+ },
+ ],
+ "label": "Outbound traffic",
+ },
+ },
+ "stats": Object {
+ "cpu": Object {
+ "label": "CPU usage",
+ "type": "percent",
+ "value": 0.0015,
+ },
+ "hosts": Object {
+ "label": "Hosts",
+ "type": "number",
+ "value": 2,
+ },
+ "inboundTraffic": Object {
+ "label": "Inbound traffic",
+ "type": "bytesPerSecond",
+ "value": 3.5,
+ },
+ "memory": Object {
+ "label": "Memory usage",
+ "type": "percent",
+ "value": 0.0015,
+ },
+ "outboundTraffic": Object {
+ "label": "Outbound traffic",
+ "type": "bytesPerSecond",
+ "value": 3,
+ },
+ },
+ "title": "Metrics",
+}
+`;
diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts
new file mode 100644
index 0000000000000..21946c7c5653a
--- /dev/null
+++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.test.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { coreMock } from 'src/core/public/mocks';
+import { createMetricsHasData, createMetricsFetchData } from './metrics_overview_fetchers';
+import { CoreStart } from 'kibana/public';
+import { InfraClientStartDeps, InfraClientStartExports } from './types';
+import moment from 'moment';
+import { FAKE_SNAPSHOT_RESPONSE } from './test_utils';
+
+function setup() {
+ const core = coreMock.createStart();
+ const mockedGetStartServices = jest.fn(() => {
+ const deps = {};
+ return Promise.resolve([
+ core as CoreStart,
+ deps as InfraClientStartDeps,
+ void 0 as InfraClientStartExports,
+ ]) as Promise<[CoreStart, InfraClientStartDeps, InfraClientStartExports]>;
+ });
+ return { core, mockedGetStartServices };
+}
+
+describe('Metrics UI Observability Homepage Functions', () => {
+ describe('createMetricsHasData()', () => {
+ it('should return true when true', async () => {
+ const { core, mockedGetStartServices } = setup();
+ core.http.get.mockResolvedValue({
+ status: {
+ indexFields: [],
+ logIndicesExist: false,
+ metricIndicesExist: true,
+ },
+ });
+ const hasData = createMetricsHasData(mockedGetStartServices);
+ const response = await hasData();
+ expect(core.http.get).toHaveBeenCalledTimes(1);
+ expect(response).toBeTruthy();
+ });
+ it('should return false when false', async () => {
+ const { core, mockedGetStartServices } = setup();
+ core.http.get.mockResolvedValue({
+ status: {
+ indexFields: [],
+ logIndicesExist: false,
+ metricIndicesExist: false,
+ },
+ });
+ const hasData = createMetricsHasData(mockedGetStartServices);
+ const response = await hasData();
+ expect(core.http.get).toHaveBeenCalledTimes(1);
+ expect(response).toBeFalsy();
+ });
+ });
+
+ describe('createMetricsFetchData()', () => {
+ it('should just work', async () => {
+ const { core, mockedGetStartServices } = setup();
+ core.http.post.mockResolvedValue(FAKE_SNAPSHOT_RESPONSE);
+ const fetchData = createMetricsFetchData(mockedGetStartServices);
+ const endTime = moment();
+ const startTime = endTime.clone().subtract(1, 'h');
+ const bucketSize = '300s';
+ const response = await fetchData({
+ startTime: startTime.toISOString(),
+ endTime: endTime.toISOString(),
+ bucketSize,
+ });
+ expect(core.http.post).toHaveBeenCalledTimes(1);
+ expect(core.http.post).toHaveBeenCalledWith('/api/metrics/snapshot', {
+ body: JSON.stringify({
+ sourceId: 'default',
+ metrics: [{ type: 'cpu' }, { type: 'memory' }, { type: 'rx' }, { type: 'tx' }],
+ groupBy: [],
+ nodeType: 'host',
+ timerange: {
+ from: startTime.valueOf(),
+ to: endTime.valueOf(),
+ interval: '300s',
+ forceInterval: true,
+ ignoreLookback: true,
+ },
+ }),
+ });
+ expect(response).toMatchSnapshot();
+ });
+ });
+});
diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts
new file mode 100644
index 0000000000000..d10ad5dda5320
--- /dev/null
+++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts
@@ -0,0 +1,161 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import moment from 'moment';
+import { sum, isFinite, isNumber } from 'lodash';
+import { i18n } from '@kbn/i18n';
+import { MetricsFetchDataResponse, FetchDataParams } from '../../observability/public';
+import {
+ SnapshotRequest,
+ SnapshotMetricInput,
+ SnapshotNode,
+ SnapshotNodeResponse,
+} from '../common/http_api/snapshot_api';
+import { SnapshotMetricType } from '../common/inventory_models/types';
+import { InfraClientCoreSetup } from './types';
+import { SourceResponse } from '../common/http_api/source_api';
+
+export const createMetricsHasData = (
+ getStartServices: InfraClientCoreSetup['getStartServices']
+) => async () => {
+ const [coreServices] = await getStartServices();
+ const { http } = coreServices;
+ const results = await http.get('/api/metrics/source/default/metrics');
+ return results.status.metricIndicesExist;
+};
+
+export const average = (values: number[]) => (values.length ? sum(values) / values.length : 0);
+
+export const combineNodesBy = (
+ metric: SnapshotMetricType,
+ nodes: SnapshotNode[],
+ combinator: (values: number[]) => number
+) => {
+ const values = nodes.reduce((acc, node) => {
+ const snapshotMetric = node.metrics.find((m) => m.name === metric);
+ if (snapshotMetric?.value != null && isFinite(snapshotMetric.value)) {
+ acc.push(snapshotMetric.value);
+ }
+ return acc;
+ }, [] as number[]);
+ return combinator(values);
+};
+
+interface CombinedRow {
+ values: number[];
+ timestamp: number;
+}
+
+export const combineNodeTimeseriesBy = (
+ metric: SnapshotMetricType,
+ nodes: SnapshotNode[],
+ combinator: (values: number[]) => number
+) => {
+ const combinedTimeseries = nodes.reduce((acc, node) => {
+ const snapshotMetric = node.metrics.find((m) => m.name === metric);
+ if (snapshotMetric && snapshotMetric.timeseries) {
+ snapshotMetric.timeseries.rows.forEach((row) => {
+ const combinedRow = acc.find((r) => r.timestamp === row.timestamp);
+ if (combinedRow) {
+ combinedRow.values.push(isNumber(row.metric_0) ? row.metric_0 : 0);
+ } else {
+ acc.push({
+ timestamp: row.timestamp,
+ values: [isNumber(row.metric_0) ? row.metric_0 : 0],
+ });
+ }
+ });
+ }
+ return acc;
+ }, [] as CombinedRow[]);
+ return combinedTimeseries.map((row) => ({ x: row.timestamp, y: combinator(row.values) }));
+};
+
+export const createMetricsFetchData = (
+ getStartServices: InfraClientCoreSetup['getStartServices']
+) => async ({
+ startTime,
+ endTime,
+ bucketSize,
+}: FetchDataParams): Promise => {
+ const [coreServices] = await getStartServices();
+ const { http } = coreServices;
+ const snapshotRequest: SnapshotRequest = {
+ sourceId: 'default',
+ metrics: ['cpu', 'memory', 'rx', 'tx'].map((type) => ({ type })) as SnapshotMetricInput[],
+ groupBy: [],
+ nodeType: 'host',
+ timerange: {
+ from: moment(startTime).valueOf(),
+ to: moment(endTime).valueOf(),
+ interval: bucketSize,
+ forceInterval: true,
+ ignoreLookback: true,
+ },
+ };
+
+ const results = await http.post('/api/metrics/snapshot', {
+ body: JSON.stringify(snapshotRequest),
+ });
+
+ const inboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.rxLabel', {
+ defaultMessage: 'Inbound traffic',
+ });
+
+ const outboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.txLabel', {
+ defaultMessage: 'Outbound traffic',
+ });
+
+ return {
+ title: i18n.translate('xpack.infra.observabilityHomepage.metrics.title', {
+ defaultMessage: 'Metrics',
+ }),
+ appLink: '/app/metrics',
+ stats: {
+ hosts: {
+ type: 'number',
+ label: i18n.translate('xpack.infra.observabilityHomepage.metrics.hostsLabel', {
+ defaultMessage: 'Hosts',
+ }),
+ value: results.nodes.length,
+ },
+ cpu: {
+ type: 'percent',
+ label: i18n.translate('xpack.infra.observabilityHomepage.metrics.cpuLabel', {
+ defaultMessage: 'CPU usage',
+ }),
+ value: combineNodesBy('cpu', results.nodes, average),
+ },
+ memory: {
+ type: 'percent',
+ label: i18n.translate('xpack.infra.observabilityHomepage.metrics.memoryLabel', {
+ defaultMessage: 'Memory usage',
+ }),
+ value: combineNodesBy('memory', results.nodes, average),
+ },
+ inboundTraffic: {
+ type: 'bytesPerSecond',
+ label: inboundLabel,
+ value: combineNodesBy('rx', results.nodes, average),
+ },
+ outboundTraffic: {
+ type: 'bytesPerSecond',
+ label: outboundLabel,
+ value: combineNodesBy('tx', results.nodes, average),
+ },
+ },
+ series: {
+ inboundTraffic: {
+ label: inboundLabel,
+ coordinates: combineNodeTimeseriesBy('rx', results.nodes, average),
+ },
+ outboundTraffic: {
+ label: outboundLabel,
+ coordinates: combineNodeTimeseriesBy('tx', results.nodes, average),
+ },
+ },
+ };
+};
diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts
index 1b28945320bb6..2dda664a7f675 100644
--- a/x-pack/plugins/infra/public/plugin.ts
+++ b/x-pack/plugins/infra/public/plugin.ts
@@ -19,6 +19,7 @@ import {
InfraClientPluginClass,
} from './types';
import { getLogsHasDataFetcher, getLogsOverviewDataFetcher } from './utils/logs_overview_fetchers';
+import { createMetricsHasData, createMetricsFetchData } from './metrics_overview_fetchers';
export class Plugin implements InfraClientPluginClass {
constructor(_context: PluginInitializerContext) {}
@@ -36,6 +37,12 @@ export class Plugin implements InfraClientPluginClass {
hasData: getLogsHasDataFetcher(core.getStartServices),
fetchData: getLogsOverviewDataFetcher(core.getStartServices),
});
+
+ pluginsSetup.observability.dashboard.register({
+ appName: 'infra_metrics',
+ hasData: createMetricsHasData(core.getStartServices),
+ fetchData: createMetricsFetchData(core.getStartServices),
+ });
}
core.application.register({
diff --git a/x-pack/plugins/infra/public/test_utils/index.ts b/x-pack/plugins/infra/public/test_utils/index.ts
new file mode 100644
index 0000000000000..3de4c40f47cc9
--- /dev/null
+++ b/x-pack/plugins/infra/public/test_utils/index.ts
@@ -0,0 +1,309 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const FAKE_SNAPSHOT_RESPONSE = {
+ nodes: [
+ {
+ path: [{ value: 'host-01', label: 'host-01', ip: '192.168.1.10' }],
+ metrics: [
+ {
+ name: 'memory',
+ value: 0.002,
+ max: 0.00134,
+ avg: 0.0009833333333333335,
+ timeseries: {
+ id: 'memory',
+ columns: [
+ { name: 'timestamp', type: 'date' },
+ { name: 'metric_0', type: 'number' },
+ ],
+ rows: [
+ { timestamp: 1593630455000, metric_0: 0 },
+ { timestamp: 1593630755000, metric_0: 0.001 },
+ { timestamp: 1593631055000, metric_0: 0.00099 },
+ { timestamp: 1593631355000, metric_0: 0.00133 },
+ { timestamp: 1593631655000, metric_0: 0.00099 },
+ { timestamp: 1593631955000, metric_0: 0.001 },
+ { timestamp: 1593632255000, metric_0: 0.00099 },
+ { timestamp: 1593632555000, metric_0: 0.00067 },
+ { timestamp: 1593632855000, metric_0: 0.001 },
+ { timestamp: 1593633155000, metric_0: 0.00099 },
+ { timestamp: 1593633455000, metric_0: 0.00099 },
+ { timestamp: 1593633755000, metric_0: 0.00099 },
+ { timestamp: 1593634055000, metric_0: 0.001 },
+ { timestamp: 1593634355000, metric_0: 0.00067 },
+ { timestamp: 1593634655000, metric_0: 0.00133 },
+ { timestamp: 1593634955000, metric_0: 0.00101 },
+ { timestamp: 1593635255000, metric_0: 0.00134 },
+ { timestamp: 1593635555000, metric_0: 0.00133 },
+ { timestamp: 1593635855000, metric_0: 0.00102 },
+ { timestamp: 1593636155000, metric_0: 0.00101 },
+ { timestamp: 1593636455000, metric_0: 0.001 },
+ ],
+ },
+ },
+ {
+ name: 'cpu',
+ value: 0.002,
+ max: 0.00134,
+ avg: 0.0009833333333333335,
+ timeseries: {
+ id: 'cpu',
+ columns: [
+ { name: 'timestamp', type: 'date' },
+ { name: 'metric_0', type: 'number' },
+ ],
+ rows: [
+ { timestamp: 1593630455000, metric_0: 0 },
+ { timestamp: 1593630755000, metric_0: 0.001 },
+ { timestamp: 1593631055000, metric_0: 0.00099 },
+ { timestamp: 1593631355000, metric_0: 0.00133 },
+ { timestamp: 1593631655000, metric_0: 0.00099 },
+ { timestamp: 1593631955000, metric_0: 0.001 },
+ { timestamp: 1593632255000, metric_0: 0.00099 },
+ { timestamp: 1593632555000, metric_0: 0.00067 },
+ { timestamp: 1593632855000, metric_0: 0.001 },
+ { timestamp: 1593633155000, metric_0: 0.00099 },
+ { timestamp: 1593633455000, metric_0: 0.00099 },
+ { timestamp: 1593633755000, metric_0: 0.00099 },
+ { timestamp: 1593634055000, metric_0: 0.001 },
+ { timestamp: 1593634355000, metric_0: 0.00067 },
+ { timestamp: 1593634655000, metric_0: 0.00133 },
+ { timestamp: 1593634955000, metric_0: 0.00101 },
+ { timestamp: 1593635255000, metric_0: 0.00134 },
+ { timestamp: 1593635555000, metric_0: 0.00133 },
+ { timestamp: 1593635855000, metric_0: 0.00102 },
+ { timestamp: 1593636155000, metric_0: 0.00101 },
+ { timestamp: 1593636455000, metric_0: 0.001 },
+ ],
+ },
+ },
+ {
+ name: 'rx',
+ value: 4,
+ max: 13,
+ avg: 3.761904761904762,
+ timeseries: {
+ id: 'rx',
+ columns: [
+ { name: 'timestamp', type: 'date' },
+ { name: 'metric_0', type: 'number' },
+ ],
+ rows: [
+ { timestamp: 1593630455000, metric_0: 0 },
+ { timestamp: 1593630755000, metric_0: 4 },
+ { timestamp: 1593631055000, metric_0: 4 },
+ { timestamp: 1593631355000, metric_0: 9 },
+ { timestamp: 1593631655000, metric_0: 4 },
+ { timestamp: 1593631955000, metric_0: 3 },
+ { timestamp: 1593632255000, metric_0: 2 },
+ { timestamp: 1593632555000, metric_0: 2 },
+ { timestamp: 1593632855000, metric_0: 4 },
+ { timestamp: 1593633155000, metric_0: 3 },
+ { timestamp: 1593633455000, metric_0: 2 },
+ { timestamp: 1593633755000, metric_0: 2 },
+ { timestamp: 1593634055000, metric_0: 3 },
+ { timestamp: 1593634355000, metric_0: 0 },
+ { timestamp: 1593634655000, metric_0: 11 },
+ { timestamp: 1593634955000, metric_0: 6 },
+ { timestamp: 1593635255000, metric_0: 14 },
+ { timestamp: 1593635555000, metric_0: 10 },
+ { timestamp: 1593635855000, metric_0: 8 },
+ { timestamp: 1593636155000, metric_0: 4 },
+ { timestamp: 1593636455000, metric_0: 4 },
+ ],
+ },
+ },
+ {
+ name: 'tx',
+ value: 3,
+ max: 13,
+ avg: 3.761904761904762,
+ timeseries: {
+ id: 'tx',
+ columns: [
+ { name: 'timestamp', type: 'date' },
+ { name: 'metric_0', type: 'number' },
+ ],
+ rows: [
+ { timestamp: 1593630455000, metric_0: 0 },
+ { timestamp: 1593630755000, metric_0: 5 },
+ { timestamp: 1593631055000, metric_0: 5 },
+ { timestamp: 1593631355000, metric_0: 10 },
+ { timestamp: 1593631655000, metric_0: 5 },
+ { timestamp: 1593631955000, metric_0: 3 },
+ { timestamp: 1593632255000, metric_0: 3 },
+ { timestamp: 1593632555000, metric_0: 3 },
+ { timestamp: 1593632855000, metric_0: 5 },
+ { timestamp: 1593633155000, metric_0: 4 },
+ { timestamp: 1593633455000, metric_0: 3 },
+ { timestamp: 1593633755000, metric_0: 3 },
+ { timestamp: 1593634055000, metric_0: 3 },
+ { timestamp: 1593634355000, metric_0: 2 },
+ { timestamp: 1593634655000, metric_0: 12 },
+ { timestamp: 1593634955000, metric_0: 7 },
+ { timestamp: 1593635255000, metric_0: 15 },
+ { timestamp: 1593635555000, metric_0: 11 },
+ { timestamp: 1593635855000, metric_0: 9 },
+ { timestamp: 1593636155000, metric_0: 4 },
+ { timestamp: 1593636455000, metric_0: 5 },
+ ],
+ },
+ },
+ ],
+ },
+ {
+ path: [{ value: 'host-02', label: 'host-02', ip: '192.168.1.11' }],
+ metrics: [
+ {
+ name: 'memory',
+ value: 0.001,
+ max: 0.00134,
+ avg: 0.0009833333333333335,
+ timeseries: {
+ id: 'memory',
+ columns: [
+ { name: 'timestamp', type: 'date' },
+ { name: 'metric_0', type: 'number' },
+ ],
+ rows: [
+ { timestamp: 1593630455000, metric_0: 0 },
+ { timestamp: 1593630755000, metric_0: 0.001 },
+ { timestamp: 1593631055000, metric_0: 0.00099 },
+ { timestamp: 1593631355000, metric_0: 0.00133 },
+ { timestamp: 1593631655000, metric_0: 0.00099 },
+ { timestamp: 1593631955000, metric_0: 0.001 },
+ { timestamp: 1593632255000, metric_0: 0.00099 },
+ { timestamp: 1593632555000, metric_0: 0.00067 },
+ { timestamp: 1593632855000, metric_0: 0.001 },
+ { timestamp: 1593633155000, metric_0: 0.00099 },
+ { timestamp: 1593633455000, metric_0: 0.00099 },
+ { timestamp: 1593633755000, metric_0: 0.00099 },
+ { timestamp: 1593634055000, metric_0: 0.001 },
+ { timestamp: 1593634355000, metric_0: 0.00067 },
+ { timestamp: 1593634655000, metric_0: 0.00133 },
+ { timestamp: 1593634955000, metric_0: 0.00101 },
+ { timestamp: 1593635255000, metric_0: 0.00134 },
+ { timestamp: 1593635555000, metric_0: 0.00133 },
+ { timestamp: 1593635855000, metric_0: 0.00102 },
+ { timestamp: 1593636155000, metric_0: 0.00101 },
+ { timestamp: 1593636455000, metric_0: 0.001 },
+ ],
+ },
+ },
+ {
+ name: 'cpu',
+ value: 0.001,
+ max: 0.00134,
+ avg: 0.0009833333333333335,
+ timeseries: {
+ id: 'cpu',
+ columns: [
+ { name: 'timestamp', type: 'date' },
+ { name: 'metric_0', type: 'number' },
+ ],
+ rows: [
+ { timestamp: 1593630455000, metric_0: 0 },
+ { timestamp: 1593630755000, metric_0: 0.001 },
+ { timestamp: 1593631055000, metric_0: 0.00099 },
+ { timestamp: 1593631355000, metric_0: 0.00133 },
+ { timestamp: 1593631655000, metric_0: 0.00099 },
+ { timestamp: 1593631955000, metric_0: 0.001 },
+ { timestamp: 1593632255000, metric_0: 0.00099 },
+ { timestamp: 1593632555000, metric_0: 0.00067 },
+ { timestamp: 1593632855000, metric_0: 0.001 },
+ { timestamp: 1593633155000, metric_0: 0.00099 },
+ { timestamp: 1593633455000, metric_0: 0.00099 },
+ { timestamp: 1593633755000, metric_0: 0.00099 },
+ { timestamp: 1593634055000, metric_0: 0.001 },
+ { timestamp: 1593634355000, metric_0: 0.00067 },
+ { timestamp: 1593634655000, metric_0: 0.00133 },
+ { timestamp: 1593634955000, metric_0: 0.00101 },
+ { timestamp: 1593635255000, metric_0: 0.00134 },
+ { timestamp: 1593635555000, metric_0: 0.00133 },
+ { timestamp: 1593635855000, metric_0: 0.00102 },
+ { timestamp: 1593636155000, metric_0: 0.00101 },
+ { timestamp: 1593636455000, metric_0: 0.001 },
+ ],
+ },
+ },
+ {
+ name: 'rx',
+ value: 3,
+ max: 13,
+ avg: 3.761904761904762,
+ timeseries: {
+ id: 'rx',
+ columns: [
+ { name: 'timestamp', type: 'date' },
+ { name: 'metric_0', type: 'number' },
+ ],
+ rows: [
+ { timestamp: 1593630455000, metric_0: 0 },
+ { timestamp: 1593630755000, metric_0: 3 },
+ { timestamp: 1593631055000, metric_0: 3 },
+ { timestamp: 1593631355000, metric_0: 8 },
+ { timestamp: 1593631655000, metric_0: 3 },
+ { timestamp: 1593631955000, metric_0: 2 },
+ { timestamp: 1593632255000, metric_0: 1 },
+ { timestamp: 1593632555000, metric_0: 1 },
+ { timestamp: 1593632855000, metric_0: 3 },
+ { timestamp: 1593633155000, metric_0: 2 },
+ { timestamp: 1593633455000, metric_0: 1 },
+ { timestamp: 1593633755000, metric_0: 1 },
+ { timestamp: 1593634055000, metric_0: 2 },
+ { timestamp: 1593634355000, metric_0: 0 },
+ { timestamp: 1593634655000, metric_0: 10 },
+ { timestamp: 1593634955000, metric_0: 5 },
+ { timestamp: 1593635255000, metric_0: 13 },
+ { timestamp: 1593635555000, metric_0: 9 },
+ { timestamp: 1593635855000, metric_0: 7 },
+ { timestamp: 1593636155000, metric_0: 2 },
+ { timestamp: 1593636455000, metric_0: 3 },
+ ],
+ },
+ },
+ {
+ name: 'tx',
+ value: 3,
+ max: 13,
+ avg: 3.761904761904762,
+ timeseries: {
+ id: 'tx',
+ columns: [
+ { name: 'timestamp', type: 'date' },
+ { name: 'metric_0', type: 'number' },
+ ],
+ rows: [
+ { timestamp: 1593630455000, metric_0: 0 },
+ { timestamp: 1593630755000, metric_0: 3 },
+ { timestamp: 1593631055000, metric_0: 3 },
+ { timestamp: 1593631355000, metric_0: 8 },
+ { timestamp: 1593631655000, metric_0: 3 },
+ { timestamp: 1593631955000, metric_0: 2 },
+ { timestamp: 1593632255000, metric_0: 1 },
+ { timestamp: 1593632555000, metric_0: 1 },
+ { timestamp: 1593632855000, metric_0: 3 },
+ { timestamp: 1593633155000, metric_0: 2 },
+ { timestamp: 1593633455000, metric_0: 1 },
+ { timestamp: 1593633755000, metric_0: 1 },
+ { timestamp: 1593634055000, metric_0: 2 },
+ { timestamp: 1593634355000, metric_0: 0 },
+ { timestamp: 1593634655000, metric_0: 10 },
+ { timestamp: 1593634955000, metric_0: 5 },
+ { timestamp: 1593635255000, metric_0: 13 },
+ { timestamp: 1593635555000, metric_0: 9 },
+ { timestamp: 1593635855000, metric_0: 7 },
+ { timestamp: 1593636155000, metric_0: 2 },
+ { timestamp: 1593636455000, metric_0: 3 },
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ interval: '300s',
+};
diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
index e65d1779520cf..e57dfebb36419 100644
--- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
+++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts
@@ -59,7 +59,6 @@ export interface MetricsFetchDataResponse extends FetchDataResponse {
hosts: Stat;
cpu: Stat;
memory: Stat;
- disk: Stat;
inboundTraffic: Stat;
outboundTraffic: Stat;
};
From a921bbf4c2e31fb0a02c7c31cef56ea009bb0ded Mon Sep 17 00:00:00 2001
From: Kevin Logan <56395104+kevinlog@users.noreply.github.com>
Date: Thu, 2 Jul 2020 16:18:52 -0400
Subject: [PATCH 33/49] [SECURITY SOLUTION] Remove unneeded options from
default policy, remove Notify user option from UI (#70546)
---
.../common/endpoint/models/policy_config.ts | 44 +------------------
.../common/endpoint/types.ts | 21 ---------
.../view/policy_forms/protections/malware.tsx | 7 ---
.../apps/endpoint/policy_details.ts | 38 ++--------------
4 files changed, 5 insertions(+), 105 deletions(-)
diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
index 199b8a91e4307..37b7308856196 100644
--- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts
@@ -25,22 +25,8 @@ export const factory = (): PolicyConfig => {
mode: ProtectionModes.prevent,
},
logging: {
- stdout: 'debug',
file: 'info',
},
- advanced: {
- elasticsearch: {
- indices: {
- control: 'control-index',
- event: 'event-index',
- logging: 'logging-index',
- },
- kernel: {
- connect: true,
- process: true,
- },
- },
- },
},
mac: {
events: {
@@ -49,25 +35,11 @@ export const factory = (): PolicyConfig => {
network: true,
},
malware: {
- mode: ProtectionModes.detect,
+ mode: ProtectionModes.prevent,
},
logging: {
- stdout: 'debug',
file: 'info',
},
- advanced: {
- elasticsearch: {
- indices: {
- control: 'control-index',
- event: 'event-index',
- logging: 'logging-index',
- },
- kernel: {
- connect: true,
- process: true,
- },
- },
- },
},
linux: {
events: {
@@ -76,22 +48,8 @@ export const factory = (): PolicyConfig => {
network: true,
},
logging: {
- stdout: 'debug',
file: 'info',
},
- advanced: {
- elasticsearch: {
- indices: {
- control: 'control-index',
- event: 'event-index',
- logging: 'logging-index',
- },
- kernel: {
- connect: true,
- process: true,
- },
- },
- },
},
};
};
diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts
index 42b1337a91464..f2b8acb627cc4 100644
--- a/x-pack/plugins/security_solution/common/endpoint/types.ts
+++ b/x-pack/plugins/security_solution/common/endpoint/types.ts
@@ -613,10 +613,8 @@ export interface PolicyConfig {
};
malware: MalwareFields;
logging: {
- stdout: string;
file: string;
};
- advanced: PolicyConfigAdvancedOptions;
};
mac: {
events: {
@@ -626,10 +624,8 @@ export interface PolicyConfig {
};
malware: MalwareFields;
logging: {
- stdout: string;
file: string;
};
- advanced: PolicyConfigAdvancedOptions;
};
linux: {
events: {
@@ -638,10 +634,8 @@ export interface PolicyConfig {
network: boolean;
};
logging: {
- stdout: string;
file: string;
};
- advanced: PolicyConfigAdvancedOptions;
};
}
@@ -663,20 +657,6 @@ export interface UIPolicyConfig {
linux: Pick;
}
-interface PolicyConfigAdvancedOptions {
- elasticsearch: {
- indices: {
- control: string;
- event: string;
- logging: string;
- };
- kernel: {
- connect: boolean;
- process: boolean;
- };
- };
-}
-
/** Policy: Malware protection fields */
export interface MalwareFields {
mode: ProtectionModes;
@@ -686,7 +666,6 @@ export interface MalwareFields {
export enum ProtectionModes {
detect = 'detect',
prevent = 'prevent',
- preventNotify = 'preventNotify',
off = 'off',
}
diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx
index 77d4d4364acdd..23ac6cc5b813d 100644
--- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx
+++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx
@@ -95,13 +95,6 @@ export const MalwareProtections = React.memo(() => {
}),
protection: 'malware',
},
- {
- id: ProtectionModes.preventNotify,
- label: i18n.translate('xpack.securitySolution.endpoint.policy.details.preventAndNotify', {
- defaultMessage: 'Prevent and notify user',
- }),
- protection: 'malware',
- },
];
}, []);
diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
index 45ea82c59bf97..bacba619e5648 100644
--- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
+++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts
@@ -118,45 +118,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
},
policy: {
linux: {
- advanced: {
- elasticsearch: {
- indices: {
- control: 'control-index',
- event: 'event-index',
- logging: 'logging-index',
- },
- kernel: { connect: true, process: true },
- },
- },
events: { file: false, network: true, process: true },
- logging: { file: 'info', stdout: 'debug' },
+ logging: { file: 'info' },
},
mac: {
- advanced: {
- elasticsearch: {
- indices: {
- control: 'control-index',
- event: 'event-index',
- logging: 'logging-index',
- },
- kernel: { connect: true, process: true },
- },
- },
events: { file: false, network: true, process: true },
- logging: { file: 'info', stdout: 'debug' },
- malware: { mode: 'detect' },
+ logging: { file: 'info' },
+ malware: { mode: 'prevent' },
},
windows: {
- advanced: {
- elasticsearch: {
- indices: {
- control: 'control-index',
- event: 'event-index',
- logging: 'logging-index',
- },
- kernel: { connect: true, process: true },
- },
- },
events: {
dll_and_driver_load: true,
dns: true,
@@ -166,7 +136,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
registry: true,
security: true,
},
- logging: { file: 'info', stdout: 'debug' },
+ logging: { file: 'info' },
malware: { mode: 'prevent' },
},
},
From fa70afb5ef3e6106f3b5d31d2f043e4b979a30ba Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?=
Date: Thu, 2 Jul 2020 22:34:53 +0200
Subject: [PATCH 34/49] [APM] Disable flaky useFetcher test (#70638)
---
.../useDelayedVisibility/index.test.tsx | 37 ++++++++++---------
1 file changed, 19 insertions(+), 18 deletions(-)
diff --git a/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx
index 9f72ac6d5916e..447e11eab5e41 100644
--- a/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx
@@ -58,28 +58,29 @@ describe.skip('useFetcher', () => {
expect(hook.result.current).toEqual(true);
});
- it('is true for minimum 1000ms', () => {
- hook = renderHook((isLoading) => useDelayedVisibility(isLoading), {
- initialProps: false,
- });
+ // Disabled because it's flaky: https://github.com/elastic/kibana/issues/66389
+ // it('is true for minimum 1000ms', () => {
+ // hook = renderHook((isLoading) => useDelayedVisibility(isLoading), {
+ // initialProps: false,
+ // });
- hook.rerender(true);
+ // hook.rerender(true);
- act(() => {
- jest.advanceTimersByTime(100);
- });
+ // act(() => {
+ // jest.advanceTimersByTime(100);
+ // });
- hook.rerender(false);
- act(() => {
- jest.advanceTimersByTime(900);
- });
+ // hook.rerender(false);
+ // act(() => {
+ // jest.advanceTimersByTime(900);
+ // });
- expect(hook.result.current).toEqual(true);
+ // expect(hook.result.current).toEqual(true);
- act(() => {
- jest.advanceTimersByTime(100);
- });
+ // act(() => {
+ // jest.advanceTimersByTime(100);
+ // });
- expect(hook.result.current).toEqual(false);
- });
+ // expect(hook.result.current).toEqual(false);
+ // });
});
From fd307a06350d8b9ddd8cb1976ed55a85bc6980da Mon Sep 17 00:00:00 2001
From: liza-mae
Date: Thu, 2 Jul 2020 14:37:02 -0600
Subject: [PATCH 35/49] Update network idle timeout (#70629)
Co-authored-by: Elastic Machine
---
test/scripts/jenkins_visual_regression.sh | 2 +-
test/scripts/jenkins_xpack_visual_regression.sh | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_visual_regression.sh
index a32782deec65b..17345d4301882 100755
--- a/test/scripts/jenkins_visual_regression.sh
+++ b/test/scripts/jenkins_visual_regression.sh
@@ -11,7 +11,7 @@ mkdir -p "$installDir"
tar -xzf "$linuxBuild" -C "$installDir" --strip=1
echo " -> running visual regression tests from kibana directory"
-yarn percy exec -t 500 -- -- \
+yarn percy exec -t 10000 -- -- \
node scripts/functional_tests \
--debug --bail \
--kibana-install-dir "$installDir" \
diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh
index b67c1c9060a6e..36bf3409a5421 100755
--- a/test/scripts/jenkins_xpack_visual_regression.sh
+++ b/test/scripts/jenkins_xpack_visual_regression.sh
@@ -13,7 +13,7 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1
echo " -> running visual regression tests from x-pack directory"
cd "$XPACK_DIR"
-yarn percy exec -t 500 -- -- \
+yarn percy exec -t 10000 -- -- \
node scripts/functional_tests \
--debug --bail \
--kibana-install-dir "$installDir" \
From e7749210b41dfda76392834714283f965c80dc1b Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Thu, 2 Jul 2020 14:48:04 -0600
Subject: [PATCH 36/49] [Maps] fix unable to edit heatmap metric (#70606)
* [Maps] fix unable to edit heatmap metric
* add comment
Co-authored-by: Elastic Machine
---
.../sources/es_agg_source/es_agg_source.js | 3 +
.../es_geo_grid_source/es_geo_grid_source.js | 4 +-
.../es_pew_pew_source/es_pew_pew_source.js | 4 +-
.../__snapshots__/metrics_editor.test.js.snap | 86 +++++++++++++++++++
.../maps/public/components/metrics_editor.js | 9 +-
.../public/components/metrics_editor.test.js | 33 +++++++
6 files changed, 132 insertions(+), 7 deletions(-)
create mode 100644 x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap
create mode 100644 x-pack/plugins/maps/public/components/metrics_editor.test.js
diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js
index 97afac9ef1745..e20c509ccd4a2 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js
@@ -10,6 +10,8 @@ import { esAggFieldsFactory } from '../../fields/es_agg_field';
import { AGG_TYPE, COUNT_PROP_LABEL, FIELD_ORIGIN } from '../../../../common/constants';
import { getSourceAggKey } from '../../../../common/get_agg_key';
+export const DEFAULT_METRIC = { type: AGG_TYPE.COUNT };
+
export class AbstractESAggSource extends AbstractESSource {
constructor(descriptor, inspectorAdapters) {
super(descriptor, inspectorAdapters);
@@ -48,6 +50,7 @@ export class AbstractESAggSource extends AbstractESSource {
getMetricFields() {
const metrics = this._metricFields.filter((esAggField) => esAggField.isValid());
+ // Handle case where metrics is empty because older saved object state is empty array or there are no valid aggs.
return metrics.length === 0
? esAggFieldsFactory({ type: AGG_TYPE.COUNT }, this, this.getOriginForField())
: metrics;
diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js
index b613f577067ba..9431fb55dc88b 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js
@@ -18,7 +18,7 @@ import {
} from '../../../../common/constants';
import { i18n } from '@kbn/i18n';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
-import { AbstractESAggSource } from '../es_agg_source';
+import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source';
import { DataRequestAbortError } from '../../util/data_request';
import { registerSource } from '../source_registry';
import { makeESBbox } from '../../../elasticsearch_geo_utils';
@@ -42,7 +42,7 @@ export class ESGeoGridSource extends AbstractESAggSource {
id: uuid(),
indexPatternId,
geoField,
- metrics: metrics ? metrics : [],
+ metrics: metrics ? metrics : [DEFAULT_METRIC],
requestType,
resolution: resolution ? resolution : GRID_RESOLUTION.COARSE,
};
diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
index 076e7a758a4fb..a4cff7c89a011 100644
--- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
+++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js
@@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
import { SOURCE_TYPES, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
import { getDataSourceLabel } from '../../../../common/i18n_getters';
import { convertToLines } from './convert_to_lines';
-import { AbstractESAggSource } from '../es_agg_source';
+import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source';
import { indexPatterns } from '../../../../../../../src/plugins/data/public';
import { registerSource } from '../source_registry';
@@ -32,7 +32,7 @@ export class ESPewPewSource extends AbstractESAggSource {
indexPatternId: indexPatternId,
sourceGeoField,
destGeoField,
- metrics: metrics ? metrics : [],
+ metrics: metrics ? metrics : [DEFAULT_METRIC],
};
}
diff --git a/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap
new file mode 100644
index 0000000000000..0d4f1f99e464c
--- /dev/null
+++ b/x-pack/plugins/maps/public/components/__snapshots__/metrics_editor.test.js.snap
@@ -0,0 +1,86 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should add default count metric when metrics is empty array 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`should render metrics editor 1`] = `
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/components/metrics_editor.js b/x-pack/plugins/maps/public/components/metrics_editor.js
index 6c5a9af8f0f02..7d4d7bf3ec7ab 100644
--- a/x-pack/plugins/maps/public/components/metrics_editor.js
+++ b/x-pack/plugins/maps/public/components/metrics_editor.js
@@ -10,11 +10,14 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonEmpty, EuiSpacer, EuiTextAlign } from '@elastic/eui';
import { MetricEditor } from './metric_editor';
-import { AGG_TYPE } from '../../common/constants';
+import { DEFAULT_METRIC } from '../classes/sources/es_agg_source';
export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, metricsFilter }) {
function renderMetrics() {
- return metrics.map((metric, index) => {
+ // There was a bug in 7.8 that initialized metrics to [].
+ // This check is needed to handle any saved objects created before the bug was patched.
+ const nonEmptyMetrics = metrics.length === 0 ? [DEFAULT_METRIC] : metrics;
+ return nonEmptyMetrics.map((metric, index) => {
const onMetricChange = (metric) => {
onChange([...metrics.slice(0, index), metric, ...metrics.slice(index + 1)]);
};
@@ -100,6 +103,6 @@ MetricsEditor.propTypes = {
};
MetricsEditor.defaultProps = {
- metrics: [{ type: AGG_TYPE.COUNT }],
+ metrics: [DEFAULT_METRIC],
allowMultipleMetrics: true,
};
diff --git a/x-pack/plugins/maps/public/components/metrics_editor.test.js b/x-pack/plugins/maps/public/components/metrics_editor.test.js
new file mode 100644
index 0000000000000..bcbeef29875ee
--- /dev/null
+++ b/x-pack/plugins/maps/public/components/metrics_editor.test.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { MetricsEditor } from './metrics_editor';
+import { AGG_TYPE } from '../../common/constants';
+
+const defaultProps = {
+ metrics: [
+ {
+ type: AGG_TYPE.SUM,
+ field: 'myField',
+ },
+ ],
+ fields: [],
+ onChange: () => {},
+ allowMultipleMetrics: true,
+ metricsFilter: () => {},
+};
+
+test('should render metrics editor', async () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+});
+
+test('should add default count metric when metrics is empty array', async () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+});
From 6a33a78f31ca4ef3a39cf573f4f74a739b7cbdfb Mon Sep 17 00:00:00 2001
From: Yara Tercero
Date: Thu, 2 Jul 2020 17:04:48 -0400
Subject: [PATCH 37/49] fix 400 error on initial signals search (#70618)
### Summary
On initial render of the SIEM pages, a 400 error was showing for POST http://localhost:5601/api/detection_engine/signals/search. This PR is a temporary fix for this bug. This initial call is being used to populate the Last alert text that shows at the top of a number of the pages. The reason the size was 0 is because we weren't interested in the signals themselves, just the timestamp of the last alert. Teamed up with @XavierM and it seems to us that the issue is the server side validation. It may be Hapi misreading the 0 as false or our updated validation not accepting size 0.
---
.../public/alerts/components/alerts_info/query.dsl.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts b/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts
index a3972fd35bf2d..4b57c7dc20d9f 100644
--- a/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts
+++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_info/query.dsl.ts
@@ -10,6 +10,7 @@ export const buildLastAlertsQuery = (ruleId: string | undefined | null) => {
bool: { should: [{ match: { 'signal.status': 'open' } }], minimum_should_match: 1 },
},
];
+
return {
aggs: {
lastSeen: { max: { field: '@timestamp' } },
@@ -30,7 +31,7 @@ export const buildLastAlertsQuery = (ruleId: string | undefined | null) => {
: queryFilter,
},
},
- size: 0,
+ size: 1,
track_total_hits: true,
};
};
From 81d26fc9f8eacf636fcf3999566bdc4f26e75f0f Mon Sep 17 00:00:00 2001
From: Andrew Goldstein
Date: Thu, 2 Jul 2020 15:37:42 -0600
Subject: [PATCH 38/49] [Security Solution] Renames the `Investigate in
Resolver` Timeline action (#70634)
## [Security Solution] Renames the `Investigate in Resolver` Timeline action
- Renames the `Investigate in Resolver` Timeline action, a follow-up item from the `What's next?` section of https://github.com/elastic/kibana/pull/70111
- Fixes a CSS issue where the icon didn't align with the others on non-default row-heights
## Before
![before-investigate-in-resolver](https://user-images.githubusercontent.com/4459398/86393038-a97eeb80-bc59-11ea-9ba4-449ab20ddd25.png)
## After
![after-analyze-event](https://user-images.githubusercontent.com/4459398/86393050-ad127280-bc59-11ea-8040-7f254b0255b0.png)
Desk tested in:
- Chrome `83.0.4103.116`
- Firefox `78.0.1`
- Safari `13.1.1`
---
.../public/timelines/components/timeline/body/translations.ts | 2 +-
.../public/timelines/components/timeline/styles.tsx | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
index ef7ee26cd3ecf..5af2f3ef488b0 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts
@@ -69,6 +69,6 @@ export const COLLAPSE = i18n.translate(
export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate(
'xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip',
{
- defaultMessage: 'Investigate in Resolver',
+ defaultMessage: 'Analyze event',
}
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
index b27f213c6a02c..d1a58dafcb328 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx
@@ -204,6 +204,7 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({
export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({
className: `siemEventsTable__tdGroupActions ${className}`,
}))<{ actionsColumnWidth: number }>`
+ align-items: center;
display: flex;
flex: 0 0 ${({ actionsColumnWidth }) => `${actionsColumnWidth}px`};
min-width: 0;
From 113962e12a507ae4c446abd3560df0324a87f1d2 Mon Sep 17 00:00:00 2001
From: CJ Cenizal
Date: Thu, 2 Jul 2020 14:47:08 -0700
Subject: [PATCH 39/49] Update component templates list to render empty prompt
inside of content container. Show detail panel when deep-linked, even if
there are no component templates. (#70633)
Co-authored-by: Elastic Machine
---
.../component_template_list/component_template_list.tsx | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
index 05a5ed462d8f7..f9e6234e1415c 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx
@@ -43,10 +43,6 @@ export const ComponentTemplateList: React.FunctionComponent = ({
trackMetric('loaded', UIM_COMPONENT_TEMPLATE_LIST_LOAD);
}, [trackMetric]);
- if (data && data.length === 0) {
- return ;
- }
-
let content: React.ReactNode;
if (isLoading) {
@@ -67,6 +63,8 @@ export const ComponentTemplateList: React.FunctionComponent = ({
history={history as ScopedHistory}
/>
);
+ } else if (data && data.length === 0) {
+ content = ;
} else if (error) {
content = ;
}
From 5fcf803d3d3fbf6ec42310e11c179496a6bf875c Mon Sep 17 00:00:00 2001
From: patrykkopycinski
Date: Thu, 2 Jul 2020 23:48:41 +0200
Subject: [PATCH 40/49] Fix saved query modal overlay (#68826)
---
.../saved_query_management_component.tsx | 87 ++++++++++++-------
.../components/query_bar/index.test.tsx | 59 +++++++++++++
2 files changed, 115 insertions(+), 31 deletions(-)
diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx
index 87f53e1c72079..8582f4a12fa38 100644
--- a/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx
+++ b/src/plugins/data/public/ui/saved_query_management/saved_query_management_component.tsx
@@ -33,7 +33,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import React, { useEffect, useState, Fragment, useRef } from 'react';
+import React, { useCallback, useEffect, useState, Fragment, useRef } from 'react';
import { sortBy } from 'lodash';
import { SavedQuery, SavedQueryService } from '../..';
import { SavedQueryListItem } from './saved_query_list_item';
@@ -88,9 +88,51 @@ export function SavedQueryManagementComponent({
}
}, [isOpen, activePage, savedQueryService]);
- const goToPage = (pageNumber: number) => {
- setActivePage(pageNumber);
- };
+ const handleTogglePopover = useCallback(() => setIsOpen((currentState) => !currentState), [
+ setIsOpen,
+ ]);
+
+ const handleClosePopover = useCallback(() => setIsOpen(false), []);
+
+ const handleSave = useCallback(() => {
+ handleClosePopover();
+ onSave();
+ }, [handleClosePopover, onSave]);
+
+ const handleSaveAsNew = useCallback(() => {
+ handleClosePopover();
+ onSaveAsNew();
+ }, [handleClosePopover, onSaveAsNew]);
+
+ const handleSelect = useCallback(
+ (savedQueryToSelect) => {
+ handleClosePopover();
+ onLoad(savedQueryToSelect);
+ },
+ [handleClosePopover, onLoad]
+ );
+
+ const handleDelete = useCallback(
+ (savedQueryToDelete: SavedQuery) => {
+ const onDeleteSavedQuery = async (savedQuery: SavedQuery) => {
+ cancelPendingListingRequest.current();
+ setSavedQueries(
+ savedQueries.filter((currentSavedQuery) => currentSavedQuery.id !== savedQuery.id)
+ );
+
+ if (loadedSavedQuery && loadedSavedQuery.id === savedQuery.id) {
+ onClearSavedQuery();
+ }
+
+ await savedQueryService.deleteSavedQuery(savedQuery.id);
+ setActivePage(0);
+ };
+
+ onDeleteSavedQuery(savedQueryToDelete);
+ handleClosePopover();
+ },
+ [handleClosePopover, loadedSavedQuery, onClearSavedQuery, savedQueries, savedQueryService]
+ );
const savedQueryDescriptionText = i18n.translate(
'data.search.searchBar.savedQueryDescriptionText',
@@ -113,25 +155,13 @@ export function SavedQueryManagementComponent({
}
);
- const onDeleteSavedQuery = async (savedQuery: SavedQuery) => {
- cancelPendingListingRequest.current();
- setSavedQueries(
- savedQueries.filter((currentSavedQuery) => currentSavedQuery.id !== savedQuery.id)
- );
-
- if (loadedSavedQuery && loadedSavedQuery.id === savedQuery.id) {
- onClearSavedQuery();
- }
-
- await savedQueryService.deleteSavedQuery(savedQuery.id);
- setActivePage(0);
+ const goToPage = (pageNumber: number) => {
+ setActivePage(pageNumber);
};
const savedQueryPopoverButton = (
{
- setIsOpen(!isOpen);
- }}
+ onClick={handleTogglePopover}
aria-label={i18n.translate('data.search.searchBar.savedQueryPopoverButtonText', {
defaultMessage: 'See saved queries',
})}
@@ -159,11 +189,8 @@ export function SavedQueryManagementComponent({
key={savedQuery.id}
savedQuery={savedQuery}
isSelected={!!loadedSavedQuery && loadedSavedQuery.id === savedQuery.id}
- onSelect={(savedQueryToSelect) => {
- onLoad(savedQueryToSelect);
- setIsOpen(false);
- }}
- onDelete={(savedQueryToDelete) => onDeleteSavedQuery(savedQueryToDelete)}
+ onSelect={handleSelect}
+ onDelete={handleDelete}
showWriteOperations={!!showSaveQuery}
/>
));
@@ -175,9 +202,7 @@ export function SavedQueryManagementComponent({
id="savedQueryPopover"
button={savedQueryPopoverButton}
isOpen={isOpen}
- closePopover={() => {
- setIsOpen(false);
- }}
+ closePopover={handleClosePopover}
anchorPosition="downLeft"
panelPaddingSize="none"
buffer={-8}
@@ -235,7 +260,7 @@ export function SavedQueryManagementComponent({
onSave()}
+ onClick={handleSave}
aria-label={i18n.translate(
'data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel',
{
@@ -256,7 +281,7 @@ export function SavedQueryManagementComponent({
onSaveAsNew()}
+ onClick={handleSaveAsNew}
aria-label={i18n.translate(
'data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel',
{
@@ -280,7 +305,7 @@ export function SavedQueryManagementComponent({
onSave()}
+ onClick={handleSave}
aria-label={i18n.translate(
'data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel',
{ defaultMessage: 'Save a new saved query' }
@@ -299,7 +324,7 @@ export function SavedQueryManagementComponent({
onClearSavedQuery()}
+ onClick={onClearSavedQuery}
aria-label={i18n.translate(
'data.search.searchBar.savedQueryPopoverClearButtonAriaLabel',
{ defaultMessage: 'Clear current saved query' }
diff --git a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx
index f079715baec1c..a3cab1cfabd71 100644
--- a/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/query_bar/index.test.tsx
@@ -376,4 +376,63 @@ describe('QueryBar ', () => {
expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit);
});
});
+
+ describe('SavedQueryManagementComponent state', () => {
+ test('popover should hidden when "Save current query" button was clicked', () => {
+ const KibanaWithStorageProvider = createKibanaContextProviderMock();
+
+ const Proxy = (props: QueryBarComponentProps) => (
+
+
+
+
+
+ );
+
+ const wrapper = mount(
+
+ );
+
+ const isSavedQueryPopoverOpen = () =>
+ wrapper.find('EuiPopover[id="savedQueryPopover"]').prop('isOpen');
+
+ expect(isSavedQueryPopoverOpen()).toBeFalsy();
+
+ wrapper
+ .find('button[data-test-subj="saved-query-management-popover-button"]')
+ .simulate('click');
+
+ expect(isSavedQueryPopoverOpen()).toBeTruthy();
+
+ wrapper.find('button[data-test-subj="saved-query-management-save-button"]').simulate('click');
+
+ expect(isSavedQueryPopoverOpen()).toBeFalsy();
+ });
+ });
});
From f5b280007f88be98255d75bcfc01ff032b999dbf Mon Sep 17 00:00:00 2001
From: Spencer
Date: Thu, 2 Jul 2020 15:06:32 -0700
Subject: [PATCH 41/49] [kbn/optimizer] only build specified themes (#70389)
Co-authored-by: spalger
Co-authored-by: Elastic Machine
Co-authored-by: cchaos
---
CONTRIBUTING.md | 8 +-
packages/kbn-optimizer/README.md | 20 ++++
...ng_constants.scss => _globals_v7dark.scss} | 0
.../ui/public/styles/_globals_v7light.scss | 1 +
.../ui/public/styles/_globals_v8dark.scss | 1 +
.../ui/public/styles/_globals_v8light.scss | 1 +
packages/kbn-optimizer/src/common/index.ts | 1 +
.../src/common/theme_tags.test.ts | 92 +++++++++++++++++++
.../kbn-optimizer/src/common/theme_tags.ts | 65 +++++++++++++
.../kbn-optimizer/src/common/worker_config.ts | 5 +
.../basic_optimization.test.ts.snap | 6 +-
.../basic_optimization.test.ts | 4 +-
.../kbn-optimizer/src/log_optimizer_state.ts | 14 +--
.../src/optimizer/cache_keys.test.ts | 4 +
.../src/optimizer/observe_stdio.test.ts | 49 ++++++++++
.../src/optimizer/observe_stdio.ts | 76 +++++++++++++++
.../src/optimizer/observe_worker.ts | 46 ++++------
.../src/optimizer/optimizer_config.test.ts | 39 +++++++-
.../src/optimizer/optimizer_config.ts | 34 ++++++-
.../src/optimizer/optimizer_state.ts | 2 +-
.../kbn-optimizer/src/worker/run_compilers.ts | 2 +-
.../kbn-optimizer/src/worker/theme_loader.ts | 41 +++++++--
.../src/worker/webpack.config.ts | 36 +++-----
.../storybook_config/webpack.config.js | 2 +-
packages/kbn-ui-shared-deps/entry.js | 9 --
packages/kbn-ui-shared-deps/theme.ts | 10 +-
src/core/public/index.scss | 4 -
.../core_plugins/kibana/public/index.scss | 2 -
.../tests_bundle/public/index.scss | 2 -
.../core_plugins/timelion/public/index.scss | 3 -
.../server/sass/__fixtures__/index.scss | 2 -
src/legacy/server/sass/build.js | 18 ++--
.../ui/public/styles/_globals_v7dark.scss | 12 +++
...g_constants.scss => _globals_v7light.scss} | 7 +-
.../ui/public/styles/_globals_v8dark.scss | 16 ++++
.../ui/public/styles/_globals_v8light.scss | 16 ++++
.../ui/ui_render/bootstrap/template.js.hbs | 3 +-
src/legacy/ui/ui_render/ui_render_mixin.js | 6 +-
src/plugins/tile_map/public/index.scss | 2 -
.../canvas/.storybook/webpack.config.js | 3 +-
x-pack/plugins/canvas/public/style/index.scss | 2 -
.../shareable_runtime/webpack.config.js | 2 +-
.../index_management/public/index.scss | 3 -
x-pack/plugins/infra/public/index.scss | 3 -
x-pack/plugins/maps/public/index.scss | 3 -
.../plugins/ml/public/application/_index.scss | 3 -
.../public/application/index.scss | 3 -
.../plugins/transform/public/app/index.scss | 3 -
.../public/application/_index.scss | 2 -
49 files changed, 545 insertions(+), 143 deletions(-)
rename packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/{_styling_constants.scss => _globals_v7dark.scss} (100%)
create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss
create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8dark.scss
create mode 100644 packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8light.scss
create mode 100644 packages/kbn-optimizer/src/common/theme_tags.test.ts
create mode 100644 packages/kbn-optimizer/src/common/theme_tags.ts
create mode 100644 packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts
create mode 100644 packages/kbn-optimizer/src/optimizer/observe_stdio.ts
create mode 100644 src/legacy/ui/public/styles/_globals_v7dark.scss
rename src/legacy/ui/public/styles/{_styling_constants.scss => _globals_v7light.scss} (59%)
create mode 100644 src/legacy/ui/public/styles/_globals_v8dark.scss
create mode 100644 src/legacy/ui/public/styles/_globals_v8light.scss
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a7345f4b2897b..a0aeed7a34949 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -436,7 +436,7 @@ We are still to develop a proper process to accept any contributed translations.
When writing a new component, create a sibling SASS file of the same name and import directly into the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint).
-Any JavaScript (or TypeScript) file that imports SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`styling_constants.scss` file](https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/styles/_styling_constants.scss). However, any Legacy (file path includes `/legacy`) files will not.
+All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/legacy/ui/public/styles/_globals_v7light.scss).
**Example:**
@@ -679,15 +679,15 @@ Part of this process only applies to maintainers, since it requires access to Gi
Kibana publishes [Release Notes](https://www.elastic.co/guide/en/kibana/current/release-notes.html) for major and minor releases. The Release Notes summarize what the PRs accomplish in language that is meaningful to users. To generate the Release Notes, the team runs a script against this repo to collect the merged PRs against the release.
#### Create the Release Notes text
-The text that appears in the Release Notes is pulled directly from your PR title, or a single paragraph of text that you specify in the PR description.
+The text that appears in the Release Notes is pulled directly from your PR title, or a single paragraph of text that you specify in the PR description.
To use a single paragraph of text, enter `Release note:` or a `## Release note` header in the PR description, followed by your text. For example, refer to this [PR](https://github.com/elastic/kibana/pull/65796) that uses the `## Release note` header.
When you create the Release Notes text, use the following best practices:
-* Use present tense.
+* Use present tense.
* Use sentence case.
* When you create a feature PR, start with `Adds`.
-* When you create an enhancement PR, start with `Improves`.
+* When you create an enhancement PR, start with `Improves`.
* When you create a bug fix PR, start with `Fixes`.
* When you create a deprecation PR, start with `Deprecates`.
diff --git a/packages/kbn-optimizer/README.md b/packages/kbn-optimizer/README.md
index 9ff0f56344274..5d5c5e3b6eb74 100644
--- a/packages/kbn-optimizer/README.md
+++ b/packages/kbn-optimizer/README.md
@@ -42,6 +42,26 @@ When a directory is listed in the "extraPublicDirs" it will always be included i
Any import in a bundle which resolves into another bundles "context" directory, ie `src/plugins/*`, must map explicitly to a "public dir" exported by that plugin. If the resolved import is not in the list of public dirs an error will be thrown and the optimizer will fail to build that bundle until the error is fixed.
+## Themes
+
+SASS imports in bundles are automatically converted to CSS for one or more themes. In development we build the `v7light` and `v7dark` themes by default to improve build performance. When producing distributable bundles the default shifts to `*` so that the distributable bundles will include all themes, preventing the bundles from needing to be rebuilt when users change the active theme in Kibana's advanced settings.
+
+To customize the themes that are built for development you can specify the `KBN_OPTIMIZER_THEMES` environment variable to one or more theme tags, or use `*` to build styles for all themes. Unfortunately building more than one theme significantly impacts build performance, so try to be strategic about which themes you build.
+
+Currently supported theme tags: `v7light`, `v7dark`, `v8light`, `v8dark`
+
+Examples:
+```sh
+# start Kibana with only a single theme
+KBN_OPTIMIZER_THEMES=v7light yarn start
+
+# start Kibana with dark themes for version 7 and 8
+KBN_OPTIMIZER_THEMES=v7dark,v8dark yarn start
+
+# start Kibana with all the themes
+KBN_OPTIMIZER_THEMES=* yarn start
+```
+
## API
To run the optimizer from code, you can import the [`OptimizerConfig`][OptimizerConfig] class and [`runOptimizer`][Optimizer] function. Create an [`OptimizerConfig`][OptimizerConfig] instance by calling it's static `create()` method with some options, then pass it to the [`runOptimizer`][Optimizer] function. `runOptimizer()` returns an observable of update objects, which are summaries of the optimizer state plus an optional `event` property which describes the internal events occuring and may be of use. You can use the [`logOptimizerState()`][LogOptimizerState] helper to write the relevant bits of state to a tooling log or checkout it's implementation to see how the internal events like [`WorkerStdio`][ObserveWorker] and [`WorkerStarted`][ObserveWorker] are used.
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss
similarity index 100%
rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_styling_constants.scss
rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss
new file mode 100644
index 0000000000000..63beb9927b9f5
--- /dev/null
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss
@@ -0,0 +1 @@
+$globalStyleConstant: 11;
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8dark.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8dark.scss
new file mode 100644
index 0000000000000..4040cab1878fc
--- /dev/null
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8dark.scss
@@ -0,0 +1 @@
+$globalStyleConstant: 12;
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8light.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8light.scss
new file mode 100644
index 0000000000000..3918413c06863
--- /dev/null
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/src/legacy/ui/public/styles/_globals_v8light.scss
@@ -0,0 +1 @@
+$globalStyleConstant: 13;
diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts
index 7d021a5ee7847..89cde2c1cd064 100644
--- a/packages/kbn-optimizer/src/common/index.ts
+++ b/packages/kbn-optimizer/src/common/index.ts
@@ -29,3 +29,4 @@ export * from './array_helpers';
export * from './event_stream_helpers';
export * from './disallowed_syntax_plugin';
export * from './parse_path';
+export * from './theme_tags';
diff --git a/packages/kbn-optimizer/src/common/theme_tags.test.ts b/packages/kbn-optimizer/src/common/theme_tags.test.ts
new file mode 100644
index 0000000000000..019a9b7bdee3e
--- /dev/null
+++ b/packages/kbn-optimizer/src/common/theme_tags.test.ts
@@ -0,0 +1,92 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { parseThemeTags } from './theme_tags';
+
+it('returns default tags when passed undefined', () => {
+ expect(parseThemeTags()).toMatchInlineSnapshot(`
+ Array [
+ "v7dark",
+ "v7light",
+ ]
+ `);
+});
+
+it('returns all tags when passed *', () => {
+ expect(parseThemeTags('*')).toMatchInlineSnapshot(`
+ Array [
+ "v7dark",
+ "v7light",
+ "v8dark",
+ "v8light",
+ ]
+ `);
+});
+
+it('returns specific tag when passed a single value', () => {
+ expect(parseThemeTags('v8light')).toMatchInlineSnapshot(`
+ Array [
+ "v8light",
+ ]
+ `);
+});
+
+it('returns specific tags when passed a comma separated list', () => {
+ expect(parseThemeTags('v8light, v7dark,v7light')).toMatchInlineSnapshot(`
+ Array [
+ "v7dark",
+ "v7light",
+ "v8light",
+ ]
+ `);
+});
+
+it('returns specific tags when passed an array', () => {
+ expect(parseThemeTags(['v8light', 'v7light'])).toMatchInlineSnapshot(`
+ Array [
+ "v7light",
+ "v8light",
+ ]
+ `);
+});
+
+it('throws when an invalid tag is in the array', () => {
+ expect(() => parseThemeTags(['v8light', 'v7light', 'bar'])).toThrowErrorMatchingInlineSnapshot(
+ `"Invalid theme tags [bar], options: [v7dark, v7light, v8dark, v8light]"`
+ );
+});
+
+it('throws when an invalid tags in comma separated list', () => {
+ expect(() => parseThemeTags('v8light ,v7light,bar,box ')).toThrowErrorMatchingInlineSnapshot(
+ `"Invalid theme tags [bar, box], options: [v7dark, v7light, v8dark, v8light]"`
+ );
+});
+
+it('returns tags in alphabetical order', () => {
+ const tags = parseThemeTags(['v7light', 'v8light']);
+ expect(tags).toEqual(tags.slice().sort((a, b) => a.localeCompare(b)));
+});
+
+it('returns an immutable array', () => {
+ expect(() => {
+ const tags = parseThemeTags('v8light');
+ // @ts-expect-error
+ tags.push('foo');
+ }).toThrowErrorMatchingInlineSnapshot(`"Cannot add property 1, object is not extensible"`);
+});
diff --git a/packages/kbn-optimizer/src/common/theme_tags.ts b/packages/kbn-optimizer/src/common/theme_tags.ts
new file mode 100644
index 0000000000000..27b5e12b807a8
--- /dev/null
+++ b/packages/kbn-optimizer/src/common/theme_tags.ts
@@ -0,0 +1,65 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ascending } from './array_helpers';
+
+const tags = (...themeTags: string[]) =>
+ Object.freeze(themeTags.sort(ascending((tag) => tag)) as ThemeTag[]);
+
+const validTag = (tag: any): tag is ThemeTag => ALL_THEMES.includes(tag);
+const isArrayOfStrings = (input: unknown): input is string[] =>
+ Array.isArray(input) && input.every((v) => typeof v === 'string');
+
+export type ThemeTags = readonly ThemeTag[];
+export type ThemeTag = 'v7light' | 'v7dark' | 'v8light' | 'v8dark';
+export const DEFAULT_THEMES = tags('v7light', 'v7dark');
+export const ALL_THEMES = tags('v7light', 'v7dark', 'v8light', 'v8dark');
+
+export function parseThemeTags(input?: any): ThemeTags {
+ if (!input) {
+ return DEFAULT_THEMES;
+ }
+
+ if (input === '*') {
+ return ALL_THEMES;
+ }
+
+ if (typeof input === 'string') {
+ input = input.split(',').map((tag) => tag.trim());
+ }
+
+ if (!isArrayOfStrings(input)) {
+ throw new Error(`Invalid theme tags, must be an array of strings`);
+ }
+
+ if (!input.length) {
+ throw new Error(
+ `Invalid theme tags, you must specify at least one of [${ALL_THEMES.join(', ')}]`
+ );
+ }
+
+ const invalidTags = input.filter((t) => !validTag(t));
+ if (invalidTags.length) {
+ throw new Error(
+ `Invalid theme tags [${invalidTags.join(', ')}], options: [${ALL_THEMES.join(', ')}]`
+ );
+ }
+
+ return tags(...input);
+}
diff --git a/packages/kbn-optimizer/src/common/worker_config.ts b/packages/kbn-optimizer/src/common/worker_config.ts
index a1ab51ee97c23..8726b3452ff1e 100644
--- a/packages/kbn-optimizer/src/common/worker_config.ts
+++ b/packages/kbn-optimizer/src/common/worker_config.ts
@@ -20,11 +20,13 @@
import Path from 'path';
import { UnknownVals } from './ts_helpers';
+import { ThemeTags, parseThemeTags } from './theme_tags';
export interface WorkerConfig {
readonly repoRoot: string;
readonly watch: boolean;
readonly dist: boolean;
+ readonly themeTags: ThemeTags;
readonly cache: boolean;
readonly profileWebpack: boolean;
readonly browserslistEnv: string;
@@ -80,6 +82,8 @@ export function parseWorkerConfig(json: string): WorkerConfig {
throw new Error('`browserslistEnv` must be a string');
}
+ const themes = parseThemeTags(parsed.themeTags);
+
return {
repoRoot,
cache,
@@ -88,6 +92,7 @@ export function parseWorkerConfig(json: string): WorkerConfig {
profileWebpack,
optimizerCacheKey,
browserslistEnv,
+ themeTags: themes,
};
} catch (error) {
throw new Error(`unable to parse worker config: ${error.message}`);
diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
index b6b0973f0d539..211cfac3806ad 100644
--- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
+++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
@@ -58,11 +58,15 @@ OptimizerConfig {
],
"profileWebpack": false,
"repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo,
+ "themeTags": Array [
+ "v7dark",
+ "v7light",
+ ],
"watch": false,
}
`;
-exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i {
tap((state) => {
if (state.event?.type === 'worker stdio') {
// eslint-disable-next-line no-console
- console.log('worker', state.event.stream, state.event.chunk.toString('utf8'));
+ console.log('worker', state.event.stream, state.event.line);
}
}),
toArray()
@@ -226,7 +226,7 @@ const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabe
// Verify the brotli variant matches
expect(
- // @ts-ignore @types/node is missing the brotli functions
+ // @ts-expect-error @types/node is missing the brotli functions
Zlib.brotliDecompressSync(
Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, `${filePath}.br`))
).toString()
diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts
index cbec159bd27a0..23767be610da4 100644
--- a/packages/kbn-optimizer/src/log_optimizer_state.ts
+++ b/packages/kbn-optimizer/src/log_optimizer_state.ts
@@ -24,7 +24,7 @@ import { tap } from 'rxjs/operators';
import { OptimizerConfig } from './optimizer';
import { OptimizerUpdate$ } from './run_optimizer';
-import { CompilerMsg, pipeClosure } from './common';
+import { CompilerMsg, pipeClosure, ALL_THEMES } from './common';
export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) {
return pipeClosure((update$: OptimizerUpdate$) => {
@@ -37,12 +37,7 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) {
const { event, state } = update;
if (event?.type === 'worker stdio') {
- const chunk = event.chunk.toString('utf8');
- log.warning(
- `worker`,
- event.stream,
- chunk.slice(0, chunk.length - (chunk.endsWith('\n') ? 1 : 0))
- );
+ log.warning(`worker`, event.stream, event.line);
}
if (event?.type === 'bundle not cached') {
@@ -76,6 +71,11 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) {
if (!loggedInit) {
loggedInit = true;
log.info(`initialized, ${state.offlineBundles.length} bundles cached`);
+ if (config.themeTags.length !== ALL_THEMES.length) {
+ log.warning(
+ `only building [${config.themeTags}] themes, customize with the KBN_OPTIMIZER_THEMES environment variable`
+ );
+ }
}
return;
}
diff --git a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts
index 9d7f1709506f9..47d01347a8f7d 100644
--- a/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/cache_keys.test.ts
@@ -103,6 +103,10 @@ describe('getOptimizerCacheKey()', () => {
"dist": false,
"optimizerCacheKey": "♻",
"repoRoot": ,
+ "themeTags": Array [
+ "v7dark",
+ "v7light",
+ ],
},
}
`);
diff --git a/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts
new file mode 100644
index 0000000000000..9bf8f9db1fe45
--- /dev/null
+++ b/packages/kbn-optimizer/src/optimizer/observe_stdio.test.ts
@@ -0,0 +1,49 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Readable } from 'stream';
+
+import { toArray } from 'rxjs/operators';
+
+import { observeStdio$ } from './observe_stdio';
+
+it('notifies on every line, uncluding partial content at the end without a newline', async () => {
+ const chunks = [`foo\nba`, `r\nb`, `az`];
+
+ await expect(
+ observeStdio$(
+ new Readable({
+ read() {
+ this.push(chunks.shift()!);
+ if (!chunks.length) {
+ this.push(null);
+ }
+ },
+ })
+ )
+ .pipe(toArray())
+ .toPromise()
+ ).resolves.toMatchInlineSnapshot(`
+ Array [
+ "foo",
+ "bar",
+ "baz",
+ ]
+ `);
+});
diff --git a/packages/kbn-optimizer/src/optimizer/observe_stdio.ts b/packages/kbn-optimizer/src/optimizer/observe_stdio.ts
new file mode 100644
index 0000000000000..e8daecef8e0dd
--- /dev/null
+++ b/packages/kbn-optimizer/src/optimizer/observe_stdio.ts
@@ -0,0 +1,76 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Readable } from 'stream';
+import * as Rx from 'rxjs';
+
+// match newline characters followed either by a non-space character or another newline
+const NEWLINE = /\r?\n/;
+
+/**
+ * Observe a readable stdio stream and emit the entire lines
+ * of text produced, completing once the stdio stream emits "end"
+ * and erroring if it emits "error".
+ */
+export function observeStdio$(stream: Readable) {
+ return new Rx.Observable((subscriber) => {
+ let buffer = '';
+
+ subscriber.add(
+ Rx.fromEvent(stream, 'data').subscribe({
+ next(chunk) {
+ buffer += chunk.toString('utf8');
+
+ while (true) {
+ const match = NEWLINE.exec(buffer);
+ if (!match) {
+ break;
+ }
+
+ const multilineChunk = buffer.slice(0, match.index);
+ buffer = buffer.slice(match.index + match[0].length);
+ subscriber.next(multilineChunk);
+ }
+ },
+ })
+ );
+
+ const flush = () => {
+ while (buffer.length && !subscriber.closed) {
+ const line = buffer;
+ buffer = '';
+ subscriber.next(line);
+ }
+ };
+
+ subscriber.add(
+ Rx.fromEvent(stream, 'end').subscribe(() => {
+ flush();
+ subscriber.complete();
+ })
+ );
+
+ subscriber.add(
+ Rx.fromEvent(stream, 'error').subscribe((error) => {
+ flush();
+ subscriber.error(error);
+ })
+ );
+ });
+}
diff --git a/packages/kbn-optimizer/src/optimizer/observe_worker.ts b/packages/kbn-optimizer/src/optimizer/observe_worker.ts
index fef3efc13a516..31b34bd5c5938 100644
--- a/packages/kbn-optimizer/src/optimizer/observe_worker.ts
+++ b/packages/kbn-optimizer/src/optimizer/observe_worker.ts
@@ -17,7 +17,6 @@
* under the License.
*/
-import { Readable } from 'stream';
import { inspect } from 'util';
import execa from 'execa';
@@ -26,12 +25,13 @@ import { map, takeUntil, first, ignoreElements } from 'rxjs/operators';
import { isWorkerMsg, WorkerConfig, WorkerMsg, Bundle, BundleRefs } from '../common';
+import { observeStdio$ } from './observe_stdio';
import { OptimizerConfig } from './optimizer_config';
export interface WorkerStdio {
type: 'worker stdio';
stream: 'stdout' | 'stderr';
- chunk: Buffer;
+ line: string;
}
export interface WorkerStarted {
@@ -99,28 +99,6 @@ function usingWorkerProc(
);
}
-function observeStdio$(stream: Readable, name: WorkerStdio['stream']) {
- return Rx.fromEvent(stream, 'data').pipe(
- takeUntil(
- Rx.race(
- Rx.fromEvent(stream, 'end'),
- Rx.fromEvent(stream, 'error').pipe(
- map((error) => {
- throw error;
- })
- )
- )
- ),
- map(
- (chunk): WorkerStdio => ({
- type: 'worker stdio',
- chunk,
- stream: name,
- })
- )
- );
-}
-
/**
* We used to pass configuration to the worker as JSON encoded arguments, but they
* grew too large for argv, especially on Windows, so we had to move to an async init
@@ -186,8 +164,24 @@ export function observeWorker(
type: 'worker started',
bundles,
}),
- observeStdio$(proc.stdout, 'stdout'),
- observeStdio$(proc.stderr, 'stderr'),
+ observeStdio$(proc.stdout).pipe(
+ map(
+ (line): WorkerStdio => ({
+ type: 'worker stdio',
+ line,
+ stream: 'stdout',
+ })
+ )
+ ),
+ observeStdio$(proc.stderr).pipe(
+ map(
+ (line): WorkerStdio => ({
+ type: 'worker stdio',
+ line,
+ stream: 'stderr',
+ })
+ )
+ ),
Rx.fromEvent<[unknown]>(proc, 'message')
.pipe(
// validate the messages from the process
diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
index d4152133f289d..5b46d67479fd5 100644
--- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
@@ -20,6 +20,7 @@
jest.mock('./assign_bundles_to_workers.ts');
jest.mock('./kibana_platform_plugins.ts');
jest.mock('./get_plugin_bundles.ts');
+jest.mock('../common/theme_tags.ts');
import Path from 'path';
import Os from 'os';
@@ -27,6 +28,7 @@ import Os from 'os';
import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils';
import { OptimizerConfig } from './optimizer_config';
+import { parseThemeTags } from '../common';
jest.spyOn(Os, 'cpus').mockReturnValue(['foo'] as any);
@@ -35,6 +37,7 @@ expect.addSnapshotSerializer(createAbsolutePathSerializer());
beforeEach(() => {
delete process.env.KBN_OPTIMIZER_MAX_WORKERS;
delete process.env.KBN_OPTIMIZER_NO_CACHE;
+ delete process.env.KBN_OPTIMIZER_THEMES;
jest.clearAllMocks();
});
@@ -81,6 +84,26 @@ describe('OptimizerConfig::parseOptions()', () => {
}).toThrowErrorMatchingInlineSnapshot(`"worker count must be a number"`);
});
+ it('defaults to * theme when dist = true', () => {
+ OptimizerConfig.parseOptions({
+ repoRoot: REPO_ROOT,
+ dist: true,
+ });
+
+ expect(parseThemeTags).toBeCalledWith('*');
+ });
+
+ it('defaults to KBN_OPTIMIZER_THEMES when dist = false', () => {
+ process.env.KBN_OPTIMIZER_THEMES = 'foo';
+
+ OptimizerConfig.parseOptions({
+ repoRoot: REPO_ROOT,
+ dist: false,
+ });
+
+ expect(parseThemeTags).toBeCalledWith('foo');
+ });
+
it('applies defaults', () => {
expect(
OptimizerConfig.parseOptions({
@@ -102,6 +125,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -127,6 +151,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -154,6 +179,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -178,6 +204,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -201,6 +228,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -222,6 +250,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -243,6 +272,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -264,6 +294,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -286,6 +317,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -308,6 +340,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": ,
+ "themeTags": undefined,
"watch": false,
}
`);
@@ -346,6 +379,7 @@ describe('OptimizerConfig::create()', () => {
pluginScanDirs: Symbol('parsed plugin scan dirs'),
repoRoot: Symbol('parsed repo root'),
watch: Symbol('parsed watch'),
+ themeTags: Symbol('theme tags'),
inspectWorkers: Symbol('parsed inspect workers'),
profileWebpack: Symbol('parsed profile webpack'),
}));
@@ -369,6 +403,7 @@ describe('OptimizerConfig::create()', () => {
"plugins": Symbol(new platform plugins),
"profileWebpack": Symbol(parsed profile webpack),
"repoRoot": Symbol(parsed repo root),
+ "themeTags": Symbol(theme tags),
"watch": Symbol(parsed watch),
}
`);
@@ -385,7 +420,7 @@ describe('OptimizerConfig::create()', () => {
[Window],
],
"invocationCallOrder": Array [
- 7,
+ 21,
],
"results": Array [
Object {
@@ -408,7 +443,7 @@ describe('OptimizerConfig::create()', () => {
[Window],
],
"invocationCallOrder": Array [
- 8,
+ 22,
],
"results": Array [
Object {
diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
index c9e9b3ad01ccc..7757004139d0d 100644
--- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
+++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
@@ -20,7 +20,14 @@
import Path from 'path';
import Os from 'os';
-import { Bundle, WorkerConfig, CacheableWorkerConfig } from '../common';
+import {
+ Bundle,
+ WorkerConfig,
+ CacheableWorkerConfig,
+ ThemeTag,
+ ThemeTags,
+ parseThemeTags,
+} from '../common';
import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins';
import { getPluginBundles } from './get_plugin_bundles';
@@ -73,6 +80,18 @@ interface Options {
/** flag that causes the core bundle to be built along with plugins */
includeCoreBundle?: boolean;
+
+ /**
+ * style themes that sass files will be converted to, the correct style will be
+ * loaded in the browser automatically by checking the global `__kbnThemeTag__`.
+ * Specifying additional styles increases build time.
+ *
+ * Defaults:
+ * - "*" when building the dist
+ * - comma separated list of themes in the `KBN_OPTIMIZER_THEMES` env var
+ * - "k7light"
+ */
+ themes?: ThemeTag | '*' | ThemeTag[];
}
interface ParsedOptions {
@@ -86,6 +105,7 @@ interface ParsedOptions {
pluginScanDirs: string[];
inspectWorkers: boolean;
includeCoreBundle: boolean;
+ themeTags: ThemeTags;
}
export class OptimizerConfig {
@@ -139,6 +159,10 @@ export class OptimizerConfig {
throw new TypeError('worker count must be a number');
}
+ const themeTags = parseThemeTags(
+ options.themes || (dist ? '*' : process.env.KBN_OPTIMIZER_THEMES)
+ );
+
return {
watch,
dist,
@@ -150,6 +174,7 @@ export class OptimizerConfig {
pluginPaths,
inspectWorkers,
includeCoreBundle,
+ themeTags,
};
}
@@ -181,7 +206,8 @@ export class OptimizerConfig {
options.repoRoot,
options.maxWorkerCount,
options.dist,
- options.profileWebpack
+ options.profileWebpack,
+ options.themeTags
);
}
@@ -194,7 +220,8 @@ export class OptimizerConfig {
public readonly repoRoot: string,
public readonly maxWorkerCount: number,
public readonly dist: boolean,
- public readonly profileWebpack: boolean
+ public readonly profileWebpack: boolean,
+ public readonly themeTags: ThemeTags
) {}
getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig {
@@ -205,6 +232,7 @@ export class OptimizerConfig {
repoRoot: this.repoRoot,
watch: this.watch,
optimizerCacheKey,
+ themeTags: this.themeTags,
browserslistEnv: this.dist ? 'production' : process.env.BROWSERSLIST_ENV || 'dev',
};
}
diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_state.ts b/packages/kbn-optimizer/src/optimizer/optimizer_state.ts
index 1572f459e6ee5..09f8ca10c6181 100644
--- a/packages/kbn-optimizer/src/optimizer/optimizer_state.ts
+++ b/packages/kbn-optimizer/src/optimizer/optimizer_state.ts
@@ -127,7 +127,7 @@ export function createOptimizerStateSummarizer(
}
if (event.type === 'worker stdio' || event.type === 'worker started') {
- // same state, but updated to the event is shared externally
+ // same state, but updated so the event is shared externally
return createOptimizerState(state);
}
diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts
index de5e9372e9e7a..ca7673748bde9 100644
--- a/packages/kbn-optimizer/src/worker/run_compilers.ts
+++ b/packages/kbn-optimizer/src/worker/run_compilers.ts
@@ -77,7 +77,7 @@ const observeCompiler = (
*/
const complete$ = Rx.fromEventPattern((cb) => done.tap(PLUGIN_NAME, cb)).pipe(
maybeMap((stats) => {
- // @ts-ignore not included in types, but it is real https://github.com/webpack/webpack/blob/ab4fa8ddb3f433d286653cd6af7e3aad51168649/lib/Watching.js#L58
+ // @ts-expect-error not included in types, but it is real https://github.com/webpack/webpack/blob/ab4fa8ddb3f433d286653cd6af7e3aad51168649/lib/Watching.js#L58
if (stats.compilation.needAdditionalPass) {
return undefined;
}
diff --git a/packages/kbn-optimizer/src/worker/theme_loader.ts b/packages/kbn-optimizer/src/worker/theme_loader.ts
index 5d02462ef1bb8..f2f685bde65d9 100644
--- a/packages/kbn-optimizer/src/worker/theme_loader.ts
+++ b/packages/kbn-optimizer/src/worker/theme_loader.ts
@@ -17,16 +17,43 @@
* under the License.
*/
+import { stringifyRequest, getOptions } from 'loader-utils';
import webpack from 'webpack';
-import { stringifyRequest } from 'loader-utils';
+import { parseThemeTags, ALL_THEMES, ThemeTag } from '../common';
+
+const getVersion = (tag: ThemeTag) => (tag.includes('v7') ? 7 : 8);
+const getIsDark = (tag: ThemeTag) => tag.includes('dark');
+const compare = (a: ThemeTag, b: ThemeTag) =>
+ (getVersion(a) === getVersion(b) ? 1 : 0) + (getIsDark(a) === getIsDark(b) ? 1 : 0);
// eslint-disable-next-line import/no-default-export
export default function (this: webpack.loader.LoaderContext) {
+ this.cacheable(true);
+
+ const options = getOptions(this);
+ const bundleId: string = options.bundleId!;
+ const themeTags = parseThemeTags(options.themeTags);
+
+ const cases = ALL_THEMES.map((tag) => {
+ if (themeTags.includes(tag)) {
+ return `
+ case '${tag}':
+ return require(${stringifyRequest(this, `${this.resourcePath}?${tag}`)});`;
+ }
+
+ const fallback = themeTags
+ .slice()
+ .sort((a, b) => compare(b, tag) - compare(a, tag))
+ .shift()!;
+
+ const message = `SASS files in [${bundleId}] were not built for theme [${tag}]. Styles were compiled using the [${fallback}] theme instead to keep Kibana somewhat usable. Please adjust the advanced settings to make use of [${themeTags}] or make sure the KBN_OPTIMIZER_THEMES environment variable includes [${tag}] in a comma separated list of themes you want to compile. You can also set it to "*" to build all themes.`;
+ return `
+ case '${tag}':
+ console.error(new Error(${JSON.stringify(message)}));
+ return require(${stringifyRequest(this, `${this.resourcePath}?${fallback}`)})`;
+ }).join('\n');
+
return `
-if (window.__kbnDarkMode__) {
- require(${stringifyRequest(this, `${this.resourcePath}?dark`)})
-} else {
- require(${stringifyRequest(this, `${this.resourcePath}?light`)});
-}
- `;
+switch (window.__kbnThemeTag__) {${cases}
+}`;
}
diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts
index 11f5544cd9274..aaea70d12c60d 100644
--- a/packages/kbn-optimizer/src/worker/webpack.config.ts
+++ b/packages/kbn-optimizer/src/worker/webpack.config.ts
@@ -21,11 +21,10 @@ import Path from 'path';
import { stringifyRequest } from 'loader-utils';
import webpack from 'webpack';
-// @ts-ignore
+// @ts-expect-error
import TerserPlugin from 'terser-webpack-plugin';
-// @ts-ignore
+// @ts-expect-error
import webpackMerge from 'webpack-merge';
-// @ts-ignore
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import CompressionPlugin from 'compression-webpack-plugin';
import * as UiSharedDeps from '@kbn/ui-shared-deps';
@@ -134,8 +133,8 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker:
test: /\.scss$/,
exclude: /node_modules/,
oneOf: [
- {
- resourceQuery: /dark|light/,
+ ...worker.themeTags.map((theme) => ({
+ resourceQuery: `?${theme}`,
use: [
{
loader: 'style-loader',
@@ -196,34 +195,27 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker:
loaderContext,
Path.resolve(
worker.repoRoot,
- 'src/legacy/ui/public/styles/_styling_constants.scss'
+ `src/legacy/ui/public/styles/_globals_${theme}.scss`
)
)};\n`;
},
webpackImporter: false,
implementation: require('node-sass'),
- sassOptions(loaderContext: webpack.loader.LoaderContext) {
- const darkMode = loaderContext.resourceQuery === '?dark';
-
- return {
- outputStyle: 'nested',
- includePaths: [Path.resolve(worker.repoRoot, 'node_modules')],
- sourceMapRoot: `/${bundle.type}:${bundle.id}`,
- importer: (url: string) => {
- if (darkMode && url.includes('eui_colors_light')) {
- return { file: url.replace('eui_colors_light', 'eui_colors_dark') };
- }
-
- return { file: url };
- },
- };
+ sassOptions: {
+ outputStyle: 'nested',
+ includePaths: [Path.resolve(worker.repoRoot, 'node_modules')],
+ sourceMapRoot: `/${bundle.type}:${bundle.id}`,
},
},
},
],
- },
+ })),
{
loader: require.resolve('./theme_loader'),
+ options: {
+ bundleId: bundle.id,
+ themeTags: worker.themeTags,
+ },
},
],
},
diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js
index caeffaabea62b..b2df4f40d4fbe 100644
--- a/packages/kbn-storybook/storybook_config/webpack.config.js
+++ b/packages/kbn-storybook/storybook_config/webpack.config.js
@@ -122,7 +122,7 @@ module.exports = async ({ config }) => {
prependData(loaderContext) {
return `@import ${stringifyRequest(
loaderContext,
- resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss')
+ resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss')
)};\n`;
},
sassOptions: {
diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js
index 02b64157686c1..0f981f3d07610 100644
--- a/packages/kbn-ui-shared-deps/entry.js
+++ b/packages/kbn-ui-shared-deps/entry.js
@@ -51,15 +51,6 @@ export const ElasticEui = require('@elastic/eui');
export const ElasticEuiLibServices = require('@elastic/eui/lib/services');
export const ElasticEuiLibServicesFormat = require('@elastic/eui/lib/services/format');
export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme');
-export let ElasticEuiLightTheme;
-export let ElasticEuiDarkTheme;
-if (window.__kbnThemeVersion__ === 'v7') {
- ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_light.json');
- ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_dark.json');
-} else {
- ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_amsterdam_light.json');
- ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_amsterdam_dark.json');
-}
import * as Theme from './theme.ts';
export { Theme };
diff --git a/packages/kbn-ui-shared-deps/theme.ts b/packages/kbn-ui-shared-deps/theme.ts
index ca4714779d39e..4b2758516fc26 100644
--- a/packages/kbn-ui-shared-deps/theme.ts
+++ b/packages/kbn-ui-shared-deps/theme.ts
@@ -23,9 +23,15 @@ const globals: any = typeof window === 'undefined' ? {} : window;
export type Theme = typeof LightTheme;
+// in the Kibana app we can rely on this global being defined, but in
+// some cases (like jest, or karma tests) the global is undefined
+export const tag: string = globals.__kbnThemeTag__ || 'v7light';
+export const version = tag.startsWith('v7') ? 7 : 8;
+export const darkMode = tag.endsWith('dark');
+
export let euiLightVars: Theme;
export let euiDarkVars: Theme;
-if (globals.__kbnThemeVersion__ === 'v7') {
+if (version === 7) {
euiLightVars = require('@elastic/eui/dist/eui_theme_light.json');
euiDarkVars = require('@elastic/eui/dist/eui_theme_dark.json');
} else {
@@ -37,7 +43,7 @@ if (globals.__kbnThemeVersion__ === 'v7') {
* EUI Theme vars that automatically adjust to light/dark theme
*/
export let euiThemeVars: Theme;
-if (globals.__kbnDarkTheme__) {
+if (darkMode) {
euiThemeVars = euiDarkVars;
} else {
euiThemeVars = euiLightVars;
diff --git a/src/core/public/index.scss b/src/core/public/index.scss
index 4be46899cff67..87825350b4e98 100644
--- a/src/core/public/index.scss
+++ b/src/core/public/index.scss
@@ -1,7 +1,3 @@
-// This file is built by both the legacy and KP build systems so we need to
-// import this explicitly
-@import '../../legacy/ui/public/styles/_styling_constants';
-
@import './core';
@import './chrome/index';
@import './overlays/index';
diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss
index e9810a747c8c7..7de0c8fc15f94 100644
--- a/src/legacy/core_plugins/kibana/public/index.scss
+++ b/src/legacy/core_plugins/kibana/public/index.scss
@@ -1,5 +1,3 @@
-@import 'src/legacy/ui/public/styles/styling_constants';
-
// Elastic charts
@import '@elastic/charts/dist/theme';
@import '@elastic/eui/src/themes/charts/theme';
diff --git a/src/legacy/core_plugins/tests_bundle/public/index.scss b/src/legacy/core_plugins/tests_bundle/public/index.scss
index 8020cef8d8492..d8dbf8d6dc885 100644
--- a/src/legacy/core_plugins/tests_bundle/public/index.scss
+++ b/src/legacy/core_plugins/tests_bundle/public/index.scss
@@ -1,5 +1,3 @@
-@import 'src/legacy/ui/public/styles/styling_constants';
-
// This file pulls some styles of NP plugins into the legacy test stylesheet
// so they are available for karma browser tests.
@import '../../../../plugins/vis_type_vislib/public/index';
diff --git a/src/legacy/core_plugins/timelion/public/index.scss b/src/legacy/core_plugins/timelion/public/index.scss
index ebf000d160b54..cf2a7859a505d 100644
--- a/src/legacy/core_plugins/timelion/public/index.scss
+++ b/src/legacy/core_plugins/timelion/public/index.scss
@@ -1,6 +1,3 @@
-// Should import both the EUI constants and any Kibana ones that are considered global
-@import 'src/legacy/ui/public/styles/styling_constants';
-
/* Timelion plugin styles */
// Prefix all styles with "tim" to avoid conflicts.
diff --git a/src/legacy/server/sass/__fixtures__/index.scss b/src/legacy/server/sass/__fixtures__/index.scss
index 019941534cadd..ed2657ed3f6ee 100644
--- a/src/legacy/server/sass/__fixtures__/index.scss
+++ b/src/legacy/server/sass/__fixtures__/index.scss
@@ -1,5 +1,3 @@
-@import 'src/legacy/ui/public/styles/styling_constants';
-
foo {
bar {
display: flex;
diff --git a/src/legacy/server/sass/build.js b/src/legacy/server/sass/build.js
index 2c0a2d84be2c0..536a6dc581db6 100644
--- a/src/legacy/server/sass/build.js
+++ b/src/legacy/server/sass/build.js
@@ -29,19 +29,15 @@ import isPathInside from 'is-path-inside';
import { PUBLIC_PATH_PLACEHOLDER } from '../../../optimize/public_path_placeholder';
const renderSass = promisify(sass.render);
+const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const access = promisify(fs.access);
const copyFile = promisify(fs.copyFile);
const mkdirAsync = promisify(fs.mkdir);
const UI_ASSETS_DIR = resolve(__dirname, '../../../core/server/core_app/assets');
-const DARK_THEME_IMPORTER = (url) => {
- if (url.includes('eui_colors_light')) {
- return { file: url.replace('eui_colors_light', 'eui_colors_dark') };
- }
-
- return { file: url };
-};
+const LIGHT_GLOBALS_PATH = resolve(__dirname, '../../../legacy/ui/public/styles/_globals_v7light');
+const DARK_GLOBALS_PATH = resolve(__dirname, '../../../legacy/ui/public/styles/_globals_v7dark');
const makeAsset = (request, { path, root, boundry, copyRoot, urlRoot }) => {
const relativePath = relative(root, path);
@@ -84,10 +80,16 @@ export class Build {
*/
async build() {
+ const scss = await readFile(this.sourcePath);
+ const relativeGlobalsPath =
+ this.theme === 'dark'
+ ? relative(this.sourceDir, DARK_GLOBALS_PATH)
+ : relative(this.sourceDir, LIGHT_GLOBALS_PATH);
+
const rendered = await renderSass({
file: this.sourcePath,
+ data: `@import '${relativeGlobalsPath}';\n${scss}`,
outFile: this.targetPath,
- importer: this.theme === 'dark' ? DARK_THEME_IMPORTER : undefined,
sourceMap: true,
outputStyle: 'nested',
sourceMapEmbed: true,
diff --git a/src/legacy/ui/public/styles/_globals_v7dark.scss b/src/legacy/ui/public/styles/_globals_v7dark.scss
new file mode 100644
index 0000000000000..d5a8535f32718
--- /dev/null
+++ b/src/legacy/ui/public/styles/_globals_v7dark.scss
@@ -0,0 +1,12 @@
+// v7dark global scope
+//
+// prepended to all .scss imports (from JS, when v7dark theme selected) and
+// legacy uiExports.styleSheetPaths when any dark theme is selected
+
+@import '@elastic/eui/src/themes/eui/eui_colors_dark';
+
+@import '@elastic/eui/src/global_styling/functions/index';
+@import '@elastic/eui/src/global_styling/variables/index';
+@import '@elastic/eui/src/global_styling/mixins/index';
+
+@import './mixins';
diff --git a/src/legacy/ui/public/styles/_styling_constants.scss b/src/legacy/ui/public/styles/_globals_v7light.scss
similarity index 59%
rename from src/legacy/ui/public/styles/_styling_constants.scss
rename to src/legacy/ui/public/styles/_globals_v7light.scss
index 74fc54b410285..522b346b64900 100644
--- a/src/legacy/ui/public/styles/_styling_constants.scss
+++ b/src/legacy/ui/public/styles/_globals_v7light.scss
@@ -1,9 +1,10 @@
-// EUI global scope
+// v7light global scope
+//
+// prepended to all .scss imports (from JS, when v7light theme selected) and
+// legacy uiExports.styleSheetPaths when any dark theme is selected
@import '@elastic/eui/src/themes/eui/eui_colors_light';
-// Note that fonts are loaded directly by src/legacy/ui/ui_render/views/chrome.pug
-
@import '@elastic/eui/src/global_styling/functions/index';
@import '@elastic/eui/src/global_styling/variables/index';
@import '@elastic/eui/src/global_styling/mixins/index';
diff --git a/src/legacy/ui/public/styles/_globals_v8dark.scss b/src/legacy/ui/public/styles/_globals_v8dark.scss
new file mode 100644
index 0000000000000..972365e9e9d0e
--- /dev/null
+++ b/src/legacy/ui/public/styles/_globals_v8dark.scss
@@ -0,0 +1,16 @@
+// v8dark global scope
+//
+// prepended to all .scss imports (from JS, when v8dark theme selected)
+
+@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_dark';
+
+@import '@elastic/eui/src/global_styling/functions/index';
+@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index';
+
+@import '@elastic/eui/src/global_styling/variables/index';
+@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index';
+
+@import '@elastic/eui/src/global_styling/mixins/index';
+@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index';
+
+@import './mixins';
diff --git a/src/legacy/ui/public/styles/_globals_v8light.scss b/src/legacy/ui/public/styles/_globals_v8light.scss
new file mode 100644
index 0000000000000..dc99f4d45082e
--- /dev/null
+++ b/src/legacy/ui/public/styles/_globals_v8light.scss
@@ -0,0 +1,16 @@
+// v8light global scope
+//
+// prepended to all .scss imports (from JS, when v8light theme selected)
+
+@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_light';
+
+@import '@elastic/eui/src/global_styling/functions/index';
+@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index';
+
+@import '@elastic/eui/src/global_styling/variables/index';
+@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index';
+
+@import '@elastic/eui/src/global_styling/mixins/index';
+@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index';
+
+@import './mixins';
diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs
index ca2e944489a73..bbca051ce31a1 100644
--- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs
+++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs
@@ -1,7 +1,6 @@
var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data'));
window.__kbnStrictCsp__ = kbnCsp.strictCsp;
-window.__kbnDarkMode__ = {{darkMode}};
-window.__kbnThemeVersion__ = "{{themeVersion}}";
+window.__kbnThemeTag__ = "{{themeTag}}";
window.__kbnPublicPath__ = {{publicPathMap}};
window.__kbnBundles__ = {{kbnBundlesLoaderSource}}
diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js
index 0cfcb91aa94ef..b4b18e086e809 100644
--- a/src/legacy/ui/ui_render/ui_render_mixin.js
+++ b/src/legacy/ui/ui_render/ui_render_mixin.js
@@ -89,6 +89,7 @@ export function uiRenderMixin(kbnServer, server, config) {
const isCore = !app;
const uiSettings = request.getUiSettingsService();
+
const darkMode =
!authEnabled || request.auth.isAuthenticated
? await uiSettings.get('theme:darkMode')
@@ -99,6 +100,8 @@ export function uiRenderMixin(kbnServer, server, config) {
? await uiSettings.get('theme:version')
: 'v7';
+ const themeTag = `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`;
+
const buildHash = server.newPlatform.env.packageInfo.buildNum;
const basePath = config.get('server.basePath');
@@ -178,8 +181,7 @@ export function uiRenderMixin(kbnServer, server, config) {
const bootstrap = new AppBootstrap({
templateData: {
- darkMode,
- themeVersion,
+ themeTag,
jsDependencyPaths,
styleSheetPaths,
publicPathMap,
diff --git a/src/plugins/tile_map/public/index.scss b/src/plugins/tile_map/public/index.scss
index 4ce500b2da4d2..f4b86b0c31190 100644
--- a/src/plugins/tile_map/public/index.scss
+++ b/src/plugins/tile_map/public/index.scss
@@ -1,5 +1,3 @@
-@import 'src/legacy/ui/public/styles/styling_constants';
-
// Prefix all styles with "tlm" to avoid conflicts.
// Examples
// tlmChart
diff --git a/x-pack/plugins/canvas/.storybook/webpack.config.js b/x-pack/plugins/canvas/.storybook/webpack.config.js
index 45a5303d8b0db..3148a6742f76a 100644
--- a/x-pack/plugins/canvas/.storybook/webpack.config.js
+++ b/x-pack/plugins/canvas/.storybook/webpack.config.js
@@ -80,7 +80,7 @@ module.exports = async ({ config }) => {
prependData(loaderContext) {
return `@import ${stringifyRequest(
loaderContext,
- path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss')
+ path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss')
)};\n`;
},
sassOptions: {
@@ -199,7 +199,6 @@ module.exports = async ({ config }) => {
config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve(__dirname, '../tasks/mocks/uiAbsoluteToParsedUrl');
config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome');
config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public');
- config.resolve.alias['src/legacy/ui/public/styles/styling_constants'] = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss');
config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock');
return config;
diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss
index 78a34a58f5f78..9cd2bdabd3f45 100644
--- a/x-pack/plugins/canvas/public/style/index.scss
+++ b/x-pack/plugins/canvas/public/style/index.scss
@@ -1,5 +1,3 @@
-@import 'src/legacy/ui/public/styles/styling_constants';
-
// Canvas core
@import 'hackery';
@import 'main';
diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js
index 66b0a7bc558cb..1a5a21985ba72 100644
--- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js
+++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js
@@ -188,7 +188,7 @@ module.exports = {
prependData(loaderContext) {
return `@import ${stringifyRequest(
loaderContext,
- path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss')
+ path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss')
)};\n`;
},
webpackImporter: false,
diff --git a/x-pack/plugins/index_management/public/index.scss b/x-pack/plugins/index_management/public/index.scss
index 0fbf8ea5036c5..02686c4f7d6f3 100644
--- a/x-pack/plugins/index_management/public/index.scss
+++ b/x-pack/plugins/index_management/public/index.scss
@@ -1,6 +1,3 @@
-// Import the EUI global scope so we can use EUI constants
-@import 'src/legacy/ui/public/styles/_styling_constants';
-
// Index management plugin styles
// Prefix all styles with "ind" to avoid conflicts.
diff --git a/x-pack/plugins/infra/public/index.scss b/x-pack/plugins/infra/public/index.scss
index 05e045c1bd53b..a3d74e3afebe3 100644
--- a/x-pack/plugins/infra/public/index.scss
+++ b/x-pack/plugins/infra/public/index.scss
@@ -1,6 +1,3 @@
-// Import the EUI global scope so we can use EUI constants
-@import 'src/legacy/ui/public/styles/_styling_constants';
-
/* Infra plugin styles */
.infra-container-element {
diff --git a/x-pack/plugins/maps/public/index.scss b/x-pack/plugins/maps/public/index.scss
index fe974fa610c03..d2dd07b0f81f9 100644
--- a/x-pack/plugins/maps/public/index.scss
+++ b/x-pack/plugins/maps/public/index.scss
@@ -1,8 +1,5 @@
/* GIS plugin styles */
-// Import the EUI global scope so we can use EUI constants
-@import 'src/legacy/ui/public/styles/_styling_constants';
-
// Prefix all styles with "map" to avoid conflicts.
// Examples
// mapChart
diff --git a/x-pack/plugins/ml/public/application/_index.scss b/x-pack/plugins/ml/public/application/_index.scss
index 11dc593a235a1..65e914a1ac923 100644
--- a/x-pack/plugins/ml/public/application/_index.scss
+++ b/x-pack/plugins/ml/public/application/_index.scss
@@ -1,6 +1,3 @@
-// Should import both the EUI constants and any Kibana ones that are considered global
-@import 'src/legacy/ui/public/styles/styling_constants';
-
// ML has it's own variables for coloring
@import 'variables';
diff --git a/x-pack/plugins/snapshot_restore/public/application/index.scss b/x-pack/plugins/snapshot_restore/public/application/index.scss
index b680f4d3ebf90..3e16e3b5301e7 100644
--- a/x-pack/plugins/snapshot_restore/public/application/index.scss
+++ b/x-pack/plugins/snapshot_restore/public/application/index.scss
@@ -1,6 +1,3 @@
-// Import the EUI global scope so we can use EUI constants
-@import 'src/legacy/ui/public/styles/_styling_constants';
-
// Snapshot and Restore plugin styles
// Prefix all styles with "snapshotRestore" to avoid conflicts.
diff --git a/x-pack/plugins/transform/public/app/index.scss b/x-pack/plugins/transform/public/app/index.scss
index beb5ee6be67e6..cc5cc87c754c9 100644
--- a/x-pack/plugins/transform/public/app/index.scss
+++ b/x-pack/plugins/transform/public/app/index.scss
@@ -1,6 +1,3 @@
-// Import the EUI global scope so we can use EUI constants
-@import 'src/legacy/ui/public/styles/_styling_constants';
-
// Transform plugin styles
// Prefix all styles with "transform" to avoid conflicts.
diff --git a/x-pack/plugins/upgrade_assistant/public/application/_index.scss b/x-pack/plugins/upgrade_assistant/public/application/_index.scss
index 6000af5498cd6..841415620d691 100644
--- a/x-pack/plugins/upgrade_assistant/public/application/_index.scss
+++ b/x-pack/plugins/upgrade_assistant/public/application/_index.scss
@@ -1,3 +1 @@
-@import 'src/legacy/ui/public/styles/_styling_constants';
-
@import 'components/index';
From 23ea7acb152d238d07e7e0db2c4b7a78b0838dc0 Mon Sep 17 00:00:00 2001
From: Nathan Reese
Date: Thu, 2 Jul 2020 16:35:16 -0600
Subject: [PATCH 42/49] [Maps] Fix cannot select Solid fill-color when removing
fields (#70621)
Co-authored-by: Elastic Machine
---
.../maps/public/actions/layer_actions.ts | 4 +-
.../maps/public/classes/styles/style.ts | 6 +-
.../classes/styles/vector/vector_style.js | 16 ++-
.../styles/vector/vector_style.test.js | 114 +++++++-----------
4 files changed, 66 insertions(+), 74 deletions(-)
diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts
index 51e251a5d8e20..a2711fbd124fb 100644
--- a/x-pack/plugins/maps/public/actions/layer_actions.ts
+++ b/x-pack/plugins/maps/public/actions/layer_actions.ts
@@ -13,6 +13,7 @@ import {
getLayerListRaw,
getSelectedLayerId,
getMapReady,
+ getMapColors,
} from '../selectors/map_selectors';
import { FLYOUT_STATE } from '../reducers/ui';
import { cancelRequest } from '../reducers/non_serializable_instances';
@@ -384,7 +385,8 @@ export function clearMissingStyleProperties(layerId: string) {
const nextFields = await (targetLayer as IVectorLayer).getFields(); // take into account all fields, since labels can be driven by any field (source or join)
const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved(
- nextFields
+ nextFields,
+ getMapColors(getState())
);
if (hasChanges && nextStyleDescriptor) {
dispatch(updateLayerStyle(layerId, nextStyleDescriptor));
diff --git a/x-pack/plugins/maps/public/classes/styles/style.ts b/x-pack/plugins/maps/public/classes/styles/style.ts
index 7d39acd504c42..1859c7875ad11 100644
--- a/x-pack/plugins/maps/public/classes/styles/style.ts
+++ b/x-pack/plugins/maps/public/classes/styles/style.ts
@@ -13,7 +13,8 @@ import { DataRequest } from '../util/data_request';
export interface IStyle {
getDescriptor(): StyleDescriptor | null;
getDescriptorWithMissingStylePropsRemoved(
- nextFields: IField[]
+ nextFields: IField[],
+ mapColors: string[]
): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor };
pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor;
renderEditor({
@@ -34,7 +35,8 @@ export class AbstractStyle implements IStyle {
}
getDescriptorWithMissingStylePropsRemoved(
- nextFields: IField[]
+ nextFields: IField[],
+ mapColors: string[]
): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor } {
return {
hasChanges: false,
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js
index 04a5381fa2592..3cff48e4d682e 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js
@@ -7,7 +7,12 @@
import _ from 'lodash';
import React from 'react';
import { VectorStyleEditor } from './components/vector_style_editor';
-import { getDefaultProperties, LINE_STYLES, POLYGON_STYLES } from './vector_style_defaults';
+import {
+ getDefaultProperties,
+ getDefaultStaticProperties,
+ LINE_STYLES,
+ POLYGON_STYLES,
+} from './vector_style_defaults';
import { AbstractStyle } from '../style';
import {
GEO_JSON_TYPE,
@@ -191,7 +196,7 @@ export class VectorStyle extends AbstractStyle {
* This method does not update its descriptor. It just returns a new descriptor that the caller
* can then use to update store state via dispatch.
*/
- getDescriptorWithMissingStylePropsRemoved(nextFields) {
+ getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors) {
const originalProperties = this.getRawProperties();
const updatedProperties = {};
@@ -201,6 +206,13 @@ export class VectorStyle extends AbstractStyle {
});
dynamicProperties.forEach((key) => {
+ // Convert dynamic styling to static stying when there are no nextFields
+ if (nextFields.length === 0) {
+ const staticProperties = getDefaultStaticProperties(mapColors);
+ updatedProperties[key] = staticProperties[key];
+ return;
+ }
+
const dynamicProperty = originalProperties[key];
const fieldName =
dynamicProperty && dynamicProperty.options.field && dynamicProperty.options.field.name;
diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js
index a0dc07b8e545b..a85cd0cc86407 100644
--- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js
+++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js
@@ -6,7 +6,12 @@
import { VectorStyle } from './vector_style';
import { DataRequest } from '../../util/data_request';
-import { FIELD_ORIGIN, STYLE_TYPE, VECTOR_SHAPE_TYPE } from '../../../../common/constants';
+import {
+ FIELD_ORIGIN,
+ STYLE_TYPE,
+ VECTOR_SHAPE_TYPE,
+ VECTOR_STYLES,
+} from '../../../../common/constants';
jest.mock('../../../kibana_services');
jest.mock('ui/new_platform');
@@ -42,6 +47,7 @@ class MockSource {
describe('getDescriptorWithMissingStylePropsRemoved', () => {
const fieldName = 'doIStillExist';
+ const mapColors = [];
const properties = {
fillColor: {
type: STYLE_TYPE.STATIC,
@@ -59,7 +65,8 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => {
iconSize: {
type: STYLE_TYPE.DYNAMIC,
options: {
- color: 'a color',
+ minSize: 1,
+ maxSize: 10,
field: { name: fieldName, origin: FIELD_ORIGIN.SOURCE },
},
},
@@ -75,86 +82,55 @@ describe('getDescriptorWithMissingStylePropsRemoved', () => {
const vectorStyle = new VectorStyle({ properties }, new MockSource());
const nextFields = [new MockField({ fieldName })];
- const { hasChanges } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields);
+ const { hasChanges } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(
+ nextFields,
+ mapColors
+ );
expect(hasChanges).toBe(false);
});
it('Should clear missing fields when next ordinal fields do not contain existing style property fields', () => {
const vectorStyle = new VectorStyle({ properties }, new MockSource());
- const nextFields = [];
+ const nextFields = [new MockField({ fieldName: 'someOtherField' })];
const {
hasChanges,
nextStyleDescriptor,
- } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields);
+ } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors);
expect(hasChanges).toBe(true);
- expect(nextStyleDescriptor.properties).toEqual({
- fillColor: {
- options: {},
- type: 'STATIC',
- },
- icon: {
- options: {
- value: 'marker',
- },
- type: 'STATIC',
- },
- iconOrientation: {
- options: {
- orientation: 0,
- },
- type: 'STATIC',
- },
- iconSize: {
- options: {
- color: 'a color',
- },
- type: 'DYNAMIC',
- },
- labelText: {
- options: {
- value: '',
- },
- type: 'STATIC',
- },
- labelBorderColor: {
- options: {
- color: '#FFFFFF',
- },
- type: 'STATIC',
- },
- labelBorderSize: {
- options: {
- size: 'SMALL',
- },
- },
- labelColor: {
- options: {
- color: '#000000',
- },
- type: 'STATIC',
- },
- labelSize: {
- options: {
- size: 14,
- },
- type: 'STATIC',
- },
- lineColor: {
- options: {},
- type: 'DYNAMIC',
+ expect(nextStyleDescriptor.properties[VECTOR_STYLES.LINE_COLOR]).toEqual({
+ options: {},
+ type: 'DYNAMIC',
+ });
+ expect(nextStyleDescriptor.properties[VECTOR_STYLES.ICON_SIZE]).toEqual({
+ options: {
+ minSize: 1,
+ maxSize: 10,
},
- lineWidth: {
- options: {
- size: 1,
- },
- type: 'STATIC',
+ type: 'DYNAMIC',
+ });
+ });
+
+ it('Should convert dynamic styles to static styles when there are no next fields', () => {
+ const vectorStyle = new VectorStyle({ properties }, new MockSource());
+
+ const nextFields = [];
+ const {
+ hasChanges,
+ nextStyleDescriptor,
+ } = vectorStyle.getDescriptorWithMissingStylePropsRemoved(nextFields, mapColors);
+ expect(hasChanges).toBe(true);
+ expect(nextStyleDescriptor.properties[VECTOR_STYLES.LINE_COLOR]).toEqual({
+ options: {
+ color: '#41937c',
},
- symbolizeAs: {
- options: {
- value: 'circle',
- },
+ type: 'STATIC',
+ });
+ expect(nextStyleDescriptor.properties[VECTOR_STYLES.ICON_SIZE]).toEqual({
+ options: {
+ size: 6,
},
+ type: 'STATIC',
});
});
});
From 20237b8d5b3a0b9bfdbd955537e1f12199318a28 Mon Sep 17 00:00:00 2001
From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com>
Date: Thu, 2 Jul 2020 18:40:39 -0400
Subject: [PATCH 43/49] [EPM] Use higher priority than default templates
(#70640)
* Use higher priority than default templates
* Fixing the jest tests
---
.../template/__snapshots__/template.test.ts.snap | 6 +++---
.../server/services/epm/elasticsearch/template/template.ts | 7 +++++--
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index 635742c82f9a4..f5fec020bf5b4 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -2,7 +2,7 @@
exports[`tests loading base.yml: base.yml 1`] = `
{
- "priority": 1,
+ "priority": 200,
"index_patterns": [
"foo-*"
],
@@ -105,7 +105,7 @@ exports[`tests loading base.yml: base.yml 1`] = `
exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
{
- "priority": 1,
+ "priority": 200,
"index_patterns": [
"foo-*"
],
@@ -208,7 +208,7 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
exports[`tests loading system.yml: system.yml 1`] = `
{
- "priority": 1,
+ "priority": 200,
"index_patterns": [
"whatsthis-*"
],
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
index 06c07da6cd77a..2de378f717534 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
@@ -247,8 +247,11 @@ function getBaseTemplate(
packageName: string
): IndexTemplate {
return {
- // This takes precedence over all index templates installed with the 'base' package
- priority: 1,
+ // This takes precedence over all index templates installed by ES by default (logs-*-* and metrics-*-*)
+ // if this number is lower than the ES value (which is 100) this template will never be applied when a data stream
+ // is created. I'm using 200 here to give some room for users to create their own template and fit it between the
+ // default and the one the ingest manager uses.
+ priority: 200,
// To be completed with the correct index patterns
index_patterns: [`${templateName}-*`],
template: {
From 67c70e762cf4a37705e3e9ecd91f61fc57a36594 Mon Sep 17 00:00:00 2001
From: CJ Cenizal
Date: Thu, 2 Jul 2020 16:00:05 -0700
Subject: [PATCH 44/49] Add Snapshot Restore README with quick-testing steps.
(#70494)
---
x-pack/plugins/snapshot_restore/README.md | 78 +++++++++++++++++++++++
1 file changed, 78 insertions(+)
create mode 100644 x-pack/plugins/snapshot_restore/README.md
diff --git a/x-pack/plugins/snapshot_restore/README.md b/x-pack/plugins/snapshot_restore/README.md
new file mode 100644
index 0000000000000..e11483785e958
--- /dev/null
+++ b/x-pack/plugins/snapshot_restore/README.md
@@ -0,0 +1,78 @@
+# Snapshot Restore
+
+## Quick steps for testing
+
+### File system
+
+1. Add the file system path you want to use to elasticsearch.yml or as part of starting up ES. Note that this path should point to a directory that exists.
+
+```
+path:
+ repo: /tmp/es-backups
+```
+
+or
+
+```
+yarn es snapshot --license=trial -E path.repo=/tmp/es-backups
+```
+
+2. Use Console or UI to add a repository. Use the file system path above as the `location` setting:
+
+```
+PUT /_snapshot/my_backup
+{
+ "type": "fs",
+ "settings": {
+ "location": "/tmp/es-backups",
+ "chunk_size": "10mb"
+ }
+}
+```
+
+3. Adjust `settings` as necessary, all available settings can be found in docs:
+https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html#_shared_file_system_repository
+
+### Readonly
+
+Readonly repositories only take `url` setting. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html#_read_only_url_repository
+
+It's easy to set up a `file:` url:
+```
+PUT _snapshot/my_readonly_repository
+{
+ "type": "url",
+ "settings": {
+ "url": "file:///tmp/es-backups"
+ }
+}
+```
+
+### Source only
+
+Source only repositories are special in that they are basically a wrapper around another repository type. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-snapshots.html#_source_only_repository
+
+This means that the settings that are available depends on the `delegate_type` parameter. For example, this source only repository delegates to `fs` (file system) type, so all file system rules and available settings apply:
+
+```
+PUT _snapshot/my_src_only_repository
+{
+ "type" : "source",
+ "settings" : {
+ "delegate_type" : "fs",
+ "location" : "/tmp/es-backups"
+ }
+}
+```
+
+### Plugin-based repositories:
+
+There are four official repository plugins available: S3, GCS, HDFS, Azure. Available plugin repository settings can be found in the docs: https://www.elastic.co/guide/en/elasticsearch/plugins/master/repository.html.
+
+To run ES with plugins:
+
+1. Run `yarn es snapshot` from the Kibana directory like normal, then exit out of process.
+2. `cd .es/8.0.0`
+3. `bin/elasticsearch-plugin install https://snapshots.elastic.co/downloads/elasticsearch-plugins/repository-s3/repository-s3-8.0.0-SNAPSHOT.zip`
+4. Repeat step 3 for additional plugins, replacing occurrences of `repository-s3` with the plugin you want to install.
+5. Run `bin/elasticsearch` from the `.es/8.0.0` directory. Otherwise, starting ES with `yarn es snapshot` would overwrite the plugins you just installed.
\ No newline at end of file
From 6c62c686cf28f76dba2248be7382c1ad9a342162 Mon Sep 17 00:00:00 2001
From: Tiago Costa
Date: Fri, 3 Jul 2020 01:30:13 +0100
Subject: [PATCH 45/49] chore(NA): upgrade to lodash@4 (#69868)
* chore(NA): upgrade oss to lodash4
chore(NA): migrate cli, cli_plugin, cli_keystore, dev, test_utils and apm src script to lodash4
chore(NA): missing file for cli plugin
chore(NA): add src core
chore(NA): es archiver and fixtures
chore(NA): try to fix functional test failure
chore(NA): migrate src/legacy entirely to lodash4 except src/legacy/core_plugins
chore(NA): move legacy core plugins to lodash4
chore(NA): upgrade optimize to lodash4
chore(NA): upgrade to lodash4 on advanced_settings, charts, console and dashboard
chore(NA): migrate to lodash4 on dev_tools, discover, embeddable, es_ui)shared, expressions, home plugins
chore(NA): upgrade data plugin to lodash4
chore(NA): upgrade usage_collection, ui_actions, tile_map, telemtry, share, saved_objects, saved_objects_management, region_map and navigation to lodash4
chore(NA): missing data upgrades to lodash4
Revert "chore(NA): upgrade usage_collection, ui_actions, tile_map, telemtry, share, saved_objects, saved_objects_management, region_map and navigation to lodash4"
This reverts commit 137055c5fed2fc52bb26547e0bc1ad2e3d4fe309.
Revert "Revert "chore(NA): upgrade usage_collection, ui_actions, tile_map, telemtry, share, saved_objects, saved_objects_management, region_map and navigation to lodash4""
This reverts commit f7e73688782998513d9fb6d7e8f0765e9beb28d1.
Revert "chore(NA): missing data upgrades to lodash4"
This reverts commit 92b85bf947a89bfc70cc4052738a6b2128ffb076.
Revert "chore(NA): upgrade data plugin to lodash4"
This reverts commit 88fdb075ee1e26c4ac979b6681d8a2b002df74c6.
chore(NA): upgrade idx_pattern_mgt, input_control_vis, inspector, kbn_legacy, kbn_react, kbn_usage_collections, kbn_utils, management and maps_legacy to lodash4
chore(NA): map src plugin data to lodash3
chore(NA): missing lodash.clonedeep dep
chore(NA): change packages kbn-config-schema deps
chore(NA): update renovate config
chore(NA): upgrade vis_type plugins to lodash4
chore(NA): move vis_type_vislib to lodash3
chore(NA): update visualizations and visualize to lodash4
chore(NA): remove lodash 3 types from src and move test to lodash4
chore(NA): move home, usage_collection and management to lodash 3
Revert "chore(NA): move home, usage_collection and management to lodash 3"
This reverts commit f86e8585f02d21550746569af54215b076a79a3d.
chore(NA): move kibana_legacy, saved_objects saved_objects_management into lodash3
chore(NA): update x-pack test to mock lodash4
Revert "chore(NA): move kibana_legacy, saved_objects saved_objects_management into lodash3"
This reverts commit 2d10fe450533e1b36db21d99cfae3ce996a244e0.
* chore(NA): move x-pack and packages to lodash 4
* chore(NA): remove mention to lodash from main package.json
* chore(NA): remove helper alias for lodash4 and make it the default lodash
* chore(NA): fix last failing types in the repo
* chore(NA): fix public api
* chore(NA): fix types for agg_row.tsx
* chore(NA): fix increment of optimizer modules in the rollup plugin
* chore(NA): migrate `src/core/public/http/fetch.ts` (#5)
* omit undefined query props
* just remove merge usage
* fix types
* chore(NA): fixes for feedback from apm team
* chore(NA): recover old behaviour on apm LoadingIndeicatorContext.tsx
* chore(NA): fixes for feedback from watson
* Platform lodash4 tweaks (#6)
* chore(NA): fix types and behaviour on src/core/server/elasticsearch/errors.ts
* Canvas fixes for lodash upgrade
* [APM] Adds unit test for APM service maps transform (#7)
* Adds a snapshot unit test for getConnections and rearranges some code to make testing easier
* reverts `ArrayList` back to `String[]` in the painless script within `fetch_service_paths_from_trace_ids.ts`
* chore(NA): update yarn.lock
* chore(NA): remove any and use a real type for alerts task runner
Co-authored-by: Gidi Meir Morris
* chore(NA): used named import for triggers_actions_ui file
* chore(NA): fix eslint
* chore(NA): fix types
* Delete most uptime lodash references.
* Simplify. Clean up types.
* [Uptime] Delete most uptime lodash references (#8)
* Delete most uptime lodash references.
* Simplify. Clean up types.
* chore(NA): add eslint rule to avoid using lodash3
* chore(NA): apply changes on feedback from es-ui team
* fix some types (#9)
* Clean up some expressions types.
* chore(NA): missing ts-expect-error statements
* Upgrade lodash 4 vislib (#11)
* replace lodash 3 with lodash 4 on vislib plugin
* Further changes
* further replacement of lodash3 to 4
* further work on upgrading to lodash 4
* final changes to update lodash
* chore(NA): upgrade data plugin to lodash4
chore(NA): upgrade data plugin public to lodash4
chore(NA): fix typecheck task
chore(NA): fix agg_config with hasIn
chore(NA): assign to assignIn and has to hasIn
chore(NA): upgrade data plugin server to lodash4
chore(NA): new signature for core api
fix(NA): match behaviour between lodash3 and lodash4 for set in search_source
* chore(NA): remove lodash3 completely from the repo
* chore(NA): fix x-pack/test/api_integration/apis/metrics_ui/snapshot.ts missing content
* chore(NA): fix lodash usage on apm
* chore(NA): fix typecheck for maps
* Patch lodash template (#12)
* Applying changes from https://github.com/elastic/kibana/pull/64985
* Using isIterateeCall, because it seems less brittle
* Also patching `lodash/template` and `lodash/fp/template`
* Reorganizing some files...
* Revising comment
* Ends up `_` is a function also... I hate JavaScript
Co-authored-by: Pierre Gayvallet
Co-authored-by: Josh Dover
Co-authored-by: Clint Andrew Hall
Co-authored-by: Oliver Gupte
Co-authored-by: Elastic Machine
Co-authored-by: Gidi Meir Morris
Co-authored-by: Justin Kambic
Co-authored-by: Stratoula Kalafateli
Co-authored-by: Luke Elmers
Co-authored-by: Brandon Kobel
Co-authored-by: kobelb
---
...-plugins-data-public.fieldformat.tojson.md | 8 +-
package.json | 9 +-
packages/kbn-config-schema/package.json | 1 +
packages/kbn-interpreter/package.json | 3 +-
.../src/common/lib/registry.js | 2 +-
packages/kbn-plugin-generator/index.js | 4 +-
packages/kbn-plugin-generator/package.json | 5 +-
.../kbn-plugin-generator/sao_template/sao.js | 4 +-
packages/kbn-pm/package.json | 4 +-
.../absolute_path_snapshot_serializer.ts | 2 +-
.../kbn-storybook/lib/webpack.dll.config.js | 1 -
packages/kbn-test/package.json | 2 +
.../lib/config/config.ts | 9 +-
.../src/legacy_es/legacy_es_test_cluster.js | 3 +-
.../src/page_load_metrics/navigation.ts | 1 -
packages/kbn-ui-framework/Gruntfile.js | 2 +-
packages/kbn-ui-framework/package.json | 2 +-
renovate.json5 | 16 -
src/apm.js | 2 +-
src/cli/cluster/cluster_manager.test.ts | 2 +-
src/cli/cluster/worker.ts | 2 +-
src/cli/help.js | 2 +-
src/core/public/http/fetch.ts | 39 +-
.../public/plugins/plugins_service.test.ts | 6 +-
.../saved_objects/saved_objects_client.ts | 4 +-
.../saved_objects/simple_saved_object.ts | 2 +-
.../server/capabilities/merge_capabilities.ts | 4 +-
.../config/deprecation/core_deprecations.ts | 5 +-
.../server/elasticsearch/legacy/errors.ts | 2 +-
.../server/http/base_path_proxy_server.ts | 4 +-
...gacy_object_to_config_adapter.test.ts.snap | 5 +-
.../mappings/lib/get_property.ts | 2 +-
.../migrations/core/document_migrator.ts | 5 +-
.../core/migration_coordinator.test.ts | 1 -
.../saved_objects/service/lib/repository.ts | 2 +-
src/core/utils/deep_freeze.test.ts | 15 +-
src/dev/precommit_hook/casing_check_config.js | 1 -
src/dev/sass/build_sass.js | 2 +-
src/fixtures/agg_resp/geohash_grid.js | 2 +-
.../core_plugins/console_legacy/index.ts | 2 +-
.../server/lib/handle_es_error.js | 2 +-
.../__tests__/vis_type_vislib/lib/dispatch.js | 2 +-
.../visualizations/column_chart.js | 4 +-
.../core_plugins/timelion/public/app.js | 2 +-
.../public/directives/saved_object_finder.js | 2 +-
.../directives/timelion_expression_input.js | 2 +-
.../timelion_interval/timelion_interval.js | 2 +-
.../public/panels/timechart/schema.ts | 2 +-
.../plugin_spec/plugin_spec.js | 3 +-
.../i18n/localization/file_integrity.ts | 2 +-
src/legacy/server/logging/log_format.js | 2 +-
src/legacy/server/status/server_status.js | 2 +-
src/legacy/server/status/states.js | 2 +-
src/legacy/ui/public/events.js | 4 +-
.../indexed_array/__tests__/indexed_array.js | 8 +-
.../ui/public/indexed_array/indexed_array.js | 2 +-
.../public/routes/__tests__/_route_manager.js | 2 +-
.../ui/public/state_management/state.js | 2 +-
src/legacy/ui/public/utils/collection.ts | 2 +-
src/legacy/utils/deep_clone_with_buffers.ts | 4 +-
src/legacy/utils/unset.js | 3 +-
.../services/colors/mapped_colors.test.ts | 2 +-
.../public/services/colors/mapped_colors.ts | 8 +-
.../application/components/settings_modal.tsx | 1 +
.../__tests__/integration.test.js | 2 +-
.../public/lib/autocomplete/body_completer.js | 2 +-
.../autocomplete/components/list_component.js | 2 +-
.../components/url_pattern_matcher.js | 109 +-
.../console/public/lib/autocomplete/engine.js | 18 +-
.../public/lib/autocomplete/url_params.js | 20 +-
src/plugins/console/public/lib/kb/api.js | 12 +-
.../console/public/lib/mappings/mappings.js | 4 +-
.../server/lib/elasticsearch_proxy_config.ts | 2 +-
.../api/console/proxy/create_handler.ts | 4 +-
.../services/spec_definitions_service.ts | 4 +-
.../actions/clone_panel_action.tsx | 1 +
.../actions/replace_panel_flyout.tsx | 1 +
.../application/dashboard_app_controller.tsx | 6 +-
.../embeddable/panel/create_panel_state.ts | 1 -
.../panel/dashboard_panel_placement.ts | 1 +
.../application/lib/update_saved_dashboard.ts | 2 +-
.../saved_objects/dashboard_migrations.ts | 6 +-
.../saved_objects/migrate_match_all_query.ts | 2 +-
.../es_query/es_query/migrate_filter.test.ts | 4 +-
.../data/common/es_query/filters/index.ts | 2 +-
.../common/es_query/filters/range_filter.ts | 6 +-
.../common/es_query/kuery/functions/is.ts | 4 +-
.../field_formats/converters/truncate.ts | 4 +-
.../common/field_formats/field_format.test.ts | 6 +-
.../data/common/field_formats/field_format.ts | 2 +-
.../field_formats/field_formats_registry.ts | 2 +-
.../common/field_mapping/mapping_setup.ts | 2 +-
.../ensure_default_index_pattern.ts | 4 +-
.../index_patterns/flatten_hit.ts | 2 +-
.../index_patterns/index_pattern.test.ts | 10 +-
.../index_patterns/index_pattern.ts | 8 +-
.../query/filter_manager/compare_filters.ts | 2 +-
.../saved_objects_client_wrapper.ts | 11 +-
src/plugins/data/public/public.api.md | 4 +-
.../query/filter_manager/filter_manager.ts | 2 +-
.../filter_manager/lib/generate_filters.ts | 2 +-
.../filter_manager/lib/mappers/map_range.ts | 6 +-
.../data/public/search/aggs/agg_config.ts | 16 +-
.../public/search/aggs/agg_configs.test.ts | 8 +-
.../public/search/aggs/buckets/filters.ts | 2 +-
.../public/search/aggs/buckets/ip_range.ts | 4 +-
.../buckets/lib/time_buckets/time_buckets.ts | 2 +-
.../buckets/migrate_include_exclude_format.ts | 2 +-
.../lib/get_response_agg_config_class.ts | 4 +-
.../search/aggs/metrics/std_deviation.ts | 2 +-
.../public/search/aggs/param_types/json.ts | 2 +-
.../data/public/search/expressions/esaggs.ts | 6 +-
.../search/search_source/search_source.ts | 19 +-
.../ui/search_bar/create_search_bar.tsx | 1 +
.../field_capabilities/field_capabilities.ts | 4 +-
.../fetcher/lib/resolve_time_pattern.ts | 4 +-
.../saved_objects/index_pattern_migrations.ts | 4 +-
.../server/saved_objects/index_patterns.ts | 2 +-
.../data/server/saved_objects/search.ts | 2 +-
.../server/saved_objects/search_migrations.ts | 8 +-
.../angular/context/query/actions.js | 2 +-
.../sidebar/lib/field_calculator.test.ts | 2 +-
.../discover/public/kibana_services.ts | 2 +
.../components/cron_editor/cron_editor.js | 6 +-
.../specs/kibana_context.ts | 4 +-
.../expression_types/specs/datatable.ts | 6 +-
.../specs/kibana_datatable.ts | 4 +-
src/plugins/expressions/public/loader.ts | 3 +-
.../edit_index_pattern/tabs/utils.ts | 4 +-
.../filter_manager/phrase_filter_manager.ts | 6 +-
.../adapters/request/request_adapter.ts | 1 -
.../public/views/data/lib/export_csv.ts | 2 +-
.../table_list_view/table_list_view.tsx | 4 +-
.../common/url/encode_uri_query.ts | 2 +-
.../public/top_nav_menu/top_nav_menu_item.tsx | 6 +-
.../public/lib/create_field_list.ts | 6 +-
.../saved_objects_table_page.tsx | 2 +-
.../public/components/agg_params_helper.ts | 2 +-
.../controls/components/input_list.tsx | 4 +-
.../controls/components/number_list/utils.ts | 2 +-
.../public/components/controls/filters.tsx | 6 +-
.../components/controls/number_interval.tsx | 2 +-
.../components/controls/time_interval.tsx | 2 +-
.../public/components/sidebar/data_tab.tsx | 5 +-
.../vis_default_editor/public/schemas.ts | 1 +
.../components/metric_vis_component.tsx | 5 +-
.../public/helpers/panel_utils.ts | 4 +-
.../public/timelion_vis_fn.ts | 7 +-
.../server/fit_functions/average.js | 2 +-
.../server/handlers/chain_runner.js | 2 +-
.../server/handlers/lib/validate_arg.js | 2 +-
.../vis_type_timelion/server/lib/as_sorted.js | 2 +-
.../server/lib/classes/timelion_function.js | 2 +-
.../server/lib/load_functions.js | 2 +-
.../vis_type_timelion/server/lib/reduce.js | 2 +-
.../server/lib/unzip_pairs.js | 2 +-
.../es/lib/agg_response_to_series_list.js | 2 +-
.../series_functions/es/lib/build_request.js | 2 +-
.../server/series_functions/movingaverage.js | 2 +-
.../server/series_functions/movingstd.js | 2 +-
.../server/series_functions/points.js | 2 +-
.../server/series_functions/static.test.js | 2 +-
.../application/components/aggs/agg_row.tsx | 2 +-
.../components/lib/series_change_handler.js | 2 +-
.../server/lib/get_fields.ts | 4 +-
.../server/saved_objects/tsvb_telemetry.ts | 4 +-
src/plugins/vis_type_vega/public/vega_fn.ts | 7 +-
.../components/options/metrics_axes/utils.ts | 4 +-
.../components/labels/flatten_series.js | 2 +-
.../vislib/components/labels/labels.test.js | 4 +-
.../vislib/components/labels/uniq_labels.js | 2 +-
.../vislib/components/legend/legend.tsx | 4 +-
.../vislib/components/legend/pie_utils.ts | 4 +-
.../components/tooltip/position_tooltip.js | 4 +-
.../vislib/components/tooltip/tooltip.js | 2 +-
.../components/zero_injection/flatten_data.js | 4 +-
.../helpers/point_series/_get_series.ts | 2 +-
.../vis_type_vislib/public/vislib/lib/data.js | 8 +-
.../public/vislib/lib/dispatch.js | 4 +-
.../public/persisted_state/persisted_state.ts | 17 +-
.../saved_visualizations/find_list_items.ts | 2 +-
.../wizard/type_selection/type_selection.tsx | 4 +-
.../saved_objects/visualization_migrations.ts | 58 +-
.../utils/create_visualize_app_state.ts | 6 +-
.../application/utils/migrate_app_state.ts | 2 +-
src/setup_node_env/harden.js | 24 -
.../{patches => harden}/child_process.js | 5 +-
.../setup_node_env/harden/index.js | 6 +-
src/setup_node_env/harden/lodash_template.js | 68 +
src/test_utils/get_url.js | 2 +-
src/test_utils/kbn_server.ts | 2 +-
.../apis/saved_objects/migrations.js | 2 +-
.../apps/management/_import_objects.js | 14 +-
test/functional/services/common/browser.ts | 6 +-
test/harden/lodash_template.js | 181 +++
test/tsconfig.json | 1 -
.../services/visual_testing/visual_testing.ts | 1 -
.../beats/elasticsearch_beats_adapter.ts | 16 +-
...asticsearch_configuration_block_adapter.ts | 4 +-
.../tags/elasticsearch_tags_adapter.ts | 8 +-
.../tokens/elasticsearch_tokens_adapter.ts | 4 +-
x-pack/package.json | 8 +-
.../server/builtin_action_types/case/utils.ts | 6 +-
.../server/usage/actions_usage_collector.ts | 2 +-
x-pack/plugins/alerts/server/alerts_client.ts | 4 +-
.../task_runner/create_execution_handler.ts | 4 +-
.../alerts/server/task_runner/task_runner.ts | 19 +-
.../task_runner/transform_action_params.ts | 4 +-
.../server/usage/alerts_usage_collector.ts | 2 +-
.../util/merge_projection/index.ts | 4 +-
.../ErrorGroupDetails/DetailView/index.tsx | 4 +-
.../ErrorGroupDetails/Distribution/index.tsx | 2 +-
.../ServiceIntegrations/WatcherFlyout.tsx | 4 +-
.../__test__/createErrorGroupWatch.test.ts | 2 +-
.../app/TransactionDetails/index.tsx | 1 -
.../components/shared/ManagedTable/index.tsx | 5 +-
.../components/shared/Stacktrace/index.tsx | 2 +-
.../shared/TransactionActionMenu/sections.ts | 6 +-
.../shared/charts/ErrorRateChart/index.tsx | 2 +-
.../context/LoadingIndicatorContext.tsx | 4 +-
.../context/UrlParamsContext/helpers.ts | 6 +-
.../services/rest/observability_dashboard.ts | 2 +-
...et_service_map_from_trace_ids.test.ts.snap | 222 ++++
.../fetch_service_paths_from_trace_ids.ts | 232 ++++
.../get_service_map_from_trace_ids.test.ts | 28 +
.../get_service_map_from_trace_ids.ts | 293 +----
...ce_map_from_trace_ids_script_response.json | 1165 +++++++++++++++++
.../transform_service_map_responses.ts | 4 +-
.../lib/transaction_groups/transform.ts | 4 +-
.../lib/transactions/breakdown/index.ts | 10 +-
.../lib/ui_filters/local_ui_filters/index.ts | 4 +-
.../apm/server/routes/create_api/index.ts | 1 +
x-pack/plugins/apm/server/routes/services.ts | 4 +-
x-pack/plugins/apm/typings/lodash.mean.d.ts | 10 -
.../public/components/enroll_beats.tsx | 8 +-
.../components/navigation/connected_link.tsx | 2 +
.../components/table/table_type_configs.tsx | 6 +-
.../adapters/beats/memory_beats_adapter.ts | 10 +-
.../beats_management/public/lib/framework.ts | 2 +-
.../canvas/.storybook/webpack.dll.config.js | 1 -
.../canvas_plugin_src/functions/common/pie.ts | 4 +-
.../functions/common/plot/get_tick_hash.ts | 12 +-
.../functions/common/plot/index.ts | 4 +-
.../common/plot/series_style_to_flot.ts | 12 +-
.../functions/server/pointseries/index.ts | 6 +-
.../time_filter/components/time_filter.tsx | 6 +-
.../uis/arguments/palette.tsx | 2 +-
.../canvas_plugin_src/uis/views/plot.js | 4 +-
.../canvas/common/lib/pivot_object_array.ts | 5 +-
.../components/enhance/error_boundary.tsx | 56 +-
.../components/function_form/function_form.js | 1 -
.../public/components/item_grid/item_grid.tsx | 18 +-
.../public/components/workpad_config/index.ts | 8 +-
.../workpad_loader/workpad_loader.js | 4 +-
.../workpad_templates/workpad_templates.tsx | 4 +-
.../series_style/extended_template.tsx | 2 +-
.../canvas/public/functions/filters.ts | 2 +-
x-pack/plugins/canvas/public/lib/keymap.ts | 2 +
.../plugins/canvas/public/lib/modify_path.js | 2 +-
.../lib/template_from_react_component.tsx | 6 +-
.../public/state/selectors/resolved_args.ts | 2 +-
.../canvas/public/state/selectors/workpad.ts | 30 +-
.../collectors/custom_element_collector.ts | 2 +-
.../collectors/workpad_collector.test.ts | 6 +-
.../server/collectors/workpad_collector.ts | 10 +-
.../canvas/server/routes/workpad/update.ts | 6 +-
.../plugins/dashboard_mode/public/plugin.ts | 4 +-
.../providers/kql_query_suggestion/index.ts | 4 +-
.../event_log/server/event_log_service.ts | 1 -
.../server/event_log_start_service.ts | 1 -
.../server/ui_capabilities_for_features.ts | 4 +-
x-pack/plugins/graph/server/routes/explore.ts | 2 +-
.../public/extend_index_management/index.js | 4 +-
.../index_actions_context_menu.js | 8 +-
.../inventory/components/expression.tsx | 2 +-
.../components/expression.tsx | 2 +-
.../components/expression_chart.tsx | 9 +-
.../lib/transform_metrics_explorer_data.ts | 4 +-
.../public/pages/link_to/redirect_to_logs.tsx | 4 +-
.../pages/link_to/redirect_to_node_logs.tsx | 4 +-
.../components/waffle/legend_controls.tsx | 4 +-
.../inventory_view/lib/color_from_value.ts | 18 +-
.../inventory_view/lib/nodes_to_wafflemap.ts | 2 +-
.../metric_detail/components/helpers.ts | 2 +-
.../metrics_explorer/components/chart.tsx | 5 +-
.../evaluate_condition.ts | 2 +-
.../inventory_metric_threshold_executor.ts | 2 +-
...review_inventory_metric_threshold_alert.ts | 2 +-
.../metric_threshold_executor.ts | 8 +-
.../preview_metric_threshold_alert.ts | 9 +-
.../server/lib/snapshot/response_helpers.ts | 2 +-
.../infra/server/routes/ip_to_hostname.ts | 2 +-
.../infra/server/routes/metadata/index.ts | 2 +-
.../server/utils/create_afterkey_handler.ts | 2 +-
.../editor_frame_service/editor_frame/save.ts | 2 +-
.../change_indexpattern.tsx | 1 -
.../indexpattern_datasource/datapanel.tsx | 4 +-
.../dimension_panel/bucket_nesting_editor.tsx | 1 -
.../dimension_panel/popover_editor.tsx | 2 +-
.../indexpattern_suggestions.ts | 14 +-
.../indexpattern_datasource/layerpanel.tsx | 1 -
.../public/indexpattern_datasource/loader.ts | 2 +-
.../operations/operations.ts | 1 -
.../indexpattern_datasource/state_helpers.ts | 2 +-
.../indexpattern_datasource/to_expression.ts | 1 -
.../public/indexpattern_datasource/utils.ts | 1 -
.../xy_visualization/xy_visualization.tsx | 2 +-
.../pipeline_list_item/pipeline_list_item.js | 4 +-
.../logstash/server/models/cluster/cluster.ts | 2 +-
.../server/models/pipeline/pipeline.ts | 10 +-
.../pipeline_list_item/pipeline_list_item.ts | 6 +-
.../update_source_editor.tsx | 1 +
.../categorical_field_meta_popover.tsx | 1 -
.../maps/public/classes/util/data_request.ts | 1 -
.../server/maps_telemetry/maps_telemetry.ts | 12 +-
.../explorer_charts_container_service.js | 2 +-
.../public/application/util/string_utils.js | 2 +-
.../models/data_visualizer/data_visualizer.ts | 10 +-
.../monitoring/public/angular/app_modules.ts | 4 +-
.../public/components/alerts/alerts.js | 4 +-
.../public/components/alerts/map_severity.js | 4 +-
.../shard_allocation/lib/decorate_shards.js | 4 +-
.../monitoring/public/components/logs/logs.js | 6 +-
.../public/lib/ajax_error_handler.tsx | 4 +-
.../monitoring/public/lib/form_validation.ts | 4 +-
.../monitoring/public/lib/route_init.js | 2 +-
.../monitoring/public/lib/setup_mode.tsx | 4 +-
.../monitoring/public/services/license.js | 4 +-
.../monitoring/server/lib/apm/get_apm_info.js | 6 +-
.../monitoring/server/lib/apm/get_apms.js | 6 +-
.../server/lib/beats/_beats_stats.js | 4 +-
.../server/lib/beats/get_beat_summary.js | 6 +-
.../monitoring/server/lib/beats/get_beats.js | 6 +-
.../server/lib/beats/get_latest_stats.js | 4 +-
.../server/lib/elasticsearch/get_ml_jobs.js | 4 +-
.../nodes/get_nodes/map_nodes_metrics.js | 10 +-
.../nodes/get_nodes/sort_nodes.js | 4 +-
.../server/lib/logstash/sort_pipelines.js | 4 +-
.../lib/tasks/visualizations/task_runner.ts | 2 +-
.../panel_actions/get_csv_panel_action.tsx | 1 +
.../chromium/driver/chromium_driver.ts | 4 +-
.../reporting/server/config/create_config.ts | 4 +-
.../execute_job/omit_blacklisted_headers.ts | 4 +-
.../csv/server/lib/flatten_hit.ts | 4 +-
.../server/usage/get_reporting_usage.ts | 20 +-
.../sections/job_create/job_create.js | 5 +-
.../sections/job_create/steps/step_metrics.js | 2 +-
.../sections/job_create/steps_config/index.js | 4 +-
.../job_create_clone.test.js | 5 +-
.../job_create_date_histogram.test.js | 5 +-
.../job_create_histogram.test.js | 5 +-
.../job_create_logistics.test.js | 5 +-
.../job_create_metrics.test.js | 5 +-
.../job_create_review.test.js | 5 +-
.../job_create_terms.test.js | 5 +-
.../client_integration/job_list_clone.test.js | 5 +-
.../rollup/server/collectors/register.ts | 8 +-
.../rollup_search_strategy.ts | 4 +-
.../register_fields_for_wildcard_route.ts | 4 +-
.../components/profile_tree/init_data.ts | 4 +-
.../components/profile_tree/unsafe_utils.ts | 8 +-
.../feature_table/change_all_privileges.tsx | 4 +-
.../kibana/feature_table/feature_table.tsx | 1 -
.../privilege_display.tsx | 2 +-
.../privilege_space_table.tsx | 1 -
.../roles/model/kibana_privilege.ts | 2 +-
.../change_password_form.tsx | 1 +
.../register_privileges_with_cluster.ts | 4 +-
x-pack/plugins/security_solution/package.json | 2 -
.../extract_tactics_techniques_mitre.js | 1 +
.../restore_table/restore_table.tsx | 4 +-
.../routes/api/external/copy_to_space.ts | 2 +-
.../server/lib/get_template_version.ts | 8 +-
.../plugins/task_manager/server/task_pool.ts | 4 +-
.../task_manager/server/task_runner.ts | 2 +-
.../task_manager/server/task_store.test.ts | 4 +-
.../plugins/task_manager/server/task_store.ts | 2 +-
.../lib/check_action_type_enabled.tsx | 6 +-
.../components/alert_details.tsx | 4 +-
.../components/alert_instances.tsx | 4 +-
.../public/common/index_controls/index.ts | 3 +-
.../components/tabs/checkup/constants.tsx | 4 +-
.../__tests__/integration_group.test.tsx | 95 +-
.../actions_popover/integration_group.tsx | 29 +-
.../monitor_status_list.tsx | 8 +-
.../monitor_list_status_column.tsx | 6 +-
.../framework/new_platform_adapter.tsx | 3 +-
.../observability_integration/build_href.ts | 2 +-
.../__tests__/enrich_monitor_groups.test.ts | 98 ++
.../requests/search/enrich_monitor_groups.ts | 46 +-
.../threshold_watch_edit.tsx | 1 +
.../models/execute_details/execute_details.js | 6 +-
.../lib/get_watch_type/get_watch_type.js | 4 +-
.../apis/metrics_ui/metrics.ts | 6 +-
.../apis/metrics_ui/metrics_explorer.ts | 10 +-
.../apis/metrics_ui/snapshot.ts | 28 +-
.../apis/security/privileges.ts | 4 +-
.../apis/security/privileges_basic.ts | 4 +-
.../trial/tests/annotations.ts | 2 +-
.../apps/lens/persistent_context.ts | 1 -
.../test/functional/apps/lens/smokescreen.ts | 1 -
.../apps/security/doc_level_security_roles.js | 6 +-
.../apps/security/field_level_security.js | 10 +-
.../functional/apps/security/rbac_phase1.js | 6 +-
.../apps/security/secure_roles_perm.js | 4 +-
.../functional/apps/security/user_email.js | 4 +-
x-pack/test/functional/apps/security/users.js | 12 +-
.../functional/apps/watcher/watcher_test.js | 4 +-
.../apps/triggers_actions_ui/details.ts | 2 +-
.../task_manager_performance/package.json | 1 -
.../task_manager_performance/server/plugin.ts | 4 +-
x-pack/typings/index.d.ts | 5 -
yarn.lock | 74 +-
413 files changed, 3180 insertions(+), 1354 deletions(-)
delete mode 100644 src/setup_node_env/harden.js
rename src/setup_node_env/{patches => harden}/child_process.js (97%)
rename typings/lodash.topath/index.d.ts => src/setup_node_env/harden/index.js (87%)
create mode 100644 src/setup_node_env/harden/lodash_template.js
create mode 100644 test/harden/lodash_template.js
create mode 100644 x-pack/plugins/apm/server/lib/service_map/__snapshots__/get_service_map_from_trace_ids.test.ts.snap
create mode 100644 x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts
create mode 100644 x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.test.ts
create mode 100644 x-pack/plugins/apm/server/lib/service_map/mock_responses/get_service_map_from_trace_ids_script_response.json
delete mode 100644 x-pack/plugins/apm/typings/lodash.mean.d.ts
create mode 100644 x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md
index 5fa7d4841537b..48ec9456c56dd 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md
@@ -12,14 +12,14 @@ Serialize this format to a simple POJO, with only the params that are not defaul
```typescript
toJSON(): {
- id: unknown;
- params: _.Dictionary | undefined;
+ id: any;
+ params: any;
};
```
Returns:
`{
- id: unknown;
- params: _.Dictionary | undefined;
+ id: any;
+ params: any;
}`
diff --git a/package.json b/package.json
index b1dd8686f818b..3c15d9ee3c97b 100644
--- a/package.json
+++ b/package.json
@@ -86,8 +86,9 @@
"**/@types/angular": "^1.6.56",
"**/@types/hoist-non-react-statics": "^3.3.1",
"**/@types/chai": "^4.2.11",
+ "**/cypress/@types/lodash": "^4.14.155",
"**/typescript": "3.9.5",
- "**/graphql-toolkit/lodash": "^4.17.13",
+ "**/graphql-toolkit/lodash": "^4.17.15",
"**/hoist-non-react-statics": "^3.3.2",
"**/isomorphic-git/**/base64-js": "^1.2.1",
"**/image-diff/gm/debug": "^2.6.9",
@@ -213,8 +214,7 @@
"leaflet.heat": "0.2.0",
"less": "npm:@elastic/less@2.7.3-kibana",
"less-loader": "5.0.0",
- "lodash": "npm:@elastic/lodash@3.10.1-kibana4",
- "lodash.clonedeep": "^4.5.0",
+ "lodash": "^4.17.15",
"lru-cache": "4.1.5",
"markdown-it": "^10.0.0",
"mini-css-extract-plugin": "0.8.0",
@@ -355,8 +355,7 @@
"@types/json5": "^0.0.30",
"@types/license-checker": "15.0.0",
"@types/listr": "^0.14.0",
- "@types/lodash": "^3.10.1",
- "@types/lodash.clonedeep": "^4.5.4",
+ "@types/lodash": "^4.14.155",
"@types/lru-cache": "^5.1.0",
"@types/markdown-it": "^0.0.7",
"@types/minimatch": "^2.0.29",
diff --git a/packages/kbn-config-schema/package.json b/packages/kbn-config-schema/package.json
index 015dca128ce91..10b607dcd4312 100644
--- a/packages/kbn-config-schema/package.json
+++ b/packages/kbn-config-schema/package.json
@@ -14,6 +14,7 @@
"tsd": "^0.7.4"
},
"peerDependencies": {
+ "lodash": "^4.17.15",
"joi": "^13.5.2",
"moment": "^2.24.0",
"type-detect": "^4.0.8"
diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json
index ea72a4a48caeb..c6bb06e68b9c0 100644
--- a/packages/kbn-interpreter/package.json
+++ b/packages/kbn-interpreter/package.json
@@ -11,8 +11,7 @@
"dependencies": {
"@babel/runtime": "^7.10.2",
"@kbn/i18n": "1.0.0",
- "lodash": "npm:@elastic/lodash@3.10.1-kibana4",
- "lodash.clone": "^4.5.0",
+ "lodash": "^4.17.15",
"uuid": "3.3.2"
},
"devDependencies": {
diff --git a/packages/kbn-interpreter/src/common/lib/registry.js b/packages/kbn-interpreter/src/common/lib/registry.js
index 25b122f400711..16572cf494cd3 100644
--- a/packages/kbn-interpreter/src/common/lib/registry.js
+++ b/packages/kbn-interpreter/src/common/lib/registry.js
@@ -17,7 +17,7 @@
* under the License.
*/
-import clone from 'lodash.clone';
+import { clone } from 'lodash';
export class Registry {
constructor(prop = 'name') {
diff --git a/packages/kbn-plugin-generator/index.js b/packages/kbn-plugin-generator/index.js
index e61037e42d63f..398b49fa1ecd5 100644
--- a/packages/kbn-plugin-generator/index.js
+++ b/packages/kbn-plugin-generator/index.js
@@ -23,7 +23,7 @@ const dedent = require('dedent');
const sao = require('sao');
const chalk = require('chalk');
const getopts = require('getopts');
-const snakeCase = require('lodash.snakecase');
+const { snakeCase } = require('lodash');
exports.run = function run(argv) {
const options = getopts(argv, {
@@ -41,7 +41,7 @@ exports.run = function run(argv) {
if (options.help) {
console.log(
dedent(chalk`
- # {dim Usage:}
+ # {dim Usage:}
node scripts/generate-plugin {bold [name]}
Generate a fresh Kibana plugin in the plugins/ directory
`) + '\n'
diff --git a/packages/kbn-plugin-generator/package.json b/packages/kbn-plugin-generator/package.json
index b9df67b32e5d3..5c1e98cd869de 100644
--- a/packages/kbn-plugin-generator/package.json
+++ b/packages/kbn-plugin-generator/package.json
@@ -8,10 +8,7 @@
"dedent": "^0.7.0",
"execa": "^4.0.2",
"getopts": "^2.2.4",
- "lodash.camelcase": "^4.3.0",
- "lodash.kebabcase": "^4.1.1",
- "lodash.snakecase": "^4.1.1",
- "lodash.startcase": "^4.4.0",
+ "lodash": "^4.17.15",
"sao": "^0.22.12"
}
}
diff --git a/packages/kbn-plugin-generator/sao_template/sao.js b/packages/kbn-plugin-generator/sao_template/sao.js
index 7fc29b1e6bd0a..dc4d8a2fc10fb 100755
--- a/packages/kbn-plugin-generator/sao_template/sao.js
+++ b/packages/kbn-plugin-generator/sao_template/sao.js
@@ -20,9 +20,7 @@
const { relative, resolve } = require('path');
const fs = require('fs');
-const startCase = require('lodash.startcase');
-const camelCase = require('lodash.camelcase');
-const snakeCase = require('lodash.snakecase');
+const { camelCase, startCase, snakeCase } = require('lodash');
const chalk = require('chalk');
const execa = require('execa');
diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json
index 3e7ed49c61314..188db0a8321a2 100644
--- a/packages/kbn-pm/package.json
+++ b/packages/kbn-pm/package.json
@@ -22,7 +22,7 @@
"@types/glob": "^5.0.35",
"@types/globby": "^6.1.0",
"@types/has-ansi": "^3.0.0",
- "@types/lodash.clonedeepwith": "^4.5.3",
+ "@types/lodash": "^4.14.155",
"@types/log-symbols": "^2.0.0",
"@types/ncp": "^2.0.1",
"@types/node": ">=10.17.17 <10.20.0",
@@ -46,7 +46,7 @@
"globby": "^8.0.1",
"has-ansi": "^3.0.0",
"is-path-inside": "^3.0.2",
- "lodash.clonedeepwith": "^4.5.0",
+ "lodash": "^4.17.15",
"log-symbols": "^2.2.0",
"multimatch": "^4.0.0",
"ncp": "^2.0.0",
diff --git a/packages/kbn-pm/src/test_helpers/absolute_path_snapshot_serializer.ts b/packages/kbn-pm/src/test_helpers/absolute_path_snapshot_serializer.ts
index 96ce6fd1d919a..cf4ecbb4ad42c 100644
--- a/packages/kbn-pm/src/test_helpers/absolute_path_snapshot_serializer.ts
+++ b/packages/kbn-pm/src/test_helpers/absolute_path_snapshot_serializer.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import cloneDeepWith from 'lodash.clonedeepwith';
+import { cloneDeepWith } from 'lodash';
import { resolve, sep as pathSep } from 'path';
const repoRoot = resolve(__dirname, '../../../../');
diff --git a/packages/kbn-storybook/lib/webpack.dll.config.js b/packages/kbn-storybook/lib/webpack.dll.config.js
index 534f503e2956a..740ee3819c36f 100644
--- a/packages/kbn-storybook/lib/webpack.dll.config.js
+++ b/packages/kbn-storybook/lib/webpack.dll.config.js
@@ -54,7 +54,6 @@ module.exports = {
'highlight.js',
'html-entities',
'jquery',
- 'lodash.clone',
'lodash',
'markdown-it',
'mocha',
diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json
index 042de2617565e..0c49ccf276b2b 100644
--- a/packages/kbn-test/package.json
+++ b/packages/kbn-test/package.json
@@ -14,6 +14,7 @@
"@kbn/babel-preset": "1.0.0",
"@kbn/dev-utils": "1.0.0",
"@types/joi": "^13.4.2",
+ "@types/lodash": "^4.14.155",
"@types/parse-link-header": "^1.0.0",
"@types/puppeteer": "^3.0.0",
"@types/strip-ansi": "^5.2.1",
@@ -28,6 +29,7 @@
"getopts": "^2.2.4",
"glob": "^7.1.2",
"joi": "^13.5.2",
+ "lodash": "^4.17.15",
"parse-link-header": "^1.0.1",
"puppeteer": "^3.3.0",
"rxjs": "^6.5.5",
diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts
index e38520f00e45b..687a0e87d4c68 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts
@@ -18,10 +18,7 @@
*/
import { Schema } from 'joi';
-import { cloneDeep, get, has } from 'lodash';
-
-// @ts-ignore internal lodash module is not typed
-import toPath from 'lodash/internal/toPath';
+import { cloneDeepWith, get, has, toPath } from 'lodash';
import { schema } from './schema';
@@ -114,7 +111,7 @@ export class Config {
throw new Error(`Unknown config key "${key}"`);
}
- return cloneDeep(get(this[$values], key, defaultValue), (v) => {
+ return cloneDeepWith(get(this[$values], key, defaultValue), (v) => {
if (typeof v === 'function') {
return v;
}
@@ -122,7 +119,7 @@ export class Config {
}
public getAll() {
- return cloneDeep(this[$values], (v) => {
+ return cloneDeepWith(this[$values], (v) => {
if (typeof v === 'function') {
return v;
}
diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js
index f795b32d78b8e..2d4c461cc2c2e 100644
--- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js
+++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js
@@ -19,8 +19,7 @@
import { resolve } from 'path';
import { format } from 'url';
-import { get } from 'lodash';
-import toPath from 'lodash/internal/toPath';
+import { get, toPath } from 'lodash';
import { Cluster } from '@kbn/es';
import { CI_PARALLEL_PROCESS_PREFIX } from '../ci_parallel_process_prefix';
import { esTestConfig } from './es_test_config';
diff --git a/packages/kbn-test/src/page_load_metrics/navigation.ts b/packages/kbn-test/src/page_load_metrics/navigation.ts
index 21dc681951b21..db53df789ac69 100644
--- a/packages/kbn-test/src/page_load_metrics/navigation.ts
+++ b/packages/kbn-test/src/page_load_metrics/navigation.ts
@@ -19,7 +19,6 @@
import Fs from 'fs';
import Url from 'url';
-import _ from 'lodash';
import puppeteer from 'puppeteer';
import { resolve } from 'path';
import { ToolingLog } from '@kbn/dev-utils';
diff --git a/packages/kbn-ui-framework/Gruntfile.js b/packages/kbn-ui-framework/Gruntfile.js
index 177fd1f153155..b7ba1e87b2f00 100644
--- a/packages/kbn-ui-framework/Gruntfile.js
+++ b/packages/kbn-ui-framework/Gruntfile.js
@@ -21,7 +21,7 @@ const sass = require('node-sass');
const postcss = require('postcss');
const postcssConfig = require('../../src/optimize/postcss.config');
const chokidar = require('chokidar');
-const debounce = require('lodash/function/debounce');
+const { debounce } = require('lodash');
const platform = require('os').platform();
const isPlatformWindows = /^win/.test(platform);
diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json
index 4da4fb21fbed5..abf64906e0253 100644
--- a/packages/kbn-ui-framework/package.json
+++ b/packages/kbn-ui-framework/package.json
@@ -17,7 +17,7 @@
"dependencies": {
"classnames": "2.2.6",
"focus-trap-react": "^3.1.1",
- "lodash": "npm:@elastic/lodash@3.10.1-kibana4",
+ "lodash": "^4.17.15",
"prop-types": "15.6.0",
"react": "^16.12.0",
"react-ace": "^5.9.0",
diff --git a/renovate.json5 b/renovate.json5
index 49a255d60f29e..5a807b4b090c1 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -557,22 +557,6 @@
'@types/lodash',
],
},
- {
- groupSlug: 'lodash.clonedeep',
- groupName: 'lodash.clonedeep related packages',
- packageNames: [
- 'lodash.clonedeep',
- '@types/lodash.clonedeep',
- ],
- },
- {
- groupSlug: 'lodash.clonedeepwith',
- groupName: 'lodash.clonedeepwith related packages',
- packageNames: [
- 'lodash.clonedeepwith',
- '@types/lodash.clonedeepwith',
- ],
- },
{
groupSlug: 'log-symbols',
groupName: 'log-symbols related packages',
diff --git a/src/apm.js b/src/apm.js
index 6c10539c6b7d3..effa6c77d7614 100644
--- a/src/apm.js
+++ b/src/apm.js
@@ -20,7 +20,7 @@
const { join } = require('path');
const { readFileSync } = require('fs');
const { execSync } = require('child_process');
-const merge = require('lodash.merge');
+const { merge } = require('lodash');
const { name, version, build } = require('../package.json');
const ROOT_DIR = join(__dirname, '..');
diff --git a/src/cli/cluster/cluster_manager.test.ts b/src/cli/cluster/cluster_manager.test.ts
index 66f68f815edac..2ddccae2fada6 100644
--- a/src/cli/cluster/cluster_manager.test.ts
+++ b/src/cli/cluster/cluster_manager.test.ts
@@ -93,7 +93,7 @@ describe('CLI cluster manager', () => {
}
const football = {};
- const messenger = sample(manager.workers);
+ const messenger = sample(manager.workers) as any;
messenger.emit('broadcast', football);
for (const worker of manager.workers) {
diff --git a/src/cli/cluster/worker.ts b/src/cli/cluster/worker.ts
index dc6e6d5676651..097a549187429 100644
--- a/src/cli/cluster/worker.ts
+++ b/src/cli/cluster/worker.ts
@@ -177,7 +177,7 @@ export class Worker extends EventEmitter {
}
flushChangeBuffer() {
- const files = _.unique(this.changes.splice(0));
+ const files = _.uniq(this.changes.splice(0));
const prefix = files.length > 1 ? '\n - ' : '';
return files.reduce(function (list, file) {
return `${list || ''}${prefix}"${file}"`;
diff --git a/src/cli/help.js b/src/cli/help.js
index 656944d85b254..0170cb53e19df 100644
--- a/src/cli/help.js
+++ b/src/cli/help.js
@@ -72,7 +72,7 @@ function commandsSummary(program) {
}, 0);
return cmds.reduce(function (help, cmd) {
- return `${help || ''}${_.padRight(cmd[0], cmdLColWidth)} ${cmd[1] || ''}\n`;
+ return `${help || ''}${_.padEnd(cmd[0], cmdLColWidth)} ${cmd[1] || ''}\n`;
}, '');
}
diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts
index bf9b4235e9444..e31094d96f3d4 100644
--- a/src/core/public/http/fetch.ts
+++ b/src/core/public/http/fetch.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { merge } from 'lodash';
+import { omitBy } from 'lodash';
import { format } from 'url';
import { BehaviorSubject } from 'rxjs';
@@ -42,6 +42,10 @@ interface Params {
const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/;
const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/;
+const removedUndefined = (obj: Record | undefined) => {
+ return omitBy(obj, (v) => v === undefined);
+};
+
export class Fetch {
private readonly interceptors = new Set();
private readonly requestCount$ = new BehaviorSubject(0);
@@ -119,24 +123,23 @@ export class Fetch {
asResponse,
asSystemRequest,
...fetchOptions
- } = merge(
- {
- method: 'GET',
- credentials: 'same-origin',
- prependBasePath: true,
- },
- options,
- {
- headers: {
- 'Content-Type': 'application/json',
- ...options.headers,
- 'kbn-version': this.params.kibanaVersion,
- },
- }
- );
+ } = {
+ method: 'GET',
+ credentials: 'same-origin',
+ prependBasePath: true,
+ ...options,
+ // options can pass an `undefined` Content-Type to erase the default value.
+ // however we can't pass it to `fetch` as it will send an `Content-Type: Undefined` header
+ headers: removedUndefined({
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ 'kbn-version': this.params.kibanaVersion,
+ }),
+ };
+
const url = format({
pathname: shouldPrependBasePath ? this.params.basePath.prepend(options.path) : options.path,
- query,
+ query: removedUndefined(query),
});
// Make sure the system request header is only present if `asSystemRequest` is true.
@@ -144,7 +147,7 @@ export class Fetch {
fetchOptions.headers['kbn-system-request'] = 'true';
}
- return new Request(url, fetchOptions);
+ return new Request(url, fetchOptions as RequestInit);
}
private async fetchResponse(fetchOptions: HttpFetchOptionsWithPath): Promise> {
diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts
index cb8671ba37a6c..7dc5f3655fca0 100644
--- a/src/core/public/plugins/plugins_service.test.ts
+++ b/src/core/public/plugins/plugins_service.test.ts
@@ -91,7 +91,7 @@ describe('PluginsService', () => {
context: contextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
http: httpServiceMock.createSetupContract(),
- injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'),
+ injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createSetupContract(),
uiSettings: uiSettingsServiceMock.createSetupContract(),
};
@@ -99,6 +99,7 @@ describe('PluginsService', () => {
...mockSetupDeps,
application: expect.any(Object),
getStartServices: expect.any(Function),
+ injectedMetadata: pick(mockSetupDeps.injectedMetadata, 'getInjectedVar'),
};
mockStartDeps = {
application: applicationServiceMock.createInternalStartContract(),
@@ -106,7 +107,7 @@ describe('PluginsService', () => {
http: httpServiceMock.createStartContract(),
chrome: chromeServiceMock.createStartContract(),
i18n: i18nServiceMock.createStartContract(),
- injectedMetadata: pick(injectedMetadataServiceMock.createStartContract(), 'getInjectedVar'),
+ injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
overlays: overlayServiceMock.createStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
@@ -117,6 +118,7 @@ describe('PluginsService', () => {
...mockStartDeps,
application: expect.any(Object),
chrome: omit(mockStartDeps.chrome, 'getComponent'),
+ injectedMetadata: pick(mockStartDeps.injectedMetadata, 'getInjectedVar'),
};
// Reset these for each test.
diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts
index cb279b2cc4c8f..c4daaf5d7f307 100644
--- a/src/core/public/saved_objects/saved_objects_client.ts
+++ b/src/core/public/saved_objects/saved_objects_client.ts
@@ -162,7 +162,9 @@ export class SavedObjectsClient {
});
if (!foundObject) {
- return queueItem.resolve(this.createSavedObject(pick(queueItem, ['id', 'type'])));
+ return queueItem.resolve(
+ this.createSavedObject(pick(queueItem, ['id', 'type']) as SavedObject)
+ );
}
queueItem.resolve(foundObject);
diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts
index d3ba506b865a4..165ef98be91d4 100644
--- a/src/core/public/saved_objects/simple_saved_object.ts
+++ b/src/core/public/saved_objects/simple_saved_object.ts
@@ -60,7 +60,7 @@ export class SimpleSavedObject {
}
public set(key: string, value: any): T {
- return set(this.attributes, key, value);
+ return set(this.attributes as any, key, value);
}
public has(key: string): boolean {
diff --git a/src/core/server/capabilities/merge_capabilities.ts b/src/core/server/capabilities/merge_capabilities.ts
index 95296346ad835..06869089598a9 100644
--- a/src/core/server/capabilities/merge_capabilities.ts
+++ b/src/core/server/capabilities/merge_capabilities.ts
@@ -17,11 +17,11 @@
* under the License.
*/
-import { merge } from 'lodash';
+import { mergeWith } from 'lodash';
import { Capabilities } from './types';
export const mergeCapabilities = (...sources: Array>): Capabilities =>
- merge({}, ...sources, (a: any, b: any) => {
+ mergeWith({}, ...sources, (a: any, b: any) => {
if (
(typeof a === 'boolean' && typeof b === 'object') ||
(typeof a === 'object' && typeof b === 'boolean')
diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts
index 483534e0c145b..715f5b883139f 100644
--- a/src/core/server/config/deprecation/core_deprecations.ts
+++ b/src/core/server/config/deprecation/core_deprecations.ts
@@ -39,10 +39,7 @@ const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
};
const xsrfDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
- if (
- has(settings, 'server.xsrf.whitelist') &&
- get(settings, 'server.xsrf.whitelist').length > 0
- ) {
+ if ((settings.server?.xsrf?.whitelist ?? []).length > 0) {
log(
'It is not recommended to disable xsrf protections for API endpoints via [server.xsrf.whitelist]. ' +
'It will be removed in 8.0 release. Instead, supply the "kbn-xsrf" header.'
diff --git a/src/core/server/elasticsearch/legacy/errors.ts b/src/core/server/elasticsearch/legacy/errors.ts
index f81903d76547a..3b3b8da51a907 100644
--- a/src/core/server/elasticsearch/legacy/errors.ts
+++ b/src/core/server/elasticsearch/legacy/errors.ts
@@ -81,7 +81,7 @@ export class LegacyElasticsearchErrorHelpers {
public static decorateNotAuthorizedError(error: Error, reason?: string) {
const decoratedError = decorate(error, ErrorCode.NOT_AUTHORIZED, 401, reason);
- const wwwAuthHeader = get(error, 'body.error.header[WWW-Authenticate]');
+ const wwwAuthHeader = get(error, 'body.error.header[WWW-Authenticate]') as string;
decoratedError.output.headers['WWW-Authenticate'] =
wwwAuthHeader || 'Basic realm="Authorization Required"';
diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts
index ffbdabadd03f7..eccc9d013176c 100644
--- a/src/core/server/http/base_path_proxy_server.ts
+++ b/src/core/server/http/base_path_proxy_server.ts
@@ -24,7 +24,7 @@ import apm from 'elastic-apm-node';
import { ByteSizeValue } from '@kbn/config-schema';
import { Server, Request, ResponseToolkit } from 'hapi';
import HapiProxy from 'h2o2';
-import { sample } from 'lodash';
+import { sampleSize } from 'lodash';
import BrowserslistUserAgent from 'browserslist-useragent';
import * as Rx from 'rxjs';
import { take } from 'rxjs/operators';
@@ -90,7 +90,7 @@ export class BasePathProxyServer {
httpConfig.maxPayload = new ByteSizeValue(ONE_GIGABYTE);
if (!httpConfig.basePath) {
- httpConfig.basePath = `/${sample(alphabet, 3).join('')}`;
+ httpConfig.basePath = `/${sampleSize(alphabet, 3).join('')}`;
}
}
diff --git a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
index 3b16bed92df97..4a6d86a0dfba6 100644
--- a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
+++ b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap
@@ -119,7 +119,10 @@ Object {
exports[`#set correctly sets values for paths that do not exist. 1`] = `
Object {
- "unknown": "value",
+ "unknown": Object {
+ "sub1": "sub-value-1",
+ "sub2": "sub-value-2",
+ },
}
`;
diff --git a/src/core/server/saved_objects/mappings/lib/get_property.ts b/src/core/server/saved_objects/mappings/lib/get_property.ts
index a31c9fe0c3ba1..91b2b1239fc55 100644
--- a/src/core/server/saved_objects/mappings/lib/get_property.ts
+++ b/src/core/server/saved_objects/mappings/lib/get_property.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import toPath from 'lodash/internal/toPath';
+import { toPath } from 'lodash';
import { SavedObjectsCoreFieldMapping, SavedObjectsFieldMapping, IndexMapping } from '../types';
function getPropertyMappingFromObjectMapping(
diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts
index 376f823267ebe..07675bb0a6819 100644
--- a/src/core/server/saved_objects/migrations/core/document_migrator.ts
+++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts
@@ -62,7 +62,6 @@
import Boom from 'boom';
import _ from 'lodash';
-import cloneDeep from 'lodash.clonedeep';
import Semver from 'semver';
import { Logger } from '../../../logging';
import { SavedObjectUnsanitizedDoc } from '../../serialization';
@@ -151,7 +150,7 @@ export class DocumentMigrator implements VersionedTransformer {
// Clone the document to prevent accidental mutations on the original data
// Ex: Importing sample data that is cached at import level, migrations would
// execute on mutated data the second time.
- const clonedDoc = cloneDeep(doc);
+ const clonedDoc = _.cloneDeep(doc);
return this.transformDoc(clonedDoc);
};
}
@@ -220,7 +219,7 @@ function buildActiveMigrations(
return {
...migrations,
[type.name]: {
- latestVersion: _.last(transforms).version,
+ latestVersion: _.last(transforms)!.version,
transforms,
},
};
diff --git a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts
index 3f2c31a7c0e5c..2d27ca7c8a29b 100644
--- a/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/migration_coordinator.test.ts
@@ -17,7 +17,6 @@
* under the License.
*/
-import _ from 'lodash';
import { coordinateMigration } from './migration_coordinator';
import { createSavedObjectsMigrationLoggerMock } from '../mocks';
diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts
index f24195c0f295e..880b71e164b5b 100644
--- a/src/core/server/saved_objects/service/lib/repository.ts
+++ b/src/core/server/saved_objects/service/lib/repository.ts
@@ -1346,7 +1346,7 @@ export class SavedObjectsRepository {
// method transparently to the specified namespace.
private _rawToSavedObject(raw: SavedObjectsRawDoc): SavedObject {
const savedObject = this._serializer.rawToSavedObject(raw);
- return omit(savedObject, 'namespace');
+ return omit(savedObject, 'namespace') as SavedObject;
}
/**
diff --git a/src/core/utils/deep_freeze.test.ts b/src/core/utils/deep_freeze.test.ts
index 58aa9c9b8c92b..48f890160d05d 100644
--- a/src/core/utils/deep_freeze.test.ts
+++ b/src/core/utils/deep_freeze.test.ts
@@ -32,7 +32,8 @@ it('returns the first argument with all original references', () => {
it('prevents adding properties to argument', () => {
const frozen = deepFreeze({});
expect(() => {
- // @ts-expect-error ts knows this shouldn't be possible, but just making sure
+ // ts knows this shouldn't be possible, but just making sure
+ // @ts-expect-error
frozen.foo = true;
}).toThrowError(`object is not extensible`);
});
@@ -40,7 +41,8 @@ it('prevents adding properties to argument', () => {
it('prevents changing properties on argument', () => {
const frozen = deepFreeze({ foo: false });
expect(() => {
- // @ts-expect-error ts knows this shouldn't be possible, but just making sure
+ // ts knows this shouldn't be possible, but just making sure
+ // @ts-expect-error
frozen.foo = true;
}).toThrowError(`read only property 'foo'`);
});
@@ -48,7 +50,8 @@ it('prevents changing properties on argument', () => {
it('prevents changing properties on nested children of argument', () => {
const frozen = deepFreeze({ foo: { bar: { baz: { box: 1 } } } });
expect(() => {
- // @ts-expect-error ts knows this shouldn't be possible, but just making sure
+ // ts knows this shouldn't be possible, but just making sure
+ // @ts-expect-error
frozen.foo.bar.baz.box = 2;
}).toThrowError(`read only property 'box'`);
});
@@ -56,7 +59,8 @@ it('prevents changing properties on nested children of argument', () => {
it('prevents adding items to a frozen array', () => {
const frozen = deepFreeze({ foo: [1] });
expect(() => {
- // @ts-expect-error ts knows this shouldn't be possible, but just making sure
+ // ts knows this shouldn't be possible, but just making sure
+ // @ts-expect-error
frozen.foo.push(2);
}).toThrowError(`object is not extensible`);
});
@@ -64,7 +68,8 @@ it('prevents adding items to a frozen array', () => {
it('prevents reassigning items in a frozen array', () => {
const frozen = deepFreeze({ foo: [1] });
expect(() => {
- // @ts-expect-error ts knows this shouldn't be possible, but just making sure
+ // ts knows this shouldn't be possible, but just making sure
+ // @ts-expect-error
frozen.foo[0] = 2;
}).toThrowError(`read only property '0'`);
});
diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js
index bc3a6265cc582..cec80dd547a53 100644
--- a/src/dev/precommit_hook/casing_check_config.js
+++ b/src/dev/precommit_hook/casing_check_config.js
@@ -98,7 +98,6 @@ export const IGNORE_DIRECTORY_GLOBS = [
'packages/*',
'packages/kbn-ui-framework/generator-kui',
'src/legacy/ui/public/flot-charts',
- 'src/legacy/ui/public/utils/lodash-mixins',
'test/functional/fixtures/es_archiver/visualize_source-filters',
'packages/kbn-pm/src/utils/__fixtures__/*',
'x-pack/dev-tools',
diff --git a/src/dev/sass/build_sass.js b/src/dev/sass/build_sass.js
index 7075bcf55adf5..68058043477d0 100644
--- a/src/dev/sass/build_sass.js
+++ b/src/dev/sass/build_sass.js
@@ -23,11 +23,11 @@ import * as Rx from 'rxjs';
import { toArray } from 'rxjs/operators';
import { createFailError } from '@kbn/dev-utils';
+import { debounce } from 'lodash';
import { findPluginSpecs } from '../../legacy/plugin_discovery';
import { collectUiExports } from '../../legacy/ui';
import { buildAll } from '../../legacy/server/sass/build_all';
import chokidar from 'chokidar';
-import debounce from 'lodash/function/debounce';
// TODO: clintandrewhall - Extract and use FSWatcher from legacy/server/sass
const build = async ({ log, kibanaDir, styleSheetPaths, watch }) => {
diff --git a/src/fixtures/agg_resp/geohash_grid.js b/src/fixtures/agg_resp/geohash_grid.js
index 0e576a88ab36a..fde1e54b0661d 100644
--- a/src/fixtures/agg_resp/geohash_grid.js
+++ b/src/fixtures/agg_resp/geohash_grid.js
@@ -44,7 +44,7 @@ export default function GeoHashGridAggResponseFixture() {
// random number of tags
let docCount = 0;
const buckets = _.times(_.random(40, 200), function () {
- return _.sample(geoHashCharts, 3).join('');
+ return _.sampleSize(geoHashCharts, 3).join('');
})
.sort()
.map(function (geoHash) {
diff --git a/src/legacy/core_plugins/console_legacy/index.ts b/src/legacy/core_plugins/console_legacy/index.ts
index c588b941112d1..82e00a99c6cfd 100644
--- a/src/legacy/core_plugins/console_legacy/index.ts
+++ b/src/legacy/core_plugins/console_legacy/index.ts
@@ -41,7 +41,7 @@ export default function (kibana: any) {
uiExports: {
injectDefaultVars: () => ({
elasticsearchUrl: url.format(
- Object.assign(url.parse(head(_legacyEsConfig.hosts)), { auth: false })
+ Object.assign(url.parse(head(_legacyEsConfig.hosts) as any), { auth: false })
),
}),
},
diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js b/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js
index fc4ff512e2bd5..d76b2a2aa9364 100644
--- a/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js
+++ b/src/legacy/core_plugins/elasticsearch/server/lib/handle_es_error.js
@@ -35,7 +35,7 @@ export function handleESError(error) {
return Boom.serverUnavailable(error);
} else if (
error instanceof esErrors.Conflict ||
- _.contains(error.message, 'index_template_already_exists')
+ _.includes(error.message, 'index_template_already_exists')
) {
return Boom.conflict(error);
} else if (error instanceof esErrors[403]) {
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js
index 4f8cee2651a9f..20281d8479ab4 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js
+++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js
@@ -51,7 +51,7 @@ describe('Vislib Dispatch Class Test Suite', function () {
});
it('implements on, off, emit methods', function () {
- const events = _.pluck(vis.handler.charts, 'events');
+ const events = _.map(vis.handler.charts, 'events');
expect(events.length).to.be.above(0);
events.forEach(function (dispatch) {
expect(dispatch).to.have.property('on');
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js
index f075dff466793..6b7ccaed25d49 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js
+++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js
@@ -267,7 +267,7 @@ describe('stackData method - data set with zeros in percentage mode', function (
expect(chart.chartData.series).to.have.length(1);
const series = chart.chartData.series[0].values;
// with the interval set in seriesMonthlyInterval data, the point at x=1454309600000 does not exist
- const point = _.find(series, 'x', 1454309600000);
+ const point = _.find(series, ['x', 1454309600000]);
expect(point).to.not.be(undefined);
expect(point.y).to.be(0);
});
@@ -279,7 +279,7 @@ describe('stackData method - data set with zeros in percentage mode', function (
const chart = vis.handler.charts[0];
expect(chart.chartData.series).to.have.length(5);
const series = chart.chartData.series[0].values;
- const point = _.find(series, 'x', 1415826240000);
+ const point = _.find(series, ['x', 1415826240000]);
expect(point).to.not.be(undefined);
expect(point.y).to.be(0);
});
diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js
index b5501982cec09..602b221b7d14d 100644
--- a/src/legacy/core_plugins/timelion/public/app.js
+++ b/src/legacy/core_plugins/timelion/public/app.js
@@ -427,7 +427,7 @@ app.controller('timelion', function (
const httpResult = $http
.post('../api/timelion/run', {
sheet: $scope.state.sheet,
- time: _.extend(
+ time: _.assignIn(
{
from: timeRangeBounds.min,
to: timeRangeBounds.max,
diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js
index 879fab206b99d..ae042310fd464 100644
--- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js
+++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js
@@ -165,7 +165,7 @@ module
};
self.getLabel = function () {
- return _.words(self.properties.nouns).map(_.capitalize).join(' ');
+ return _.words(self.properties.nouns).map(_.upperFirst).join(' ');
};
//key handler for the filter text box
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
index f3fd2fde8f2c5..2102b02194bc8 100644
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
+++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
@@ -78,7 +78,7 @@ export function TimelionExpInput($http, $timeout) {
function init() {
$http.get('../api/timelion/functions').then(function (resp) {
Object.assign(functionReference, {
- byName: _.indexBy(resp.data, 'name'),
+ byName: _.keyBy(resp.data, 'name'),
list: resp.data,
});
});
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js b/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js
index 577ee984e05c6..3750e15c000e7 100644
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js
+++ b/src/legacy/core_plugins/timelion/public/directives/timelion_interval/timelion_interval.js
@@ -47,7 +47,7 @@ export function TimelionInterval($timeout) {
// Only run this on initialization
if (newVal !== oldVal || oldVal == null) return;
- if (_.contains($scope.intervalOptions, newVal)) {
+ if (_.includes($scope.intervalOptions, newVal)) {
$scope.interval = newVal;
} else {
$scope.interval = 'other';
diff --git a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
index b1999eb4b483c..087e166925327 100644
--- a/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
+++ b/src/legacy/core_plugins/timelion/public/panels/timechart/schema.ts
@@ -346,7 +346,7 @@ export function timechartFn(dependencies: TimelionVisualizationDependencies) {
}
if (serie._global) {
- _.merge(options, serie._global, function (objVal, srcVal) {
+ _.mergeWith(options, serie._global, function (objVal, srcVal) {
// This is kind of gross, it means that you can't replace a global value with a null
// best you can do is an empty string. Deal with it.
if (objVal == null) return srcVal;
diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js
index 17da5ffca1242..db1ec425f2ce5 100644
--- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js
+++ b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js
@@ -19,8 +19,7 @@
import { resolve, basename, isAbsolute as isAbsolutePath } from 'path';
-import toPath from 'lodash/internal/toPath';
-import { get } from 'lodash';
+import { get, toPath } from 'lodash';
import { createInvalidPluginError } from '../errors';
import { isVersionCompatible } from './is_version_compatible';
diff --git a/src/legacy/server/i18n/localization/file_integrity.ts b/src/legacy/server/i18n/localization/file_integrity.ts
index a852fba4a1c5a..7400d84ea2ce7 100644
--- a/src/legacy/server/i18n/localization/file_integrity.ts
+++ b/src/legacy/server/i18n/localization/file_integrity.ts
@@ -33,7 +33,7 @@ export interface Integrities {
export async function getIntegrityHashes(filepaths: string[]): Promise