diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc index 900f87ec29d8a0..e6f3301bfea2b6 100644 --- a/docs/api/saved-objects/create.asciidoc +++ b/docs/api/saved-objects/create.asciidoc @@ -13,7 +13,7 @@ experimental[] Create {kib} saved objects. `POST :/api/saved_objects//` -`POST :/s//api/saved_objects/` +`POST :/s//saved_objects/` [[saved-objects-api-create-path-params]] ==== Path parameters diff --git a/docs/developer/getting-started/development-plugin-resources.asciidoc b/docs/developer/getting-started/development-plugin-resources.asciidoc index 8f81138b81ed7c..1fe211c87c6605 100644 --- a/docs/developer/getting-started/development-plugin-resources.asciidoc +++ b/docs/developer/getting-started/development-plugin-resources.asciidoc @@ -33,7 +33,7 @@ To enable TypeScript support, create a `tsconfig.json` file at the root of your ["source","js"] ----------- { - // extend {kib}'s tsconfig, or use your own settings + // extend Kibana's tsconfig, or use your own settings "extends": "../../kibana/tsconfig.json", // tell the TypeScript compiler where to find your source files diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index be31458ff39fae..af80b17f8605f7 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -11,10 +11,19 @@ a| <> | Send email from your server. +a| <> + +| Create an incident in IBM Resilient. + a| <> | Index data into Elasticsearch. +a| <> + +| Create an incident in Jira. + + a| <> | Send an event in PagerDuty. @@ -53,10 +62,12 @@ before {kib} starts. If you preconfigure a connector, you can also <>. include::action-types/email.asciidoc[] +include::action-types/resilient.asciidoc[] include::action-types/index.asciidoc[] +include::action-types/jira.asciidoc[] include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] +include::action-types/servicenow.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] include::action-types/pre-configured-connectors.asciidoc[] -include::action-types/servicenow.asciidoc[] diff --git a/docs/user/alerting/action-types/jira.asciidoc b/docs/user/alerting/action-types/jira.asciidoc new file mode 100644 index 00000000000000..48bd6c8501b9f6 --- /dev/null +++ b/docs/user/alerting/action-types/jira.asciidoc @@ -0,0 +1,77 @@ +[role="xpack"] +[[jira-action-type]] +=== Jira action + +The Jira action type uses the https://developer.atlassian.com/cloud/jira/platform/rest/v2/[REST API v2] to create Jira issues. + +[float] +[[jira-connector-configuration]] +==== Connector configuration + +Jira connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. +URL:: Jira instance URL. +Project key:: Jira project key. +Email (or username):: The account email (or username) for HTTP Basic authentication. +API token (or password):: Jira API authentication token (or password) for HTTP Basic authentication. + +[float] +[[Preconfigured-jira-configuration]] +==== Preconfigured action type + +[source,text] +-- + my-jira: + name: preconfigured-jira-action-type + actionTypeId: .jira + config: + apiUrl: https://elastic.atlassian.net + projectKey: ES + secrets: + email: testuser + apiToken: tokenkeystorevalue +-- + +`config` defines the action type specific to the configuration and contains the following properties: + +[cols="2*<"] +|=== + +| `apiUrl` +| An address that corresponds to *URL*. + +| `projectKey` +| A key that corresponds to *Project Key*. + +|=== + +`secrets` defines sensitive information for the action type: + +[cols="2*<"] +|=== + +| `email` +| A string that corresponds to *Email*. + +| `apiToken` +| A string that corresponds to *API Token*. Should be stored in the <>. + +|=== + +[[jira-action-configuration]] +==== Action configuration + +Jira actions have the following configuration properties: + +Issue type:: The type of the issue. +Priority:: The priority of the incident. +Labels:: The labels of the incident. +Title:: A title for the issue, used for searching the contents of the knowledge base. +Description:: The details about the incident. +Additional comments:: Additional information for the client, such as how to troubleshoot the issue. + +[[configuring-jira]] +==== Configuring and testing Jira + +Jira offers free https://www.atlassian.com/software/jira/free[Instances], which you can use to test incidents. diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index 2c9add5233c913..9301224e6df48e 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -36,7 +36,7 @@ This is required to encrypt parameters that must be secured, for example PagerDu If you have security enabled: * You must have -application privileges to access Metrics, APM, Uptime, or SIEM. +application privileges to access Metrics, APM, Uptime, or Security. * If you are using a self-managed deployment with security, you must have Transport Security Layer (TLS) enabled for communication <>. Alerts uses API keys to secure background alert checks and actions, diff --git a/docs/user/alerting/action-types/resilient.asciidoc b/docs/user/alerting/action-types/resilient.asciidoc new file mode 100644 index 00000000000000..b5ddb76d49b0cd --- /dev/null +++ b/docs/user/alerting/action-types/resilient.asciidoc @@ -0,0 +1,76 @@ +[role="xpack"] +[[resilient-action-type]] +=== IBM Resilient action + +The IBM Resilient action type uses the https://developer.ibm.com/security/resilient/rest/[RESILIENT REST v2] to create IBM Resilient incidents. + +[float] +[[resilient-connector-configuration]] +==== Connector configuration + +IBM Resilient connectors have the following configuration properties: + +Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. +URL:: IBM Resilient instance URL. +Organization ID:: IBM Resilient organization ID. +API key ID:: The authentication key ID for HTTP Basic authentication. +API key secret:: The authentication key secret for HTTP Basic authentication. + +[float] +[[Preconfigured-resilient-configuration]] +==== Preconfigured action type + +[source,text] +-- + my-resilient: + name: preconfigured-resilient-action-type + actionTypeId: .resilient + config: + apiUrl: https://elastic.resilient.net + orgId: ES + secrets: + apiKeyId: testuser + apiKeySecret: tokenkeystorevalue +-- + +`config` defines the action type specific to the configuration and contains the following properties: + +[cols="2*<"] +|=== + +| `apiUrl` +| An address that corresponds to *URL*. + +| `orgId` +| An ID that corresponds to *Organization ID*. + +|=== + +`secrets` defines sensitive information for the action type: + +[cols="2*<"] +|=== + +| `apiKeyId` +| A string that corresponds to *API key ID*. + +| `apiKeySecret` +| A string that corresponds to *API Key secret*. Should be stored in the <>. + +|=== + +[[resilient-action-configuration]] +==== Action configuration + +IBM Resilient actions have the following configuration properties: + +Incident types:: The incident types of the incident. +Severity code:: The severity of the incident. +Name:: A name for the issue, used for searching the contents of the knowledge base. +Description:: The details about the incident. +Additional comments:: Additional information for the client, such as how to troubleshoot the issue. + +[[configuring-resilient]] +==== Configuring and testing IBM Resilient + +IBM Resilient offers https://www.ibm.com/security/intelligent-orchestration/resilient[Instances], which you can use to test incidents. diff --git a/docs/user/alerting/action-types/servicenow.asciidoc b/docs/user/alerting/action-types/servicenow.asciidoc index 32f828aea2357e..0acb92bcdb5ee5 100644 --- a/docs/user/alerting/action-types/servicenow.asciidoc +++ b/docs/user/alerting/action-types/servicenow.asciidoc @@ -10,7 +10,7 @@ The ServiceNow action type uses the https://developer.servicenow.com/app.do#!/re ServiceNow connectors have the following configuration properties: -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. URL:: ServiceNow instance URL. Username:: Username for HTTP Basic authentication. Password:: Password for HTTP Basic authentication. @@ -37,7 +37,7 @@ Password:: Password for HTTP Basic authentication. |=== | `apiUrl` -| An address that corresponds to *Sender*. +| An address that corresponds to *URL*. |=== @@ -47,7 +47,7 @@ Password:: Password for HTTP Basic authentication. |=== | `username` -| A string that corresponds to *User*. +| A string that corresponds to *Username*. | `password` | A string that corresponds to *Password*. Should be stored in the <>. @@ -62,7 +62,7 @@ ServiceNow actions have the following configuration properties: Urgency:: The extent to which the incident resolution can delay. Severity:: The severity of the incident. Impact:: The effect an incident has on business. Can be measured by the number of affected users or by how critical it is to the business in question. -Short description:: A short description of the incident, used for searching the contents of the knowledge base. +Short description:: A short description for the incident, used for searching the contents of the knowledge base. Description:: The details about the incident. Additional comments:: Additional information for the client, such as how to troubleshoot the issue. diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 6bc085b0f78b9e..bdb72b1658cd26 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -6,7 +6,7 @@ beta[] -- -Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. +Alerting allows you to detect complex conditions within different {kib} apps and trigger actions when those conditions are met. Alerting is integrated with <>, <>, <>, <>, can be centrally managed from the <> UI, and provides a set of built-in <> and <> for you to use. image::images/alerting-overview.png[Alerts and actions UI] @@ -148,7 +148,7 @@ Functionally, {kib} alerting differs in that: * {kib} alerts tracks and persists the state of each detected condition through *alert instances*. This makes it possible to mute and throttle individual instances, and detect changes in state such as resolution. * Actions are linked to *alert instances* in {kib} alerting. Actions are fired for each occurrence of a detected condition, rather than for the entire alert. -At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. +At a higher level, {kib} alerts allow rich integrations across use cases like <>, <>, <>, and <>. Pre-packaged *alert types* simplify setup, hide the details complex domain-specific detections, while providing a consistent interface across {kib}. [float] @@ -171,7 +171,7 @@ To access alerting in a space, a user must have access to one of the following f * <> * <> -* <> +* <> * <> See <> for more information on configuring roles that provide access to these features. diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index d05a727016455f..7f201d2c39e89c 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -2,7 +2,7 @@ [[defining-alerts]] == Defining alerts -{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. +{kib} alerts can be created in a variety of apps including <>, <>, <>, <> and from <> UI. While alerting details may differ from app to app, they share a common interface for defining and configuring alerts that this section describes in more detail. [float] === Alert flyout diff --git a/package.json b/package.json index 5345f8752d4aff..10083ff07fccd1 100644 --- a/package.json +++ b/package.json @@ -227,9 +227,9 @@ "devDependencies": { "@babel/parser": "^7.11.2", "@babel/types": "^7.11.0", - "@elastic/apm-rum": "^5.5.0", + "@elastic/apm-rum": "^5.6.0", "@elastic/charts": "21.1.2", - "@elastic/ems-client": "7.9.3", + "@elastic/ems-client": "7.10.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/filesaver": "1.1.2", diff --git a/renovate.json5 b/renovate.json5 index 2b2e31a6707930..17391c2f838273 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -53,7 +53,7 @@ { groupName: 'vega related modules', packageNames: ['vega', 'vega-lite', 'vega-schema-url-parser', 'vega-tooltip'], - reviewers: ['team:elastic/kibana-app'], + reviewers: ['team:kibana-app'], labels: ['Feature:Lens', 'Team:KibanaApp'], enabled: true, }, diff --git a/src/core/server/capabilities/capabilities_service.mock.ts b/src/core/server/capabilities/capabilities_service.mock.ts index 7d134f9592dc72..72824693705d9b 100644 --- a/src/core/server/capabilities/capabilities_service.mock.ts +++ b/src/core/server/capabilities/capabilities_service.mock.ts @@ -18,6 +18,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; import { CapabilitiesService, CapabilitiesSetup, CapabilitiesStart } from './capabilities_service'; +import { Capabilities } from './types'; const createSetupContractMock = () => { const setupContract: jest.Mocked = { @@ -34,6 +35,14 @@ const createStartContractMock = () => { return setupContract; }; +const createCapabilitiesMock = (): Capabilities => { + return { + navLinks: {}, + management: {}, + catalogue: {}, + }; +}; + type CapabilitiesServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { @@ -47,4 +56,5 @@ export const capabilitiesServiceMock = { create: createMock, createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, + createCapabilities: createCapabilitiesMock, }; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 7e001ffe28100e..e9b345317e9996 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -54,6 +54,7 @@ export { metricsServiceMock } from './metrics/metrics_service.mock'; export { renderingMock } from './rendering/rendering_service.mock'; export { statusServiceMock } from './status/status_service.mock'; export { contextServiceMock } from './context/context_service.mock'; +export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f465ece697a700..9cf7234c4a9ffd 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -44,6 +44,7 @@ import { DeleteDocumentParams } from 'elasticsearch'; import { DeleteScriptParams } from 'elasticsearch'; import { DeleteTemplateParams } from 'elasticsearch'; import { Duration } from 'moment'; +import { ElasticsearchClient as ElasticsearchClient_2 } from 'kibana/server'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { ErrorToastOptions } from 'src/core/public/notifications'; diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts index 709736a37d8026..23a77c2d4c288a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/telemetry_application_usage_collector.test.ts @@ -17,7 +17,11 @@ * under the License. */ -import { savedObjectsRepositoryMock, loggingSystemMock } from '../../../../../core/server/mocks'; +import { + savedObjectsRepositoryMock, + loggingSystemMock, + elasticsearchServiceMock, +} from '../../../../../core/server/mocks'; import { CollectorOptions, createUsageCollectionSetupMock, @@ -50,6 +54,7 @@ describe('telemetry_application_usage', () => { const getUsageCollector = jest.fn(); const registerType = jest.fn(); const callCluster = jest.fn(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; beforeAll(() => registerApplicationUsageCollector(logger, usageCollectionMock, registerType, getUsageCollector) @@ -62,7 +67,7 @@ describe('telemetry_application_usage', () => { test('if no savedObjectClient initialised, return undefined', async () => { expect(collector.isReady()).toBe(false); - expect(await collector.fetch(callCluster)).toBeUndefined(); + expect(await collector.fetch(callCluster, esClient)).toBeUndefined(); jest.runTimersToTime(ROLL_INDICES_START); }); @@ -80,7 +85,7 @@ describe('telemetry_application_usage', () => { jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run expect(collector.isReady()).toBe(true); - expect(await collector.fetch(callCluster)).toStrictEqual({}); + expect(await collector.fetch(callCluster, esClient)).toStrictEqual({}); expect(savedObjectClient.bulkCreate).not.toHaveBeenCalled(); }); @@ -137,7 +142,7 @@ describe('telemetry_application_usage', () => { jest.runTimersToTime(ROLL_TOTAL_INDICES_INTERVAL); // Force rollTotals to run - expect(await collector.fetch(callCluster)).toStrictEqual({ + expect(await collector.fetch(callCluster, esClient)).toStrictEqual({ appId: { clicks_total: total + 1 + 10, clicks_7_days: total + 1, @@ -197,7 +202,7 @@ describe('telemetry_application_usage', () => { getUsageCollector.mockImplementation(() => savedObjectClient); - expect(await collector.fetch(callCluster)).toStrictEqual({ + expect(await collector.fetch(callCluster, esClient)).toStrictEqual({ appId: { clicks_total: 1, clicks_7_days: 0, diff --git a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js b/src/plugins/maps_legacy/public/get_service_settings.ts similarity index 52% rename from src/plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js rename to src/plugins/maps_legacy/public/get_service_settings.ts index d1354608385f64..8d0656237976d6 100644 --- a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js +++ b/src/plugins/maps_legacy/public/get_service_settings.ts @@ -17,33 +17,23 @@ * under the License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; +import { lazyLoadMapsLegacyModules } from './lazy_load_bundle'; +// @ts-expect-error +import { getMapsLegacyConfig } from './kibana_services'; +import { IServiceSettings } from './map/service_settings_types'; -import { TIMEOUT } from '../constants'; -import { getClusterStats } from '../get_cluster_stats'; +let loadPromise: Promise; -export function mockGetClusterStats(callCluster, clusterStats, req) { - callCluster - .withArgs(req, 'cluster.stats', { - timeout: TIMEOUT, - }) - .returns(clusterStats); +export async function getServiceSettings(): Promise { + if (typeof loadPromise !== 'undefined') { + return loadPromise; + } - callCluster - .withArgs('cluster.stats', { - timeout: TIMEOUT, - }) - .returns(clusterStats); -} - -describe.skip('get_cluster_stats', () => { - it('uses callCluster to get cluster.stats API', async () => { - const callCluster = sinon.stub(); - const response = Promise.resolve({}); - - mockGetClusterStats(callCluster, response); - - expect(getClusterStats(callCluster)).to.be(response); + loadPromise = new Promise(async (resolve) => { + const modules = await lazyLoadMapsLegacyModules(); + const config = getMapsLegacyConfig(); + // @ts-expect-error + resolve(new modules.ServiceSettings(config, config.tilemap)); }); -}); + return loadPromise; +} diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index 8f14cd1b15e2c2..d31f23f4bc4a66 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -19,8 +19,6 @@ // @ts-ignore import { PluginInitializerContext } from 'kibana/public'; -// @ts-ignore -import { L } from './leaflet'; import { MapsLegacyPlugin } from './plugin'; // @ts-ignore import * as colorUtil from './map/color_util'; @@ -29,14 +27,14 @@ import { KibanaMapLayer } from './map/kibana_map_layer'; // @ts-ignore import { convertToGeoJson } from './map/convert_to_geojson'; // @ts-ignore -import { scaleBounds, getPrecision, geoContains } from './map/decode_geo_hash'; +import { getPrecision, geoContains } from './map/decode_geo_hash'; import { VectorLayer, FileLayerField, FileLayer, TmsLayer, IServiceSettings, -} from './map/service_settings'; +} from './map/service_settings_types'; // @ts-ignore import { mapTooltipProvider } from './tooltip_provider'; @@ -48,7 +46,6 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public */ export { - scaleBounds, getPrecision, geoContains, colorUtil, @@ -60,7 +57,6 @@ export { FileLayer, TmsLayer, mapTooltipProvider, - L, }; export * from './common/types'; @@ -68,5 +64,7 @@ export { ORIGIN } from './common/constants/origin'; export { WmsOptions } from './components/wms_options'; +export { lazyLoadMapsLegacyModules } from './lazy_load_bundle'; + export type MapsLegacyPluginSetup = ReturnType; export type MapsLegacyPluginStart = ReturnType; diff --git a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_info.js b/src/plugins/maps_legacy/public/lazy_load_bundle/index.ts similarity index 58% rename from src/plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_info.js rename to src/plugins/maps_legacy/public/lazy_load_bundle/index.ts index fe83b76cd11583..292949503a6160 100644 --- a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_info.js +++ b/src/plugins/maps_legacy/public/lazy_load_bundle/index.ts @@ -17,23 +17,27 @@ * under the License. */ -import expect from '@kbn/expect'; -import sinon from 'sinon'; +let loadModulesPromise: Promise; -import { getClusterInfo } from '../get_cluster_info'; - -export function mockGetClusterInfo(callCluster, clusterInfo, req) { - callCluster.withArgs(req, 'info').returns(clusterInfo); - callCluster.withArgs('info').returns(clusterInfo); +interface LazyLoadedMapsLegacyModules { + KibanaMap: unknown; + L: unknown; + ServiceSettings: unknown; } -describe('get_cluster_info', () => { - it('uses callCluster to get info API', () => { - const callCluster = sinon.stub(); - const response = Promise.resolve({}); +export async function lazyLoadMapsLegacyModules(): Promise { + if (typeof loadModulesPromise !== 'undefined') { + return loadModulesPromise; + } - mockGetClusterInfo(callCluster, response); + loadModulesPromise = new Promise(async (resolve) => { + const { KibanaMap, L, ServiceSettings } = await import('./lazy'); - expect(getClusterInfo(callCluster)).to.be(response); + resolve({ + KibanaMap, + L, + ServiceSettings, + }); }); -}); + return loadModulesPromise; +} diff --git a/src/plugins/maps_legacy/public/lazy_load_bundle/lazy/index.ts b/src/plugins/maps_legacy/public/lazy_load_bundle/lazy/index.ts new file mode 100644 index 00000000000000..5031b29e74b9ae --- /dev/null +++ b/src/plugins/maps_legacy/public/lazy_load_bundle/lazy/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// @ts-expect-error +export { KibanaMap } from '../../map/kibana_map'; +// @ts-expect-error +export { ServiceSettings } from '../../map/service_settings'; +// @ts-expect-error +export { L } from '../../leaflet'; diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 2d78fdc246e197..406dae43c9b5ea 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -17,25 +17,22 @@ * under the License. */ -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; import { getEmsTileLayerId, getUiSettings, getToasts } from '../kibana_services'; +import { lazyLoadMapsLegacyModules } from '../lazy_load_bundle'; +import { getServiceSettings } from '../get_service_settings'; const WMS_MINZOOM = 0; const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS -export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) { +export function BaseMapsVisualizationProvider() { /** * Abstract base class for a visualization consisting of a map with a single baselayer. * @class BaseMapsVisualization * @constructor */ - - const serviceSettings = mapServiceSettings; - const toastService = getToasts(); - return class BaseMapsVisualization { constructor(element, vis) { this.vis = vis; @@ -95,9 +92,9 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) const centerFromUIState = uiState.get('mapCenter'); options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : this.vis.params.mapZoom; options.center = centerFromUIState ? centerFromUIState : this.vis.params.mapCenter; - const services = { toastService }; - this._kibanaMap = getKibanaMap(this._container, options, services); + const modules = await lazyLoadMapsLegacyModules(); + this._kibanaMap = new modules.KibanaMap(this._container, options); this._kibanaMap.setMinZoom(WMS_MINZOOM); //use a default this._kibanaMap.setMaxZoom(WMS_MAXZOOM); //use a default @@ -138,6 +135,7 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) const mapParams = this._getMapsParams(); if (!this._tmsConfigured()) { try { + const serviceSettings = await getServiceSettings(); const tmsServices = await serviceSettings.getTMSServices(); const userConfiguredTmsLayer = tmsServices[0]; const initBasemapLayer = userConfiguredTmsLayer @@ -147,7 +145,7 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) this._setTmsLayer(initBasemapLayer); } } catch (e) { - toastService.addWarning(e.message); + getToasts().addWarning(e.message); return; } return; @@ -174,7 +172,7 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) this._setTmsLayer(selectedTmsLayer); } } catch (tmsLoadingError) { - toastService.addWarning(tmsLoadingError.message); + getToasts().addWarning(tmsLoadingError.message); } } @@ -189,13 +187,14 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) isDesaturated = true; } const isDarkMode = getUiSettings().get('theme:darkMode'); + const serviceSettings = await getServiceSettings(); const meta = await serviceSettings.getAttributesForTMSLayer( tmsLayer, isDesaturated, isDarkMode ); const showZoomMessage = serviceSettings.shouldShowZoomMessage(tmsLayer); - const options = _.cloneDeep(tmsLayer); + const options = { ...tmsLayer }; delete options.id; delete options.subdomains; this._kibanaMap.setBaseLayer({ @@ -228,12 +227,11 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) } _getMapsParams() { - return _.assign( - {}, - this.vis.type.visConfig.defaults, - { type: this.vis.type.name }, - this._params - ); + return { + ...this.vis.type.visConfig.defaults, + type: this.vis.type.name, + ...this._params, + }; } _whenBaseLayerIsLoaded() { diff --git a/src/plugins/maps_legacy/public/map/decode_geo_hash.ts b/src/plugins/maps_legacy/public/map/decode_geo_hash.ts index 8c39ada03a46b4..65184a8244777a 100644 --- a/src/plugins/maps_legacy/public/map/decode_geo_hash.ts +++ b/src/plugins/maps_legacy/public/map/decode_geo_hash.ts @@ -17,8 +17,6 @@ * under the License. */ -import _ from 'lodash'; - interface DecodedGeoHash { latitude: number[]; longitude: number[]; @@ -101,33 +99,6 @@ interface GeoBoundingBox { bottom_right: GeoBoundingBoxCoordinate; } -export function scaleBounds(bounds: GeoBoundingBox): GeoBoundingBox { - const scale = 0.5; // scale bounds by 50% - - const topLeft = bounds.top_left; - const bottomRight = bounds.bottom_right; - let latDiff = _.round(Math.abs(topLeft.lat - bottomRight.lat), 5); - const lonDiff = _.round(Math.abs(bottomRight.lon - topLeft.lon), 5); - // map height can be zero when vis is first created - if (latDiff === 0) latDiff = lonDiff; - - const latDelta = latDiff * scale; - let topLeftLat = _.round(topLeft.lat, 5) + latDelta; - if (topLeftLat > 90) topLeftLat = 90; - let bottomRightLat = _.round(bottomRight.lat, 5) - latDelta; - if (bottomRightLat < -90) bottomRightLat = -90; - const lonDelta = lonDiff * scale; - let topLeftLon = _.round(topLeft.lon, 5) - lonDelta; - if (topLeftLon < -180) topLeftLon = -180; - let bottomRightLon = _.round(bottomRight.lon, 5) + lonDelta; - if (bottomRightLon > 180) bottomRightLon = 180; - - return { - top_left: { lat: topLeftLat, lon: topLeftLon }, - bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, - }; -} - export function geoContains(collar?: GeoBoundingBox, bounds?: GeoBoundingBox) { if (!bounds || !collar) return false; // test if bounds top_left is outside collar diff --git a/src/plugins/maps_legacy/public/map/grid_dimensions.js b/src/plugins/maps_legacy/public/map/grid_dimensions.js index d146adf2ca67f9..0f84e972104bab 100644 --- a/src/plugins/maps_legacy/public/map/grid_dimensions.js +++ b/src/plugins/maps_legacy/public/map/grid_dimensions.js @@ -17,8 +17,6 @@ * under the License. */ -import _ from 'lodash'; - // geohash precision mapping of geohash grid cell dimensions (width x height, in meters) at equator. // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator const gridAtEquator = { @@ -37,5 +35,5 @@ const gridAtEquator = { }; export function gridDimensions(precision) { - return _.get(gridAtEquator, precision); + return gridAtEquator[precision]; } diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index ad5d2c089b8757..3948692e556762 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -20,7 +20,7 @@ import { EventEmitter } from 'events'; import { createZoomWarningMsg } from './map_messages'; import $ from 'jquery'; -import _ from 'lodash'; +import { get, isEqual, escape } from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../common/constants/origin'; @@ -380,7 +380,7 @@ export class KibanaMap extends EventEmitter { const distanceX = latLngC.distanceTo(latLngX); // calculate distance between c and x (latitude) const distanceY = latLngC.distanceTo(latLngY); // calculate distance between c and y (longitude) - return _.min([distanceX, distanceY]); + return Math.min(distanceX, distanceY); } _getLeafletBounds(resizeOnFail) { @@ -544,7 +544,7 @@ export class KibanaMap extends EventEmitter { } setBaseLayer(settings) { - if (_.isEqual(settings, this._baseLayerSettings)) { + if (isEqual(settings, this._baseLayerSettings)) { return; } @@ -567,7 +567,7 @@ export class KibanaMap extends EventEmitter { let baseLayer; if (settings.baseLayerType === 'wms') { //This is user-input that is rendered with the Leaflet attribution control. Needs to be sanitized. - this._baseLayerSettings.options.attribution = _.escape(settings.options.attribution); + this._baseLayerSettings.options.attribution = escape(settings.options.attribution); baseLayer = this._getWMSBaseLayer(settings.options); } else if (settings.baseLayerType === 'tms') { baseLayer = this._getTMSBaseLayer(settings.options); @@ -661,7 +661,7 @@ export class KibanaMap extends EventEmitter { _updateDesaturation() { const tiles = $('img.leaflet-tile-loaded'); // Don't apply client-side styling to EMS basemaps - if (_.get(this._baseLayerSettings, 'options.origin') === ORIGIN.EMS) { + if (get(this._baseLayerSettings, 'options.origin') === ORIGIN.EMS) { tiles.addClass('filters-off'); } else { if (this._baseLayerIsDesaturated) { diff --git a/src/plugins/maps_legacy/public/map/service_settings.d.ts b/src/plugins/maps_legacy/public/map/service_settings_types.ts similarity index 100% rename from src/plugins/maps_legacy/public/map/service_settings.d.ts rename to src/plugins/maps_legacy/public/map/service_settings_types.ts diff --git a/src/plugins/maps_legacy/public/plugin.ts b/src/plugins/maps_legacy/public/plugin.ts index 8c9f1e9cef1944..17cee226cb70ca 100644 --- a/src/plugins/maps_legacy/public/plugin.ts +++ b/src/plugins/maps_legacy/public/plugin.ts @@ -22,15 +22,12 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/p // @ts-ignore import { setToasts, setUiSettings, setKibanaVersion, setMapsLegacyConfig } from './kibana_services'; // @ts-ignore -import { ServiceSettings } from './map/service_settings'; -// @ts-ignore import { getPrecision, getZoomPrecision } from './map/precision'; -// @ts-ignore -import { KibanaMap } from './map/kibana_map'; import { MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; import { MapsLegacyConfig } from '../config'; // @ts-ignore import { BaseMapsVisualizationProvider } from './map/base_maps_visualization'; +import { getServiceSettings } from './get_service_settings'; /** * These are the interfaces with your public contracts. You should export these @@ -67,17 +64,13 @@ export class MapsLegacyPlugin implements Plugin new KibanaMap(...args); - const getBaseMapsVis = () => - new BaseMapsVisualizationProvider(getKibanaMapFactoryProvider, serviceSettings); + const getBaseMapsVis = () => new BaseMapsVisualizationProvider(); return { - serviceSettings, + getServiceSettings, getZoomPrecision, getPrecision, config, - getKibanaMapFactoryProvider, getBaseMapsVis, }; } diff --git a/src/plugins/region_map/public/choropleth_layer.js b/src/plugins/region_map/public/choropleth_layer.js index 30fa8b544cdec3..14a91e189e0d50 100644 --- a/src/plugins/region_map/public/choropleth_layer.js +++ b/src/plugins/region_map/public/choropleth_layer.js @@ -33,7 +33,7 @@ const EMPTY_STYLE = { fillOpacity: 0, }; -export default class ChoroplethLayer extends KibanaMapLayer { +export class ChoroplethLayer extends KibanaMapLayer { static _doInnerJoin(sortedMetrics, sortedGeojsonFeatures, joinField) { let j = 0; for (let i = 0; i < sortedGeojsonFeatures.length; i++) { @@ -71,7 +71,16 @@ export default class ChoroplethLayer extends KibanaMapLayer { } } - constructor(name, attribution, format, showAllShapes, meta, layerConfig, serviceSettings) { + constructor( + name, + attribution, + format, + showAllShapes, + meta, + layerConfig, + serviceSettings, + leaflet + ) { super(); this._serviceSettings = serviceSettings; this._metrics = null; @@ -84,9 +93,10 @@ export default class ChoroplethLayer extends KibanaMapLayer { this._showAllShapes = showAllShapes; this._layerName = name; this._layerConfig = layerConfig; + this._leaflet = leaflet; // eslint-disable-next-line no-undef - this._leafletLayer = L.geoJson(null, { + this._leafletLayer = this._leaflet.geoJson(null, { onEachFeature: (feature, layer) => { layer.on('click', () => { this.emit('select', feature.properties[this._joinField]); @@ -97,7 +107,7 @@ export default class ChoroplethLayer extends KibanaMapLayer { const tooltipContents = this._tooltipFormatter(feature); if (!location) { // eslint-disable-next-line no-undef - const leafletGeojson = L.geoJson(feature); + const leafletGeojson = this._leaflet.geoJson(feature); location = leafletGeojson.getBounds().getCenter(); } this.emit('showTooltip', { @@ -425,7 +435,7 @@ CORS configuration of the server permits requests from the Kibana application on const { min, max } = getMinMax(this._metrics); // eslint-disable-next-line no-undef - const boundsOfAllFeatures = new L.LatLngBounds(); + const boundsOfAllFeatures = new this._leaflet.LatLngBounds(); return { leafletStyleFunction: (geojsonFeature) => { const match = geojsonFeature.__kbnJoinedMetric; @@ -433,7 +443,7 @@ CORS configuration of the server permits requests from the Kibana application on return emptyStyle(); } // eslint-disable-next-line no-undef - const boundsOfFeature = L.geoJson(geojsonFeature).getBounds(); + const boundsOfFeature = this._leaflet.geoJson(geojsonFeature).getBounds(); boundsOfAllFeatures.extend(boundsOfFeature); return { diff --git a/src/plugins/region_map/public/components/region_map_options.tsx b/src/plugins/region_map/public/components/region_map_options.tsx index be3d7fe86ab3f9..4d564d7347a1e9 100644 --- a/src/plugins/region_map/public/components/region_map_options.tsx +++ b/src/plugins/region_map/public/components/region_map_options.tsx @@ -37,11 +37,11 @@ const mapFieldForOption = ({ description, name }: FileLayerField) => ({ }); export type RegionMapOptionsProps = { - serviceSettings: IServiceSettings; + getServiceSettings: () => Promise; } & VisOptionsProps; function RegionMapOptions(props: RegionMapOptionsProps) { - const { serviceSettings, stateParams, vis, setValue } = props; + const { getServiceSettings, stateParams, vis, setValue } = props; const { vectorLayers } = vis.type.editorConfig.collections; const vectorLayerOptions = useMemo(() => vectorLayers.map(mapLayerForOption), [vectorLayers]); const fieldOptions = useMemo( @@ -54,10 +54,11 @@ function RegionMapOptions(props: RegionMapOptionsProps) { const setEmsHotLink = useCallback( async (layer: VectorLayer) => { + const serviceSettings = await getServiceSettings(); const emsHotLink = await serviceSettings.getEMSHotLink(layer); setValue('emsHotLink', emsHotLink); }, - [setValue, serviceSettings] + [setValue, getServiceSettings] ); const setLayer = useCallback( diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index ec9ee943105788..c641c16a8112bd 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -41,7 +41,7 @@ import { KibanaLegacyStart } from '../../kibana_legacy/public'; interface RegionMapVisualizationDependencies { uiSettings: IUiSettingsClient; regionmapsConfig: RegionMapsConfig; - serviceSettings: IServiceSettings; + getServiceSettings: () => Promise; BaseMapsVisualization: any; } @@ -93,7 +93,7 @@ export class RegionMapPlugin implements Plugin = { uiSettings: core.uiSettings, regionmapsConfig: config as RegionMapsConfig, - serviceSettings: mapsLegacy.serviceSettings, + getServiceSettings: mapsLegacy.getServiceSettings, BaseMapsVisualization: mapsLegacy.getBaseMapsVis(), }; diff --git a/src/plugins/region_map/public/region_map_type.js b/src/plugins/region_map/public/region_map_type.js index def95950e61519..036726f6a4a9e2 100644 --- a/src/plugins/region_map/public/region_map_type.js +++ b/src/plugins/region_map/public/region_map_type.js @@ -26,7 +26,7 @@ import { Schemas } from '../../vis_default_editor/public'; import { ORIGIN } from '../../maps_legacy/public'; export function createRegionMapTypeDefinition(dependencies) { - const { uiSettings, regionmapsConfig, serviceSettings } = dependencies; + const { uiSettings, regionmapsConfig, getServiceSettings } = dependencies; const visualization = createRegionMapVisualization(dependencies); return { @@ -54,7 +54,9 @@ provided base maps, or add your own. Darker colors represent higher values.', }, visualization, editorConfig: { - optionsTemplate: (props) => , + optionsTemplate: (props) => ( + + ), collections: { colorSchemas: truncatedColorSchemas, vectorLayers: [], @@ -97,6 +99,7 @@ provided base maps, or add your own. Darker colors represent higher values.', ]), }, setup: async (vis) => { + const serviceSettings = await getServiceSettings(); const tmsLayers = await serviceSettings.getTMSServices(); vis.type.editorConfig.collections.tmsLayers = tmsLayers; if (!vis.params.wms.selectedTmsLayer && tmsLayers.length) { diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index 43959c367558f0..9b20a35630c862 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -18,18 +18,16 @@ */ import { i18n } from '@kbn/i18n'; -import ChoroplethLayer from './choropleth_layer'; import { getFormatService, getNotifications, getKibanaLegacy } from './kibana_services'; import { truncatedColorMaps } from '../../charts/public'; import { tooltipFormatter } from './tooltip_formatter'; -import { mapTooltipProvider, ORIGIN } from '../../maps_legacy/public'; -import _ from 'lodash'; +import { mapTooltipProvider, ORIGIN, lazyLoadMapsLegacyModules } from '../../maps_legacy/public'; export function createRegionMapVisualization({ regionmapsConfig, - serviceSettings, uiSettings, BaseMapsVisualization, + getServiceSettings, }) { return class RegionMapsVisualization extends BaseMapsVisualization { constructor(container, vis) { @@ -71,7 +69,7 @@ export function createRegionMapVisualization({ return; } - this._updateChoroplethLayerForNewMetrics( + await this._updateChoroplethLayerForNewMetrics( selectedLayer.name, selectedLayer.attribution, this._params.showAllShapes, @@ -98,10 +96,13 @@ export function createRegionMapVisualization({ // Do not use the selectedLayer from the visState. // These settings are stored in the URL and can be used to inject dirty display content. + const { escape } = await import('lodash'); + if ( fileLayerConfig.isEMS || //Hosted by EMS. Metadata needs to be resolved through EMS (fileLayerConfig.layerId && fileLayerConfig.layerId.startsWith(`${ORIGIN.EMS}.`)) //fallback for older saved objects ) { + const serviceSettings = await getServiceSettings(); return await serviceSettings.loadFileLayerConfig(fileLayerConfig); } @@ -113,7 +114,7 @@ export function createRegionMapVisualization({ if (configuredLayer) { return { ...configuredLayer, - attribution: _.escape(configuredLayer.attribution ? configuredLayer.attribution : ''), + attribution: escape(configuredLayer.attribution ? configuredLayer.attribution : ''), }; } @@ -133,7 +134,7 @@ export function createRegionMapVisualization({ return; } - this._updateChoroplethLayerForNewProperties( + await this._updateChoroplethLayerForNewProperties( selectedLayer.name, selectedLayer.attribution, this._params.showAllShapes @@ -151,24 +152,24 @@ export function createRegionMapVisualization({ ); } - _updateChoroplethLayerForNewMetrics(name, attribution, showAllData, newMetrics) { + async _updateChoroplethLayerForNewMetrics(name, attribution, showAllData, newMetrics) { if ( this._choroplethLayer && this._choroplethLayer.canReuseInstanceForNewMetrics(name, showAllData, newMetrics) ) { return; } - return this._recreateChoroplethLayer(name, attribution, showAllData); + await this._recreateChoroplethLayer(name, attribution, showAllData); } - _updateChoroplethLayerForNewProperties(name, attribution, showAllData) { + async _updateChoroplethLayerForNewProperties(name, attribution, showAllData) { if (this._choroplethLayer && this._choroplethLayer.canReuseInstance(name, showAllData)) { return; } - return this._recreateChoroplethLayer(name, attribution, showAllData); + await this._recreateChoroplethLayer(name, attribution, showAllData); } - _recreateChoroplethLayer(name, attribution, showAllData) { + async _recreateChoroplethLayer(name, attribution, showAllData) { this._kibanaMap.removeLayer(this._choroplethLayer); if (this._choroplethLayer) { @@ -179,9 +180,10 @@ export function createRegionMapVisualization({ showAllData, this._params.selectedLayer.meta, this._params.selectedLayer, - serviceSettings + await getServiceSettings() ); } else { + const { ChoroplethLayer } = await import('./choropleth_layer'); this._choroplethLayer = new ChoroplethLayer( name, attribution, @@ -189,7 +191,8 @@ export function createRegionMapVisualization({ showAllData, this._params.selectedLayer.meta, this._params.selectedLayer, - serviceSettings + await getServiceSettings(), + (await lazyLoadMapsLegacyModules()).L ); } diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 005c5f96d98d03..dfbbe3355e69c9 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -34,6 +34,7 @@ import { SavedObjectsClient, Plugin, Logger, + IClusterClient, } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -83,6 +84,7 @@ export class TelemetryPlugin implements Plugin) { this.logger = initializerContext.logger.get(); @@ -102,8 +104,11 @@ export class TelemetryPlugin implements Plugin this.elasticsearchClient + ); const router = http.createRouter(); registerRoutes({ @@ -126,14 +131,12 @@ export class TelemetryPlugin implements Plugin { - const { savedObjects, uiSettings } = core; + public async start(core: CoreStart, { telemetryCollectionManager }: TelemetryPluginsDepsStart) { + const { savedObjects, uiSettings, elasticsearch } = core; this.savedObjectsClient = savedObjects.createInternalRepository(); const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient); this.uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + this.elasticsearchClient = elasticsearch.client; try { await handleOldSettings(savedObjectsClient, this.uiSettingsClient); 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 deleted file mode 100644 index 8541745faea3b6..00000000000000 --- a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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 expect from '@kbn/expect'; -import sinon from 'sinon'; -import { merge, omit } from 'lodash'; - -import { TIMEOUT } from '../constants'; -import { mockGetClusterInfo } from './get_cluster_info'; -import { mockGetClusterStats } from './get_cluster_stats'; - -import { getLocalStats, handleLocalStats } from '../get_local_stats'; - -const mockUsageCollection = (kibanaUsage = {}) => ({ - bulkFetch: () => kibanaUsage, - toObject: (data) => data, -}); - -const getMockServer = (getCluster = sinon.stub()) => ({ - log(tags, message) { - console.log({ tags, message }); - }, - config() { - return { - get(item) { - switch (item) { - case 'pkg.version': - return '8675309-snapshot'; - default: - throw Error(`unexpected config.get('${item}') received.`); - } - }, - }; - }, - plugins: { - elasticsearch: { getCluster }, - }, -}); -function mockGetNodesUsage(callCluster, nodesUsage, req) { - callCluster - .withArgs( - req, - { - method: 'GET', - path: '/_nodes/usage', - query: { - timeout: TIMEOUT, - }, - }, - 'transport.request' - ) - .returns(nodesUsage); -} - -function mockGetLocalStats(callCluster, clusterInfo, clusterStats, nodesUsage, req) { - mockGetClusterInfo(callCluster, clusterInfo, req); - mockGetClusterStats(callCluster, clusterStats, req); - mockGetNodesUsage(callCluster, nodesUsage, req); -} - -describe('get_local_stats', () => { - const clusterUuid = 'abc123'; - const clusterName = 'my-cool-cluster'; - const version = '2.3.4'; - const clusterInfo = { - cluster_uuid: clusterUuid, - cluster_name: clusterName, - version: { - number: version, - }, - }; - const nodesUsage = [ - { - node_id: 'some_node_id', - timestamp: 1588617023177, - since: 1588616945163, - rest_actions: { - nodes_usage_action: 1, - create_index_action: 1, - document_get_action: 1, - search_action: 19, - nodes_info_action: 36, - }, - aggregations: { - terms: { - bytes: 2, - }, - scripted_metric: { - other: 7, - }, - }, - }, - ]; - const clusterStats = { - _nodes: { failed: 123 }, - cluster_name: 'real-cool', - indices: { totally: 456 }, - nodes: { yup: 'abc' }, - random: 123, - }; - - const kibana = { - kibana: { - great: 'googlymoogly', - versions: [{ version: '8675309', count: 1 }], - }, - kibana_stats: { - os: { - platform: 'rocky', - platformRelease: 'iv', - }, - }, - localization: { - locale: 'en', - labelsCount: 0, - integrities: {}, - }, - sun: { chances: 5 }, - clouds: { chances: 95 }, - rain: { chances: 2 }, - snow: { chances: 0 }, - }; - - const clusterStatsWithNodesUsage = { - ...clusterStats, - nodes: merge(clusterStats.nodes, { usage: nodesUsage }), - }; - const combinedStatsResult = { - collection: 'local', - cluster_uuid: clusterUuid, - cluster_name: clusterName, - version, - cluster_stats: omit(clusterStatsWithNodesUsage, '_nodes', 'cluster_name'), - stack_stats: { - kibana: { - great: 'googlymoogly', - count: 1, - indices: 1, - os: { - platforms: [{ platform: 'rocky', count: 1 }], - platformReleases: [{ platformRelease: 'iv', count: 1 }], - }, - versions: [{ version: '8675309', count: 1 }], - plugins: { - localization: { - locale: 'en', - labelsCount: 0, - integrities: {}, - }, - sun: { chances: 5 }, - clouds: { chances: 95 }, - rain: { chances: 2 }, - snow: { chances: 0 }, - }, - }, - }, - }; - - const context = { - logger: console, - version: '8.0.0', - }; - - describe('handleLocalStats', () => { - it('returns expected object without xpack and kibana data', () => { - 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, data: undefined }); - }); - - it('returns expected object with xpack', () => { - 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); - expect(cluster.license).to.eql(combinedStatsResult.license); - expect(stack.xpack).to.eql(combinedStatsResult.stack_stats.xpack); - }); - }); - - describe.skip('getLocalStats', () => { - it('returns expected object without xpack data when X-Pack fails to respond', async () => { - const callClusterUsageFailed = sinon.stub(); - const usageCollection = mockUsageCollection(); - mockGetLocalStats( - callClusterUsageFailed, - Promise.resolve(clusterInfo), - Promise.resolve(clusterStats), - Promise.resolve(nodesUsage) - ); - const result = await getLocalStats([], { - server: getMockServer(), - callCluster: callClusterUsageFailed, - usageCollection, - }); - 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.cluster_stats.nodes).to.eql(combinedStatsResult.cluster_stats.nodes); - expect(result.version).to.be('2.3.4'); - expect(result.collection).to.be('local'); - - // license and xpack usage info come from the same cluster call - expect(result.license).to.be(undefined); - expect(result.stack_stats.xpack).to.be(undefined); - }); - - it('returns expected object with xpack and kibana data', async () => { - const callCluster = sinon.stub(); - const usageCollection = mockUsageCollection(kibana); - mockGetLocalStats( - callCluster, - Promise.resolve(clusterInfo), - Promise.resolve(clusterStats), - Promise.resolve(nodesUsage) - ); - - const result = await getLocalStats([], { - server: getMockServer(callCluster), - usageCollection, - callCluster, - }); - - expect(result.stack_stats.xpack).to.eql(combinedStatsResult.stack_stats.xpack); - expect(result.stack_stats.kibana).to.eql(combinedStatsResult.stack_stats.kibana); - }); - }); -}); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.test.ts new file mode 100644 index 00000000000000..459b18d252e171 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { getClusterInfo } from './get_cluster_info'; + +export function mockGetClusterInfo(clusterInfo: any) { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.info + // @ts-ignore we only care about the response body + .mockResolvedValue( + // @ts-ignore we only care about the response body + { + body: { ...clusterInfo }, + } + ); + return esClient; +} + +describe('get_cluster_info using the elasticsearch client', () => { + it('uses the esClient to get info API', async () => { + const clusterInfo = { + cluster_uuid: '1234', + cluster_name: 'testCluster', + version: { + number: '7.9.2', + build_flavor: 'default', + build_type: 'docker', + build_hash: 'b5ca9c58fb664ca8bf', + build_date: '2020-07-21T16:40:44.668009Z', + build_snapshot: false, + lucene_version: '8.5.1', + minimum_wire_compatibility_version: '6.8.0', + minimum_index_compatibility_version: '6.0.0-beta1', + }, + }; + const esClient = mockGetClusterInfo(clusterInfo); + + expect(await getClusterInfo(esClient)).toStrictEqual(clusterInfo); + }); +}); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts index 4a33356ee97614..407f3325c3a9f5 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_cluster_info.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'src/core/server'; // This can be removed when the ES client improves the types export interface ESClusterInfo { @@ -25,24 +25,24 @@ export interface ESClusterInfo { cluster_name: string; version: { number: string; - build_flavor: string; - build_type: string; - build_hash: string; - build_date: string; + build_flavor?: string; + build_type?: string; + build_hash?: string; + build_date?: string; build_snapshot?: boolean; - lucene_version: string; - minimum_wire_compatibility_version: string; - minimum_index_compatibility_version: string; + lucene_version?: string; + minimum_wire_compatibility_version?: string; + minimum_index_compatibility_version?: string; }; } - /** * Get the cluster info from the connected cluster. * * This is the equivalent to GET / * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) + * @param {function} esClient The asInternalUser handler (exposed for testing) */ -export function getClusterInfo(callCluster: LegacyAPICaller) { - return callCluster('info'); +export async function getClusterInfo(esClient: ElasticsearchClient) { + const { body } = await esClient.info(); + return body; } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.test.ts new file mode 100644 index 00000000000000..81551c0c4d93db --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.test.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { getClusterStats } from './get_cluster_stats'; +import { TIMEOUT } from './constants'; + +export function mockGetClusterStats(clusterStats: any) { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.cluster.stats.mockResolvedValue(clusterStats); + return esClient; +} + +describe('get_cluster_stats', () => { + it('uses the esClient to get the response from the `cluster.stats` API', async () => { + const response = Promise.resolve({ body: { cluster_uuid: '1234' } }); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.cluster.stats.mockImplementationOnce( + // @ts-ignore the method only cares about the response body + async (_params = { timeout: TIMEOUT }) => { + return response; + } + ); + const result = getClusterStats(esClient); + expect(esClient.cluster.stats).toHaveBeenCalledWith({ timeout: TIMEOUT }); + expect(result).toStrictEqual(response); + }); +}); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.ts index d7c0110a99c6fd..d2a64e48786799 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_cluster_stats.ts @@ -18,23 +18,23 @@ */ import { ClusterDetailsGetter } from 'src/plugins/telemetry_collection_manager/server'; -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'src/core/server'; import { TIMEOUT } from './constants'; /** * Get the cluster stats from the connected cluster. * * This is the equivalent to GET /_cluster/stats?timeout=30s. */ -export async function getClusterStats(callCluster: LegacyAPICaller) { - return await callCluster('cluster.stats', { - timeout: TIMEOUT, - }); +export async function getClusterStats(esClient: ElasticsearchClient) { + const { body } = await esClient.cluster.stats({ timeout: TIMEOUT }); + return body; } /** * Get the cluster uuids from the connected cluster. */ -export const getClusterUuids: ClusterDetailsGetter = async ({ callCluster }) => { - const result = await getClusterStats(callCluster); - return [{ clusterUuid: result.cluster_uuid }]; +export const getClusterUuids: ClusterDetailsGetter = async ({ esClient }) => { + const { body } = await esClient.cluster.stats({ timeout: TIMEOUT }); + + return [{ clusterUuid: body.cluster_uuid }]; }; 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 index dee718decdc1f7..bb5eb7f6b726da 100644 --- 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 @@ -19,6 +19,7 @@ import { buildDataTelemetryPayload, getDataTelemetry } from './get_data_telemetry'; import { DATA_DATASETS_INDEX_PATTERNS, DATA_DATASETS_INDEX_PATTERNS_UNIQUE } from './constants'; +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; describe('get_data_telemetry', () => { describe('DATA_DATASETS_INDEX_PATTERNS', () => { @@ -195,13 +196,15 @@ describe('get_data_telemetry', () => { 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([]); + const esClient = mockEsClient(); + await expect(getDataTelemetry(esClient)).resolves.toStrictEqual([]); + expect(esClient.indices.getMapping).toHaveBeenCalledTimes(1); + expect(esClient.indices.stats).toHaveBeenCalledTimes(1); }); test('can only see the index mappings, but not the stats', async () => { - const callCluster = mockCallCluster(['filebeat-12314']); - await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + const esClient = mockEsClient(['filebeat-12314']); + await expect(getDataTelemetry(esClient)).resolves.toStrictEqual([ { pattern_name: 'filebeat', shipper: 'filebeat', @@ -209,10 +212,12 @@ describe('get_data_telemetry', () => { ecs_index_count: 0, }, ]); + expect(esClient.indices.getMapping).toHaveBeenCalledTimes(1); + expect(esClient.indices.stats).toHaveBeenCalledTimes(1); }); test('can see the mappings and the stats', async () => { - const callCluster = mockCallCluster( + const esClient = mockEsClient( ['filebeat-12314'], { isECS: true }, { @@ -221,7 +226,7 @@ describe('get_data_telemetry', () => { }, } ); - await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + await expect(getDataTelemetry(esClient)).resolves.toStrictEqual([ { pattern_name: 'filebeat', shipper: 'filebeat', @@ -234,7 +239,7 @@ describe('get_data_telemetry', () => { }); test('find an index that does not match any index pattern but has mappings metadata', async () => { - const callCluster = mockCallCluster( + const esClient = mockEsClient( ['cannot_match_anything'], { isECS: true, dataStreamType: 'traces', shipper: 'my-beat' }, { @@ -245,7 +250,7 @@ describe('get_data_telemetry', () => { }, } ); - await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + await expect(getDataTelemetry(esClient)).resolves.toStrictEqual([ { data_stream: { dataset: undefined, type: 'traces' }, shipper: 'my-beat', @@ -258,45 +263,51 @@ describe('get_data_telemetry', () => { }); 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([]); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.indices.getMapping.mockRejectedValue(new Error('Something went terribly wrong')); + esClient.indices.stats.mockRejectedValue(new Error('Something went terribly wrong')); + await expect(getDataTelemetry(esClient)).resolves.toStrictEqual([]); }); }); }); - -function mockCallCluster( - indicesMappings: string[] = [], +function mockEsClient( + indicesMappings: string[] = [], // an array of `indices` to get mappings from. { isECS = false, dataStreamDataset = '', dataStreamType = '', 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' } } } }), - ...((dataStreamType || dataStreamDataset) && { - data_stream: { - properties: { - ...(dataStreamDataset && { - dataset: { type: 'constant_keyword', value: dataStreamDataset }, - }), - ...(dataStreamType && { - type: { type: 'constant_keyword', value: dataStreamType }, - }), - }, + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // @ts-ignore + esClient.indices.getMapping.mockImplementationOnce(async () => { + const body = Object.fromEntries( + indicesMappings.map((index) => [ + index, + { + mappings: { + ...(shipper && { _meta: { beat: shipper } }), + properties: { + ...(isECS && { ecs: { properties: { version: { type: 'keyword' } } } }), + ...((dataStreamType || dataStreamDataset) && { + data_stream: { + properties: { + ...(dataStreamDataset && { + dataset: { type: 'constant_keyword', value: dataStreamDataset }, + }), + ...(dataStreamType && { + type: { type: 'constant_keyword', value: dataStreamType }, + }), }, - }), - }, + }, + }), }, }, - ]) - ); - } - return indexStats; + }, + ]) + ); + return { body }; + }); + // @ts-ignore + esClient.indices.stats.mockImplementationOnce(async () => { + return { body: indexStats }; }); + return esClient; } 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 index f4734dde251cc8..67769793cbfdf8 100644 --- 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 @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { ElasticsearchClient } from 'src/core/server'; -import { LegacyAPICaller } from 'kibana/server'; import { DATA_DATASETS_INDEX_PATTERNS_UNIQUE, DataPatternName, @@ -224,42 +224,50 @@ interface IndexMappings { }; } -export async function getDataTelemetry(callCluster: LegacyAPICaller) { +export async function getDataTelemetry(esClient: ElasticsearchClient) { try { const index = [ ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern), '*-*-*', // Include data-streams aliases `{type}-{dataset}-{namespace}` ]; - const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([ + const indexMappingsParams: { index: string; filter_path: string[] } = { // 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', - // _meta.package.name tells the Ingest Manager's package - '*.mappings._meta.package.name', - // _meta.managed_by is usually populated by Ingest Manager for the UI to identify it - '*.mappings._meta.managed_by', - // Does it have `ecs.version` in the mappings? => It follows the ECS conventions - '*.mappings.properties.ecs.properties.version.type', + index: '*', // Request all indices because filter_path already filters out the indices without any of those fields + filter_path: [ + // _meta.beat tells the shipper + '*.mappings._meta.beat', + // _meta.package.name tells the Ingest Manager's package + '*.mappings._meta.package.name', + // _meta.managed_by is usually populated by Ingest Manager for the UI to identify it + '*.mappings._meta.managed_by', + // Does it have `ecs.version` in the mappings? => It follows the ECS conventions + '*.mappings.properties.ecs.properties.version.type', - // If `data_stream.type` is a `constant_keyword`, it can be reported as a type - '*.mappings.properties.data_stream.properties.type.value', - // If `data_stream.dataset` is a `constant_keyword`, it can be reported as the dataset - '*.mappings.properties.data_stream.properties.dataset.value', - ], - }), + // If `data_stream.type` is a `constant_keyword`, it can be reported as a type + '*.mappings.properties.data_stream.properties.type.value', + // If `data_stream.dataset` is a `constant_keyword`, it can be reported as the dataset + '*.mappings.properties.data_stream.properties.dataset.value', + ], + }; + const indicesStatsParams: { + index: string | string[] | undefined; + level: 'cluster' | 'indices' | 'shards' | undefined; + metric: string[]; + filter_path: string[]; + } = { // GET /_stats/docs,store?level=indices&filter_path=indices.*.total - callCluster('indices.stats', { - index, - level: 'indices', - metric: ['docs', 'store'], - filterPath: ['indices.*.total'], - }), + index, + level: 'indices', + metric: ['docs', 'store'], + filter_path: ['indices.*.total'], + }; + const [{ body: indexMappings }, { body: indexStats }] = await Promise.all([ + esClient.indices.getMapping(indexMappingsParams), + esClient.indices.stats(indicesStatsParams), ]); const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices }); + const indices = indexNames.map((name) => { const baseIndexInfo = { name, 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 index d056d1c9f299f3..0e2ab98a24cbaa 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -20,8 +20,8 @@ export { DATA_TELEMETRY_ID } from './constants'; export { - DataTelemetryIndex, - DataTelemetryPayload, getDataTelemetry, buildDataTelemetryPayload, + DataTelemetryPayload, + DataTelemetryIndex, } from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 5d27774a630a59..0ef9815a4eadb1 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -21,6 +21,7 @@ import { omit } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { LegacyAPICaller } from 'kibana/server'; import { StatsCollectionContext } from 'src/plugins/telemetry_collection_manager/server'; +import { ElasticsearchClient } from 'src/core/server'; export interface KibanaUsageStats { kibana: { @@ -48,7 +49,6 @@ export function handleKibanaStats( logger.warn('No Kibana stats returned from usage collectors'); return; } - const { kibana, kibana_stats: kibanaStats, ...plugins } = response; const os = { @@ -83,8 +83,9 @@ export function handleKibanaStats( export async function getKibana( usageCollection: UsageCollectionSetup, - callWithInternalUser: LegacyAPICaller + callWithInternalUser: LegacyAPICaller, + asInternalUser: ElasticsearchClient ): Promise { - const usage = await usageCollection.bulkFetch(callWithInternalUser); + const usage = await usageCollection.bulkFetch(callWithInternalUser, asInternalUser); return usageCollection.toObject(usage); } diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts index d41904c6d8e0e8..879416cda62fc4 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_license.ts @@ -17,44 +17,41 @@ * under the License. */ -import { LegacyAPICaller } from 'kibana/server'; import { ESLicense, LicenseGetter } from 'src/plugins/telemetry_collection_manager/server'; +import { ElasticsearchClient } from 'src/core/server'; let cachedLicense: ESLicense | undefined; -function fetchLicense(callCluster: LegacyAPICaller, local: boolean) { - return callCluster<{ license: ESLicense }>('transport.request', { - method: 'GET', - path: '/_license', - query: { - local, - // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. - accept_enterprise: 'true', - }, +async function fetchLicense(esClient: ElasticsearchClient, local: boolean) { + const { body } = await esClient.license.get({ + local, + // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. + accept_enterprise: true, }); + return body; } - /** * Get the cluster's license from the connected node. * - * This is the equivalent of GET /_license?local=true . + * This is the equivalent of GET /_license?local=true&accept_enterprise=true. * * Like any X-Pack related API, X-Pack must installed for this to work. + * + * In OSS we'll get a 400 response using the new elasticsearch client. */ -async function getLicenseFromLocalOrMaster(callCluster: LegacyAPICaller) { - // Fetching the local license is cheaper than getting it from the master and good enough - const { license } = await fetchLicense(callCluster, true).catch(async (err) => { +async function getLicenseFromLocalOrMaster(esClient: ElasticsearchClient) { + // Fetching the local license is cheaper than getting it from the master node and good enough + const { license } = await fetchLicense(esClient, true).catch(async (err) => { if (cachedLicense) { try { // Fallback to the master node's license info - const response = await fetchLicense(callCluster, false); + const response = await fetchLicense(esClient, false); return response; } catch (masterError) { - if (masterError.statusCode === 404) { + if ([400, 404].includes(masterError.statusCode)) { // If the master node does not have a license, we can assume there is no license cachedLicense = undefined; } else { - // Any other errors from the master node, throw and do not send any telemetry throw err; } } @@ -68,9 +65,8 @@ async function getLicenseFromLocalOrMaster(callCluster: LegacyAPICaller) { return license; } -export const getLocalLicense: LicenseGetter = async (clustersDetails, { callCluster }) => { - const license = await getLicenseFromLocalOrMaster(callCluster); - +export const getLocalLicense: LicenseGetter = async (clustersDetails, { esClient }) => { + const license = await getLicenseFromLocalOrMaster(esClient); // It should be called only with 1 cluster element in the clustersDetails array, but doing reduce just in case. return clustersDetails.reduce((acc, { clusterUuid }) => ({ ...acc, [clusterUuid]: license }), {}); }; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts new file mode 100644 index 00000000000000..0c8b0b249f7d11 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.test.ts @@ -0,0 +1,259 @@ +/* + * 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 { merge, omit } from 'lodash'; + +import { getLocalStats, handleLocalStats } from './get_local_stats'; +import { usageCollectionPluginMock } from '../../../usage_collection/server/mocks'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; + +function mockUsageCollection(kibanaUsage = {}) { + const usageCollection = usageCollectionPluginMock.createSetupContract(); + usageCollection.bulkFetch = jest.fn().mockResolvedValue(kibanaUsage); + usageCollection.toObject = jest.fn().mockImplementation((data: any) => data); + return usageCollection; +} +// set up successful call mocks for info, cluster stats, nodes usage and data telemetry +function mockGetLocalStats(clusterInfo: any, clusterStats: any) { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.info + // @ts-ignore we only care about the response body + .mockResolvedValue( + // @ts-ignore we only care about the response body + { + body: { ...clusterInfo }, + } + ); + esClient.cluster.stats + // @ts-ignore we only care about the response body + .mockResolvedValue({ body: { ...clusterStats } }); + esClient.nodes.usage.mockResolvedValue( + // @ts-ignore we only care about the response body + { + body: { + cluster_name: 'testCluster', + nodes: { + some_node_id: { + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + create_index_action: 1, + document_get_action: 1, + search_action: 19, + nodes_info_action: 36, + }, + aggregations: { + terms: { + bytes: 2, + }, + scripted_metric: { + other: 7, + }, + }, + }, + }, + }, + } + ); + // @ts-ignore we only care about the response body + esClient.indices.getMapping.mockResolvedValue({ body: { mappings: {} } }); + // @ts-ignore we only care about the response body + esClient.indices.stats.mockResolvedValue({ body: { indices: {} } }); + return esClient; +} + +describe('get_local_stats', () => { + const clusterUuid = 'abc123'; + const clusterName = 'my-cool-cluster'; + const version = '2.3.4'; + const clusterInfo = { + cluster_uuid: clusterUuid, + cluster_name: clusterName, + version: { number: version }, + }; + const nodesUsage = [ + { + node_id: 'some_node_id', + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + create_index_action: 1, + document_get_action: 1, + search_action: 19, + nodes_info_action: 36, + }, + aggregations: { + terms: { + bytes: 2, + }, + scripted_metric: { + other: 7, + }, + }, + }, + ]; + const clusterStats = { + _nodes: { failed: 123 }, + cluster_name: 'real-cool', + indices: { totally: 456 }, + nodes: { yup: 'abc' }, + random: 123, + }; + + const kibana = { + kibana: { + great: 'googlymoogly', + versions: [{ version: '8675309', count: 1 }], + }, + kibana_stats: { + os: { + platform: 'rocky', + platformRelease: 'iv', + }, + }, + localization: { + locale: 'en', + labelsCount: 0, + integrities: {}, + }, + sun: { chances: 5 }, + clouds: { chances: 95 }, + rain: { chances: 2 }, + snow: { chances: 0 }, + }; + + const clusterStatsWithNodesUsage = { + ...clusterStats, + nodes: merge(clusterStats.nodes, { usage: { nodes: nodesUsage } }), + }; + + const combinedStatsResult = { + collection: 'local', + cluster_uuid: clusterUuid, + cluster_name: clusterName, + version, + cluster_stats: omit(clusterStatsWithNodesUsage, '_nodes', 'cluster_name'), + stack_stats: { + kibana: { + great: 'googlymoogly', + count: 1, + indices: 1, + os: { + platforms: [{ platform: 'rocky', count: 1 }], + platformReleases: [{ platformRelease: 'iv', count: 1 }], + }, + versions: [{ version: '8675309', count: 1 }], + plugins: { + localization: { + locale: 'en', + labelsCount: 0, + integrities: {}, + }, + sun: { chances: 5 }, + clouds: { chances: 95 }, + rain: { chances: 2 }, + snow: { chances: 0 }, + }, + }, + }, + }; + + const context = { + logger: console, + version: '8.0.0', + }; + + describe('handleLocalStats', () => { + it('returns expected object without xpack or kibana data', () => { + const result = handleLocalStats( + clusterInfo, + clusterStatsWithNodesUsage, + void 0, + void 0, + context + ); + expect(result.cluster_uuid).toStrictEqual(combinedStatsResult.cluster_uuid); + expect(result.cluster_name).toStrictEqual(combinedStatsResult.cluster_name); + expect(result.cluster_stats).toStrictEqual(combinedStatsResult.cluster_stats); + expect(result.version).toEqual('2.3.4'); + expect(result.collection).toEqual('local'); + expect(Object.keys(result)).not.toContain('license'); + expect(result.stack_stats).toEqual({ kibana: undefined, data: undefined }); + }); + + it('returns expected object with xpack', () => { + const result = handleLocalStats( + clusterInfo, + clusterStatsWithNodesUsage, + void 0, + void 0, + context + ); + + const { stack_stats: stack, ...cluster } = result; + expect(cluster.collection).toBe(combinedStatsResult.collection); + expect(cluster.cluster_uuid).toBe(combinedStatsResult.cluster_uuid); + expect(cluster.cluster_name).toBe(combinedStatsResult.cluster_name); + expect(stack.kibana).toBe(undefined); // not mocked for this test + expect(stack.data).toBe(undefined); // not mocked for this test + + expect(cluster.version).toEqual(combinedStatsResult.version); + expect(cluster.cluster_stats).toEqual(combinedStatsResult.cluster_stats); + expect(Object.keys(cluster).indexOf('license')).toBeLessThan(0); + expect(Object.keys(stack).indexOf('xpack')).toBeLessThan(0); + }); + }); + + describe('getLocalStats', () => { + it('returns expected object with kibana data', async () => { + const callCluster = jest.fn(); + const usageCollection = mockUsageCollection(kibana); + const esClient = mockGetLocalStats(clusterInfo, clusterStats); + const response = await getLocalStats( + [{ clusterUuid: 'abc123' }], + { callCluster, usageCollection, esClient, start: '', end: '' }, + context + ); + const result = response[0]; + expect(result.cluster_uuid).toEqual(combinedStatsResult.cluster_uuid); + expect(result.cluster_name).toEqual(combinedStatsResult.cluster_name); + expect(result.cluster_stats).toEqual(combinedStatsResult.cluster_stats); + expect(result.cluster_stats.nodes).toEqual(combinedStatsResult.cluster_stats.nodes); + expect(result.version).toBe('2.3.4'); + expect(result.collection).toBe('local'); + expect(Object.keys(result).indexOf('license')).toBeLessThan(0); + expect(Object.keys(result.stack_stats).indexOf('xpack')).toBeLessThan(0); + }); + + it('returns an empty array when no cluster uuid is provided', async () => { + const callCluster = jest.fn(); + const usageCollection = mockUsageCollection(kibana); + const esClient = mockGetLocalStats(clusterInfo, clusterStats); + const response = await getLocalStats( + [], + { callCluster, usageCollection, esClient, start: '', end: '' }, + context + ); + expect(response).toBeDefined(); + expect(response.length).toEqual(0); + }); + }); +}); 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 98c83a3394628b..6244c6fac51d39 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -40,8 +40,8 @@ export function handleLocalStats( // eslint-disable-next-line @typescript-eslint/naming-convention { cluster_name, cluster_uuid, version }: ESClusterInfo, { _nodes, cluster_name: clusterName, ...clusterStats }: any, - kibana: KibanaUsageStats, - dataTelemetry: DataTelemetryPayload, + kibana: KibanaUsageStats | undefined, + dataTelemetry: DataTelemetryPayload | undefined, context: StatsCollectionContext ) { return { @@ -62,22 +62,25 @@ export type TelemetryLocalStats = ReturnType; /** * Get statistics for all products joined by Elasticsearch cluster. + * @param {Array} cluster uuids + * @param {Object} config contains the new esClient already scoped contains usageCollection, callCluster, esClient, start, end + * @param {Object} StatsCollectionContext contains logger and version (string) */ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( - clustersDetails, - config, - context + clustersDetails, // array of cluster uuid's + config, // contains the new esClient already scoped contains usageCollection, callCluster, esClient, start, end + context // StatsCollectionContext contains logger and version (string) ) => { - const { callCluster, usageCollection } = config; + const { callCluster, usageCollection, esClient } = config; return await Promise.all( clustersDetails.map(async (clustersDetail) => { 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), + getClusterInfo(esClient), // cluster info + getClusterStats(esClient), // cluster stats (not to be confused with cluster _state_) + getNodesUsage(esClient), // nodes_usage info + getKibana(usageCollection, callCluster, esClient), + getDataTelemetry(esClient), ]); return handleLocalStats( clusterInfo, diff --git a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts index 4e4b0e11b79794..acf403ba254477 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts @@ -19,6 +19,7 @@ import { getNodesUsage } from './get_nodes_usage'; import { TIMEOUT } from './constants'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; const mockedNodesFetchResponse = { cluster_name: 'test cluster', @@ -44,37 +45,35 @@ const mockedNodesFetchResponse = { }, }, }; + describe('get_nodes_usage', () => { - it('calls fetchNodesUsage', async () => { - const callCluster = jest.fn(); - callCluster.mockResolvedValueOnce(mockedNodesFetchResponse); - await getNodesUsage(callCluster); - expect(callCluster).toHaveBeenCalledWith('transport.request', { - path: '/_nodes/usage', - method: 'GET', - query: { - timeout: TIMEOUT, - }, - }); - }); - it('returns a modified array of node usage data', async () => { - const callCluster = jest.fn(); - callCluster.mockResolvedValueOnce(mockedNodesFetchResponse); - const result = await getNodesUsage(callCluster); - expect(result.nodes).toEqual([ - { - aggregations: { scripted_metric: { other: 7 }, terms: { bytes: 2 } }, - node_id: 'some_node_id', - rest_actions: { - create_index_action: 1, - document_get_action: 1, - nodes_info_action: 36, - nodes_usage_action: 1, - search_action: 19, + it('returns a modified array of nodes usage data', async () => { + const response = Promise.resolve({ body: mockedNodesFetchResponse }); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + esClient.nodes.usage.mockImplementationOnce( + // @ts-ignore + async (_params = { timeout: TIMEOUT }) => { + return response; + } + ); + const item = await getNodesUsage(esClient); + expect(esClient.nodes.usage).toHaveBeenCalledWith({ timeout: TIMEOUT }); + expect(item).toStrictEqual({ + nodes: [ + { + aggregations: { scripted_metric: { other: 7 }, terms: { bytes: 2 } }, + node_id: 'some_node_id', + rest_actions: { + create_index_action: 1, + document_get_action: 1, + nodes_info_action: 36, + nodes_usage_action: 1, + search_action: 19, + }, + since: 1588616945163, + timestamp: 1588617023177, }, - since: 1588616945163, - timestamp: 1588617023177, - }, - ]); + ], + }); }); }); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts index c5c110fbb4149b..959840d0020a20 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'src/core/server'; import { TIMEOUT } from './constants'; export interface NodeAggregation { @@ -44,7 +44,7 @@ export interface NodesFeatureUsageResponse { } export type NodesUsageGetter = ( - callCluster: LegacyAPICaller + esClient: ElasticsearchClient ) => Promise<{ nodes: NodeObj[] | Array<{}> }>; /** * Get the nodes usage data from the connected cluster. @@ -54,16 +54,12 @@ export type NodesUsageGetter = ( * The Nodes usage API was introduced in v6.0.0 */ export async function fetchNodesUsage( - callCluster: LegacyAPICaller + esClient: ElasticsearchClient ): Promise { - const response = await callCluster('transport.request', { - method: 'GET', - path: '/_nodes/usage', - query: { - timeout: TIMEOUT, - }, + const { body } = await esClient.nodes.usage({ + timeout: TIMEOUT, }); - return response; + return body; } /** @@ -71,8 +67,8 @@ export async function fetchNodesUsage( * @param callCluster APICaller * @returns Object containing array of modified usage information with the node_id nested within the data for that node. */ -export const getNodesUsage: NodesUsageGetter = async (callCluster) => { - const result = await fetchNodesUsage(callCluster); +export const getNodesUsage: NodesUsageGetter = async (esClient) => { + const result = await fetchNodesUsage(esClient); const transformedNodes = Object.entries(result?.nodes || {}).map(([key, value]) => ({ ...(value as NodeObj), node_id: key, diff --git a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts index 438fcadad92555..9dac4900f5f108 100644 --- a/src/plugins/telemetry/server/telemetry_collection/register_collection.ts +++ b/src/plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -38,16 +38,19 @@ import { ILegacyClusterClient } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; +import { IClusterClient } from '../../../../../src/core/server'; import { getLocalStats } from './get_local_stats'; import { getClusterUuids } from './get_cluster_stats'; import { getLocalLicense } from './get_local_license'; export function registerCollection( telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, - esCluster: ILegacyClusterClient + esCluster: ILegacyClusterClient, + esClientGetter: () => IClusterClient | undefined ) { telemetryCollectionManager.setCollection({ esCluster, + esClientGetter, title: 'local', priority: 0, statsGetter: getLocalStats, diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index 051bb3a11cb16c..e54e7451a670af 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -24,6 +24,7 @@ import { CoreStart, Plugin, Logger, + IClusterClient, } from '../../../core/server'; import { @@ -86,6 +87,7 @@ export class TelemetryCollectionManagerPlugin title, priority, esCluster, + esClientGetter, statsGetter, clusterDetailsGetter, licenseGetter, @@ -105,6 +107,9 @@ export class TelemetryCollectionManagerPlugin if (!esCluster) { throw Error('esCluster name must be set for the getCluster method.'); } + if (!esClientGetter) { + throw Error('esClientGetter method not set.'); + } if (!clusterDetailsGetter) { throw Error('Cluster UUIds method is not set.'); } @@ -118,6 +123,7 @@ export class TelemetryCollectionManagerPlugin clusterDetailsGetter, esCluster, title, + esClientGetter, }); this.usageGetterMethodPriority = priority; } @@ -126,6 +132,7 @@ export class TelemetryCollectionManagerPlugin private getStatsCollectionConfig( config: StatsGetterConfig, collection: Collection, + collectionEsClient: IClusterClient, usageCollection: UsageCollectionSetup ): StatsCollectionConfig { const { start, end, request } = config; @@ -133,8 +140,11 @@ export class TelemetryCollectionManagerPlugin const callCluster = config.unencrypted ? collection.esCluster.asScoped(request).callAsCurrentUser : collection.esCluster.callAsInternalUser; - - return { callCluster, start, end, usageCollection }; + // Scope the new elasticsearch Client appropriately and pass to the stats collection config + const esClient = config.unencrypted + ? collectionEsClient.asScoped(config.request).asCurrentUser + : collectionEsClient.asInternalUser; + return { callCluster, start, end, usageCollection, esClient }; } private async getOptInStats(optInStatus: boolean, config: StatsGetterConfig) { @@ -142,27 +152,33 @@ export class TelemetryCollectionManagerPlugin return []; } for (const collection of this.collections) { - const statsCollectionConfig = this.getStatsCollectionConfig( - config, - collection, - this.usageCollection - ); - try { - const optInStats = await this.getOptInStatsForCollection( + // first fetch the client and make sure it's not undefined. + const collectionEsClient = collection.esClientGetter(); + if (collectionEsClient !== undefined) { + const statsCollectionConfig = this.getStatsCollectionConfig( + config, collection, - optInStatus, - statsCollectionConfig + collectionEsClient, + this.usageCollection ); - if (optInStats && optInStats.length) { - this.logger.debug(`Got Opt In stats using ${collection.title} collection.`); - if (config.unencrypted) { - return optInStats; + + try { + const optInStats = await this.getOptInStatsForCollection( + collection, + optInStatus, + statsCollectionConfig + ); + if (optInStats && optInStats.length) { + this.logger.debug(`Got Opt In stats using ${collection.title} collection.`); + if (config.unencrypted) { + return optInStats; + } + return encryptTelemetry(optInStats, { useProdKey: this.isDistributable }); } - return encryptTelemetry(optInStats, { useProdKey: this.isDistributable }); + } catch (err) { + this.logger.debug(`Failed to collect any opt in stats with registered collections.`); + // swallow error to try next collection; } - } catch (err) { - this.logger.debug(`Failed to collect any opt in stats with registered collections.`); - // swallow error to try next collection; } } @@ -192,28 +208,32 @@ export class TelemetryCollectionManagerPlugin return []; } for (const collection of this.collections) { - const statsCollectionConfig = this.getStatsCollectionConfig( - config, - collection, - this.usageCollection - ); - try { - const usageData = await this.getUsageForCollection(collection, statsCollectionConfig); - if (usageData.length) { - this.logger.debug(`Got Usage using ${collection.title} collection.`); - if (config.unencrypted) { - return usageData; - } + const collectionEsClient = collection.esClientGetter(); + if (collectionEsClient !== undefined) { + const statsCollectionConfig = this.getStatsCollectionConfig( + config, + collection, + collectionEsClient, + this.usageCollection + ); + try { + const usageData = await this.getUsageForCollection(collection, statsCollectionConfig); + if (usageData.length) { + this.logger.debug(`Got Usage using ${collection.title} collection.`); + if (config.unencrypted) { + return usageData; + } - return encryptTelemetry(usageData.filter(isClusterOptedIn), { - useProdKey: this.isDistributable, - }); + return encryptTelemetry(usageData.filter(isClusterOptedIn), { + useProdKey: this.isDistributable, + }); + } + } catch (err) { + this.logger.debug( + `Failed to collect any usage with registered collection ${collection.title}.` + ); + // swallow error to try next collection; } - } catch (err) { - this.logger.debug( - `Failed to collect any usage with registered collection ${collection.title}.` - ); - // swallow error to try next collection; } } diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 16f96c07fd8ead..44970df30fd16b 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -17,8 +17,15 @@ * under the License. */ -import { LegacyAPICaller, Logger, KibanaRequest, ILegacyClusterClient } from 'kibana/server'; +import { + LegacyAPICaller, + Logger, + KibanaRequest, + ILegacyClusterClient, + IClusterClient, +} from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { ElasticsearchClient } from '../../../../src/core/server'; import { TelemetryCollectionManagerPlugin } from './plugin'; export interface TelemetryCollectionManagerPluginSetup { @@ -67,6 +74,7 @@ export interface StatsCollectionConfig { callCluster: LegacyAPICaller; start: string | number; end: string | number; + esClient: ElasticsearchClient; } export interface BasicStatsPayload { @@ -100,7 +108,7 @@ export interface ESLicense { } export interface StatsCollectionContext { - logger: Logger; + logger: Logger | Console; version: string; } @@ -130,6 +138,7 @@ export interface CollectionConfig< title: string; priority: number; esCluster: ILegacyClusterClient; + esClientGetter: () => IClusterClient | undefined; // --> by now we know that the client getter will return the IClusterClient but we assure that through a code check statsGetter: StatsGetter; clusterDetailsGetter: ClusterDetailsGetter; licenseGetter: LicenseGetter; @@ -145,5 +154,6 @@ export interface Collection< licenseGetter: LicenseGetter; clusterDetailsGetter: ClusterDetailsGetter; esCluster: ILegacyClusterClient; + esClientGetter: () => IClusterClient | undefined; // the collection could still return undefined for the es client getter. title: string; } diff --git a/src/plugins/tile_map/public/geohash_layer.js b/src/plugins/tile_map/public/geohash_layer.js index ca2f49a1f31e00..ca992a0d09ec99 100644 --- a/src/plugins/tile_map/public/geohash_layer.js +++ b/src/plugins/tile_map/public/geohash_layer.js @@ -19,14 +19,14 @@ import { min, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { L, KibanaMapLayer, MapTypes } from '../../maps_legacy/public'; +import { KibanaMapLayer, MapTypes } from '../../maps_legacy/public'; import { HeatmapMarkers } from './markers/heatmap'; import { ScaledCirclesMarkers } from './markers/scaled_circles'; import { ShadedCirclesMarkers } from './markers/shaded_circles'; import { GeohashGridMarkers } from './markers/geohash_grid'; export class GeohashLayer extends KibanaMapLayer { - constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap) { + constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap, leaflet) { super(); this._featureCollection = featureCollection; @@ -35,7 +35,8 @@ export class GeohashLayer extends KibanaMapLayer { this._geohashOptions = options; this._zoom = zoom; this._kibanaMap = kibanaMap; - const geojson = L.geoJson(this._featureCollection); + this._leaflet = leaflet; + const geojson = this._leaflet.geoJson(this._featureCollection); this._bounds = geojson.getBounds(); this._createGeohashMarkers(); this._lastBounds = null; @@ -56,7 +57,8 @@ export class GeohashLayer extends KibanaMapLayer { this._featureCollectionMetaData, markerOptions, this._zoom, - this._kibanaMap + this._kibanaMap, + this._leaflet ); break; case MapTypes.ShadedCircleMarkers: @@ -65,7 +67,8 @@ export class GeohashLayer extends KibanaMapLayer { this._featureCollectionMetaData, markerOptions, this._zoom, - this._kibanaMap + this._kibanaMap, + this._leaflet ); break; case MapTypes.ShadedGeohashGrid: @@ -74,7 +77,8 @@ export class GeohashLayer extends KibanaMapLayer { this._featureCollectionMetaData, markerOptions, this._zoom, - this._kibanaMap + this._kibanaMap, + this._leaflet ); break; case MapTypes.Heatmap: @@ -95,7 +99,8 @@ export class GeohashLayer extends KibanaMapLayer { tooltipFormatter: this._geohashOptions.tooltipFormatter, }, this._zoom, - this._featureCollectionMetaData.max + this._featureCollectionMetaData.max, + this._leaflet ); break; default: @@ -126,9 +131,15 @@ export class GeohashLayer extends KibanaMapLayer { if (this._geohashOptions.fetchBounds) { const geoHashBounds = await this._geohashOptions.fetchBounds(); if (geoHashBounds) { - const northEast = L.latLng(geoHashBounds.top_left.lat, geoHashBounds.bottom_right.lon); - const southWest = L.latLng(geoHashBounds.bottom_right.lat, geoHashBounds.top_left.lon); - return L.latLngBounds(southWest, northEast); + const northEast = this._leaflet.latLng( + geoHashBounds.top_left.lat, + geoHashBounds.bottom_right.lon + ); + const southWest = this._leaflet.latLng( + geoHashBounds.bottom_right.lat, + geoHashBounds.top_left.lon + ); + return this._leaflet.latLngBounds(southWest, northEast); } } diff --git a/src/plugins/tile_map/public/markers/geohash_grid.js b/src/plugins/tile_map/public/markers/geohash_grid.js index 46e7987c601f58..d81e435a6dd5d6 100644 --- a/src/plugins/tile_map/public/markers/geohash_grid.js +++ b/src/plugins/tile_map/public/markers/geohash_grid.js @@ -18,11 +18,10 @@ */ import { ScaledCirclesMarkers } from './scaled_circles'; -import { L } from '../../../maps_legacy/public'; export class GeohashGridMarkers extends ScaledCirclesMarkers { getMarkerFunction() { - return function (feature) { + return (feature) => { const geohashRect = feature.properties.geohash_meta.rectangle; // get bounds from northEast[3] and southWest[1] // corners in geohash rectangle @@ -30,7 +29,7 @@ export class GeohashGridMarkers extends ScaledCirclesMarkers { [geohashRect[3][0], geohashRect[3][1]], [geohashRect[1][0], geohashRect[1][1]], ]; - return L.rectangle(corners); + return this._leaflet.rectangle(corners); }; } } diff --git a/src/plugins/tile_map/public/markers/heatmap.js b/src/plugins/tile_map/public/markers/heatmap.js index f2d014797bce03..79bbee16548ac2 100644 --- a/src/plugins/tile_map/public/markers/heatmap.js +++ b/src/plugins/tile_map/public/markers/heatmap.js @@ -20,7 +20,6 @@ import _ from 'lodash'; import d3 from 'd3'; import { EventEmitter } from 'events'; -import { L } from '../../../maps_legacy/public'; /** * Map overlay: canvas layer with leaflet.heat plugin @@ -30,17 +29,17 @@ import { L } from '../../../maps_legacy/public'; * @param params {Object} */ export class HeatmapMarkers extends EventEmitter { - constructor(featureCollection, options, zoom, max) { + constructor(featureCollection, options, zoom, max, leaflet) { super(); this._geojsonFeatureCollection = featureCollection; const points = dataToHeatArray(featureCollection, max); - this._leafletLayer = new L.HeatLayer(points, options); + this._leafletLayer = new leaflet.HeatLayer(points, options); this._tooltipFormatter = options.tooltipFormatter; this._zoom = zoom; this._disableTooltips = false; this._getLatLng = _.memoize( function (feature) { - return L.latLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0]); + return leaflet.latLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0]); }, function (feature) { // turn coords into a string for the memoize cache diff --git a/src/plugins/tile_map/public/markers/scaled_circles.js b/src/plugins/tile_map/public/markers/scaled_circles.js index cb111107f6fe3b..4a5f1b452580bb 100644 --- a/src/plugins/tile_map/public/markers/scaled_circles.js +++ b/src/plugins/tile_map/public/markers/scaled_circles.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import d3 from 'd3'; import $ from 'jquery'; import { EventEmitter } from 'events'; -import { L, colorUtil } from '../../../maps_legacy/public'; +import { colorUtil } from '../../../maps_legacy/public'; import { truncatedColorMaps } from '../../../charts/public'; export class ScaledCirclesMarkers extends EventEmitter { @@ -31,14 +31,13 @@ export class ScaledCirclesMarkers extends EventEmitter { options, targetZoom, kibanaMap, - metricAgg + leaflet ) { super(); this._featureCollection = featureCollection; this._featureCollectionMetaData = featureCollectionMetaData; this._zoom = targetZoom; - this._metricAgg = metricAgg; this._valueFormatter = options.valueFormatter || @@ -55,6 +54,7 @@ export class ScaledCirclesMarkers extends EventEmitter { this._legendColors = null; this._legendQuantizer = null; + this._leaflet = leaflet; this._popups = []; @@ -72,7 +72,7 @@ export class ScaledCirclesMarkers extends EventEmitter { return kibanaMap.isInside(bucketRectBounds); }; } - this._leafletLayer = L.geoJson(null, layerOptions); + this._leafletLayer = this._leaflet.geoJson(null, layerOptions); this._leafletLayer.addData(this._featureCollection); } @@ -143,7 +143,7 @@ export class ScaledCirclesMarkers extends EventEmitter { mouseover: (e) => { const layer = e.target; // bring layer to front if not older browser - if (!L.Browser.ie && !L.Browser.opera) { + if (!this._leaflet.Browser.ie && !this._leaflet.Browser.opera) { layer.bringToFront(); } this._showTooltip(feature); @@ -170,7 +170,10 @@ export class ScaledCirclesMarkers extends EventEmitter { return; } - const latLng = L.latLng(feature.geometry.coordinates[1], feature.geometry.coordinates[0]); + const latLng = this._leaflet.latLng( + feature.geometry.coordinates[1], + feature.geometry.coordinates[0] + ); this.emit('showTooltip', { content: content, position: latLng, @@ -182,7 +185,7 @@ export class ScaledCirclesMarkers extends EventEmitter { return (feature, latlng) => { const value = feature.properties.value; const scaledRadius = this._radiusScale(value) * scaleFactor; - return L.circleMarker(latlng).setRadius(scaledRadius); + return this._leaflet.circleMarker(latlng).setRadius(scaledRadius); }; } diff --git a/src/plugins/tile_map/public/markers/shaded_circles.js b/src/plugins/tile_map/public/markers/shaded_circles.js index 745d0422856c68..3468cab7d8b75c 100644 --- a/src/plugins/tile_map/public/markers/shaded_circles.js +++ b/src/plugins/tile_map/public/markers/shaded_circles.js @@ -19,7 +19,6 @@ import _ from 'lodash'; import { ScaledCirclesMarkers } from './scaled_circles'; -import { L } from '../../../maps_legacy/public'; export class ShadedCirclesMarkers extends ScaledCirclesMarkers { getMarkerFunction() { @@ -27,7 +26,7 @@ export class ShadedCirclesMarkers extends ScaledCirclesMarkers { const scaleFactor = 0.8; return (feature, latlng) => { const radius = this._geohashMinDistance(feature) * scaleFactor; - return L.circle(latlng, radius); + return this._leaflet.circle(latlng, radius); }; } @@ -49,12 +48,12 @@ export class ShadedCirclesMarkers extends ScaledCirclesMarkers { // clockwise, each value being an array of [lat, lng] // center lat and southeast lng - const east = L.latLng([centerPoint[0], geohashRect[2][1]]); + const east = this._leaflet.latLng([centerPoint[0], geohashRect[2][1]]); // southwest lat and center lng - const north = L.latLng([geohashRect[3][0], centerPoint[1]]); + const north = this._leaflet.latLng([geohashRect[3][0], centerPoint[1]]); // get latLng of geohash center point - const center = L.latLng([centerPoint[0], centerPoint[1]]); + const center = this._leaflet.latLng([centerPoint[0], centerPoint[1]]); // get smallest radius at center of geohash grid rectangle const eastRadius = Math.floor(center.distanceTo(east)); diff --git a/src/plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts index 9a164f8a303f84..07add6901fb498 100644 --- a/src/plugins/tile_map/public/plugin.ts +++ b/src/plugins/tile_map/public/plugin.ts @@ -47,7 +47,7 @@ interface TileMapVisualizationDependencies { getZoomPrecision: any; getPrecision: any; BaseMapsVisualization: any; - serviceSettings: IServiceSettings; + getServiceSettings: () => Promise; } /** @internal */ @@ -81,13 +81,13 @@ export class TileMapPlugin implements Plugin = { getZoomPrecision, getPrecision, BaseMapsVisualization: mapsLegacy.getBaseMapsVis(), uiSettings: core.uiSettings, - serviceSettings, + getServiceSettings, }; expressions.registerFunction(() => createTileMapFn(visualizationDependencies)); diff --git a/src/plugins/tile_map/public/tile_map_type.js b/src/plugins/tile_map/public/tile_map_type.js index f76da26022a778..2b23f345f012e4 100644 --- a/src/plugins/tile_map/public/tile_map_type.js +++ b/src/plugins/tile_map/public/tile_map_type.js @@ -28,7 +28,7 @@ import { truncatedColorSchemas } from '../../charts/public'; export function createTileMapTypeDefinition(dependencies) { const CoordinateMapsVisualization = createTileMapVisualization(dependencies); - const { uiSettings, serviceSettings } = dependencies; + const { uiSettings, getServiceSettings } = dependencies; return { name: 'tile_map', @@ -142,6 +142,7 @@ export function createTileMapTypeDefinition(dependencies) { let tmsLayers; try { + const serviceSettings = await getServiceSettings(); tmsLayers = await serviceSettings.getTMSServices(); } catch (e) { return vis; diff --git a/src/plugins/tile_map/public/tile_map_visualization.js b/src/plugins/tile_map/public/tile_map_visualization.js index 2ebb76d05c2195..b09a2f3bac48f4 100644 --- a/src/plugins/tile_map/public/tile_map_visualization.js +++ b/src/plugins/tile_map/public/tile_map_visualization.js @@ -17,12 +17,42 @@ * under the License. */ -import { get } from 'lodash'; -import { GeohashLayer } from './geohash_layer'; +import { get, round } from 'lodash'; import { getFormatService, getQueryService, getKibanaLegacy } from './services'; -import { scaleBounds, geoContains, mapTooltipProvider } from '../../maps_legacy/public'; +import { + geoContains, + mapTooltipProvider, + lazyLoadMapsLegacyModules, +} from '../../maps_legacy/public'; import { tooltipFormatter } from './tooltip_formatter'; +function scaleBounds(bounds) { + const scale = 0.5; // scale bounds by 50% + + const topLeft = bounds.top_left; + const bottomRight = bounds.bottom_right; + let latDiff = round(Math.abs(topLeft.lat - bottomRight.lat), 5); + const lonDiff = round(Math.abs(bottomRight.lon - topLeft.lon), 5); + // map height can be zero when vis is first created + if (latDiff === 0) latDiff = lonDiff; + + const latDelta = latDiff * scale; + let topLeftLat = round(topLeft.lat, 5) + latDelta; + if (topLeftLat > 90) topLeftLat = 90; + let bottomRightLat = round(bottomRight.lat, 5) - latDelta; + if (bottomRightLat < -90) bottomRightLat = -90; + const lonDelta = lonDiff * scale; + let topLeftLon = round(topLeft.lon, 5) - lonDelta; + if (topLeftLon < -180) topLeftLon = -180; + let bottomRightLon = round(bottomRight.lon, 5) + lonDelta; + if (bottomRightLon > 180) bottomRightLon = 180; + + return { + top_left: { lat: topLeftLat, lon: topLeftLon }, + bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, + }; +} + export const createTileMapVisualization = (dependencies) => { const { getZoomPrecision, getPrecision, BaseMapsVisualization } = dependencies; @@ -147,7 +177,9 @@ export const createTileMapVisualization = (dependencies) => { this._recreateGeohashLayer(); } - _recreateGeohashLayer() { + async _recreateGeohashLayer() { + const { GeohashLayer } = await import('./geohash_layer'); + if (this._geohashLayer) { this._kibanaMap.removeLayer(this._geohashLayer); this._geohashLayer = null; @@ -158,7 +190,8 @@ export const createTileMapVisualization = (dependencies) => { this._geoJsonFeatureCollectionAndMeta.meta, geohashOptions, this._kibanaMap.getZoomLevel(), - this._kibanaMap + this._kibanaMap, + (await lazyLoadMapsLegacyModules()).L ); this._kibanaMap.addLayer(this._geohashLayer); } diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 0b1cca07de007d..d8edc5bb8d18a7 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -63,7 +63,7 @@ All you need to provide is a `type` for organizing your fields, `schema` field t total: 'long', }, }, - fetch: async (callCluster: APICluster) => { + fetch: async (callCluster: APICluster, esClient: IClusterClient) => { // query ES and get some data // summarize the data into a model @@ -86,9 +86,9 @@ Some background: - `MY_USAGE_TYPE` can be any string. It usually matches the plugin name. As a safety mechanism, we double check there are no duplicates at the moment of registering the collector. - The `fetch` method needs to support multiple contexts in which it is called. For example, when stats are pulled from a Kibana Metricbeat module, the Beat calls Kibana's stats API to invoke usage collection. -In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest`, where the request headers are expected to have read privilege on the entire `.kibana' index. +In this case, the `fetch` method is called as a result of an HTTP API request and `callCluster` wraps `callWithRequest` or `esClient` wraps `asCurrentUser`, where the request headers are expected to have read privilege on the entire `.kibana' index. -Note: there will be many cases where you won't need to use the `callCluster` function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS, or use other clients like a custom SavedObjects client. In that case it's up to the plugin to initialize those clients like the example below: +Note: there will be many cases where you won't need to use the `callCluster` (or `esClient`) function that gets passed in to your `fetch` method at all. Your feature might have an accumulating value in server memory, or read something from the OS, or use other clients like a custom SavedObjects client. In that case it's up to the plugin to initialize those clients like the example below: ```ts // server/plugin.ts @@ -302,4 +302,4 @@ These saved objects are automatically consumed by the stats API and surfaced und By storing these metrics and their counts as key-value pairs, we can add more metrics without having to worry about exceeding the 1000-field soft limit in Elasticsearch. -The only caveat is that it makes it harder to consume in Kibana when analysing each entry in the array separately. In the telemetry team we are working to find a solution to this. We are building a new way of reporting telemetry called [Pulse](../../../rfcs/text/0008_pulse.md) that will help on making these UI-Metrics easier to consume. +The only caveat is that it makes it harder to consume in Kibana when analysing each entry in the array separately. In the telemetry team we are working to find a solution to this. diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index d57700024c088f..365e1ce2013370 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Logger, LegacyAPICaller } from 'kibana/server'; +import { Logger, LegacyAPICaller, ElasticsearchClient } from 'kibana/server'; export type CollectorFormatForBulkUpload = (result: T) => { type: string; payload: U }; @@ -48,7 +48,7 @@ export interface CollectorOptions { type: string; init?: Function; schema?: MakeSchemaFrom>; // Using Required to enforce all optional keys in the object - fetch: (callCluster: LegacyAPICaller) => Promise | T; + fetch: (callCluster: LegacyAPICaller, esClient?: ElasticsearchClient) => Promise | T; /* * A hook for allowing the fetched data payload to be organized into a typed * data model for internal bulk upload. See defaultFormatterForBulkUpload for diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 545642c5dcfa3b..3f943ad8bf2ffd 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -21,7 +21,7 @@ import { noop } from 'lodash'; import { Collector } from './collector'; import { CollectorSet } from './collector_set'; import { UsageCollector } from './usage_collector'; -import { loggingSystemMock } from '../../../../core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from '../../../../core/server/mocks'; const logger = loggingSystemMock.createLogger(); @@ -42,6 +42,7 @@ describe('CollectorSet', () => { }); const mockCallCluster = jest.fn().mockResolvedValue({ passTest: 1000 }); + const mockEsClient = elasticsearchServiceMock.createClusterClient().asInternalUser; it('should throw an error if non-Collector type of object is registered', () => { const collectors = new CollectorSet({ logger }); @@ -85,7 +86,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); expect(loggerSpies.debug).toHaveBeenCalledTimes(1); expect(loggerSpies.debug).toHaveBeenCalledWith( 'Fetching data from MY_TEST_COLLECTOR collector' @@ -110,7 +111,7 @@ describe('CollectorSet', () => { let result; try { - result = await collectors.bulkFetch(mockCallCluster); + result = await collectors.bulkFetch(mockCallCluster, mockEsClient); } catch (err) { // Do nothing } @@ -128,7 +129,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -146,7 +147,7 @@ describe('CollectorSet', () => { } as any) ); - const result = await collectors.bulkFetch(mockCallCluster); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', @@ -169,7 +170,7 @@ describe('CollectorSet', () => { }) ); - const result = await collectors.bulkFetch(mockCallCluster); + const result = await collectors.bulkFetch(mockCallCluster, mockEsClient); expect(result).toStrictEqual([ { type: 'MY_TEST_COLLECTOR', diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index fce17a46b71689..6861be7f4f76b1 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -18,7 +18,7 @@ */ import { snakeCase } from 'lodash'; -import { Logger, LegacyAPICaller } from 'kibana/server'; +import { Logger, LegacyAPICaller, ElasticsearchClient } from 'kibana/server'; import { Collector, CollectorOptions } from './collector'; import { UsageCollector } from './usage_collector'; @@ -117,8 +117,12 @@ export class CollectorSet { return allReady; }; + // all collections eventually pass through bulkFetch. + // the shape of the response is different when using the new ES client as is the error handling. + // We'll handle the refactor for using the new client in a follow up PR. public bulkFetch = async ( callCluster: LegacyAPICaller, + esClient: ElasticsearchClient, collectors: Map> = this.collectors ) => { const responses = await Promise.all( @@ -127,7 +131,7 @@ export class CollectorSet { try { return { type: collector.type, - result: await collector.fetch(callCluster), + result: await collector.fetch(callCluster, esClient), // each collector must ensure they handle the response appropriately. }; } catch (err) { this.logger.warn(err); @@ -149,9 +153,9 @@ export class CollectorSet { return this.makeCollectorSetFromArray(filtered); }; - public bulkFetchUsage = async (callCluster: LegacyAPICaller) => { + public bulkFetchUsage = async (callCluster: LegacyAPICaller, esClient: ElasticsearchClient) => { const usageCollectors = this.getFilteredCollectorSet((c) => c instanceof UsageCollector); - return await this.bulkFetch(callCluster, usageCollectors.collectors); + return await this.bulkFetch(callCluster, esClient, usageCollectors.collectors); }; // convert an array of fetched stats results into key/object diff --git a/src/plugins/usage_collection/server/routes/stats.ts b/src/plugins/usage_collection/server/routes/stats.ts index 7c64c9f180319b..ef5da2eb11ba6a 100644 --- a/src/plugins/usage_collection/server/routes/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats.ts @@ -24,6 +24,7 @@ import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { + ElasticsearchClient, IRouter, LegacyAPICaller, MetricsServiceSetup, @@ -61,8 +62,11 @@ export function registerStatsRoute({ metrics: MetricsServiceSetup; overallStatus$: Observable; }) { - const getUsage = async (callCluster: LegacyAPICaller): Promise => { - const usage = await collectorSet.bulkFetchUsage(callCluster); + const getUsage = async ( + callCluster: LegacyAPICaller, + esClient: ElasticsearchClient + ): Promise => { + const usage = await collectorSet.bulkFetchUsage(callCluster, esClient); return collectorSet.toObject(usage); }; @@ -96,13 +100,14 @@ export function registerStatsRoute({ let extended; if (isExtended) { const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const esClient = context.core.elasticsearch.client.asCurrentUser; const collectorsReady = await collectorSet.areAllCollectorsReady(); if (shouldGetUsage && !collectorsReady) { return res.customError({ statusCode: 503, body: { message: STATS_NOT_READY_MESSAGE } }); } - const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve({}); + const usagePromise = shouldGetUsage ? getUsage(callCluster, esClient) : Promise.resolve({}); const [usage, clusterUuid] = await Promise.all([usagePromise, getClusterUuid(callCluster)]); let modifiedUsage = usage; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index 4883a8129903ab..5c7656efe925b0 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -162,12 +162,15 @@ describe('VegaParser._resolveEsQueries', () => { function check(spec, expected, warnCount) { return async () => { - const vp = new VegaParser(spec, searchApiStub, 0, 0, { - getFileLayers: async () => [{ name: 'file1', url: 'url1' }], - getUrlForRegionLayer: async (layer) => { - return layer.url; - }, - }); + const mockGetServiceSettings = async () => { + return { + getFileLayers: async () => [{ name: 'file1', url: 'url1' }], + getUrlForRegionLayer: async (layer) => { + return layer.url; + }, + }; + }; + const vp = new VegaParser(spec, searchApiStub, 0, 0, mockGetServiceSettings); await vp._resolveDataUrls(); expect(vp.spec).toEqual(expected); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index b3fb2d54cb2669..ccb89207e222fc 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -65,7 +65,7 @@ export class VegaParser { hideWarnings: boolean; error?: string; warnings: string[]; - _urlParsers: UrlParserConfig; + _urlParsers: UrlParserConfig | undefined; isVegaLite?: boolean; useHover?: boolean; _config?: VegaConfig; @@ -80,13 +80,16 @@ export class VegaParser { containerDir?: ControlsLocation | ControlsDirection; controlsDir?: ControlsLocation; searchAPI: SearchAPI; + getServiceSettings: () => Promise; + filters: Bool; + timeCache: TimeCache; constructor( spec: VegaSpec | string, searchAPI: SearchAPI, timeCache: TimeCache, filters: Bool, - serviceSettings: IServiceSettings + getServiceSettings: () => Promise ) { this.spec = spec as VegaSpec; this.hideWarnings = false; @@ -94,13 +97,9 @@ export class VegaParser { this.error = undefined; this.warnings = []; this.searchAPI = searchAPI; - - const onWarn = this._onWarning.bind(this); - this._urlParsers = { - elasticsearch: new EsQueryParser(timeCache, this.searchAPI, filters, onWarn), - emsfile: new EmsFileParser(serviceSettings), - url: new UrlParser(onWarn), - }; + this.getServiceSettings = getServiceSettings; + this.filters = filters; + this.timeCache = timeCache; } async parseAsync() { @@ -123,7 +122,7 @@ export class VegaParser { throw new Error( i18n.translate('visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage', { defaultMessage: `Your specification requires a {schemaParam} field with a valid URL for -Vega (see {vegaSchemaUrl}) or +Vega (see {vegaSchemaUrl}) or Vega-Lite (see {vegaLiteSchemaUrl}). The URL is an identifier only. Kibana and your browser will never access this URL.`, values: { @@ -547,6 +546,15 @@ The URL is an identifier only. Kibana and your browser will never access this UR * @private */ async _resolveDataUrls() { + if (!this._urlParsers) { + const serviceSettings = await this.getServiceSettings(); + const onWarn = this._onWarning.bind(this); + this._urlParsers = { + elasticsearch: new EsQueryParser(this.timeCache, this.searchAPI, this.filters, onWarn), + emsfile: new EmsFileParser(serviceSettings), + url: new UrlParser(onWarn), + }; + } const pending: PendingType = {}; this.searchAPI.resetSearchStats(); @@ -560,7 +568,7 @@ The URL is an identifier only. Kibana and your browser will never access this UR type = DEFAULT_PARSER; } - const parser = this._urlParsers[type]; + const parser = this._urlParsers![type]; if (parser === undefined) { throw new Error( i18n.translate('visTypeVega.vegaParser.notSupportedUrlTypeErrorMessage', { @@ -584,7 +592,7 @@ The URL is an identifier only. Kibana and your browser will never access this UR if (pendingParsers.length > 0) { // let each parser populate its data in parallel await Promise.all( - pendingParsers.map((type) => this._urlParsers[type].populateData(pending[type])) + pendingParsers.map((type) => this._urlParsers![type].populateData(pending[type])) ); } } diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 4b8ff8e2cb43a5..ce5c5130961c6b 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -28,7 +28,6 @@ import { setSavedObjects, setInjectedVars, setUISettings, - setKibanaMapFactory, setMapsLegacyConfig, setInjectedMetadata, } from './services'; @@ -47,7 +46,7 @@ export interface VegaVisualizationDependencies { plugins: { data: DataPublicPluginSetup; }; - serviceSettings: IServiceSettings; + getServiceSettings: () => Promise; } /** @internal */ @@ -81,7 +80,6 @@ export class VegaPlugin implements Plugin, void> { emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); - setKibanaMapFactory(mapsLegacy.getKibanaMapFactoryProvider); setMapsLegacyConfig(mapsLegacy.config); const visualizationDependencies: Readonly = { @@ -89,7 +87,7 @@ export class VegaPlugin implements Plugin, void> { plugins: { data, }, - serviceSettings: mapsLegacy.serviceSettings, + getServiceSettings: mapsLegacy.getServiceSettings, }; inspector.registerView(getVegaInspectorView({ uiSettings: core.uiSettings })); diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index dfb2c96e9f8940..455fe67dbc4233 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -33,9 +33,6 @@ export const [getData, setData] = createGetterSetter('Dat export const [getNotifications, setNotifications] = createGetterSetter( 'Notifications' ); -export const [getKibanaMapFactory, setKibanaMapFactory] = createGetterSetter( - 'KibanaMapFactory' -); export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index c09a9466df6028..f48b61ed70822b 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -40,7 +40,7 @@ interface VegaRequestHandlerContext { } export function createVegaRequestHandler( - { plugins: { data }, core: { uiSettings }, serviceSettings }: VegaVisualizationDependencies, + { plugins: { data }, core: { uiSettings }, getServiceSettings }: VegaVisualizationDependencies, context: VegaRequestHandlerContext = {} ) { let searchAPI: SearchAPI; @@ -70,7 +70,7 @@ export function createVegaRequestHandler( const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); const { VegaParser } = await import('./data_model/vega_parser'); - const vp = new VegaParser(visParams.spec, searchAPI, timeCache, filtersDsl, serviceSettings); + const vp = new VegaParser(visParams.spec, searchAPI, timeCache, filtersDsl, getServiceSettings); return await vp.parseAsync(); }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js index bc1cb4e4734c7d..ec4959a51d7413 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js @@ -17,16 +17,16 @@ * under the License. */ -import { KibanaMapLayer, L } from '../../../maps_legacy/public'; +import { KibanaMapLayer } from '../../../maps_legacy/public'; export class VegaMapLayer extends KibanaMapLayer { - constructor(spec, options) { + constructor(spec, options, leaflet) { super(); // Used by super.getAttributions() this._attribution = options.attribution; delete options.attribution; - this._leafletLayer = L.vega(spec, options); + this._leafletLayer = leaflet.vega(spec, options); } getVegaView() { diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 78ae2efdbdda57..d925aaeeced66b 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -21,7 +21,8 @@ import { i18n } from '@kbn/i18n'; import { vega } from '../lib/vega'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { getEmsTileLayerId, getUISettings, getKibanaMapFactory } from '../services'; +import { getEmsTileLayerId, getUISettings } from '../services'; +import { lazyLoadMapsLegacyModules } from '../../../maps_legacy/public'; export class VegaMapView extends VegaBaseView { constructor(opts) { @@ -106,7 +107,9 @@ export class VegaMapView extends VegaBaseView { // maxBounds = L.latLngBounds(L.latLng(b[1], b[0]), L.latLng(b[3], b[2])); // } - this._kibanaMap = getKibanaMapFactory()(this._$container.get(0), { + const modules = await lazyLoadMapsLegacyModules(); + + this._kibanaMap = new modules.KibanaMap(this._$container.get(0), { zoom, minZoom, maxZoom, @@ -122,14 +125,18 @@ export class VegaMapView extends VegaBaseView { }); } - const vegaMapLayer = new VegaMapLayer(this._parser.spec, { - vega, - bindingsContainer: this._$controls.get(0), - delayRepaint: mapConfig.delayRepaint, - viewConfig: this._vegaViewConfig, - onWarning: this.onWarn.bind(this), - onError: this.onError.bind(this), - }); + const vegaMapLayer = new VegaMapLayer( + this._parser.spec, + { + vega, + bindingsContainer: this._$controls.get(0), + delayRepaint: mapConfig.delayRepaint, + viewConfig: this._vegaViewConfig, + onWarning: this.onWarn.bind(this), + onError: this.onError.bind(this), + }, + modules.L + ); this._kibanaMap.addLayer(vegaMapLayer); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js index d6db0f9ea239f9..2d58e9cda60cdc 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.js @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { getNotifications, getData, getSavedObjects } from './services'; -export const createVegaVisualization = ({ serviceSettings }) => +export const createVegaVisualization = ({ getServiceSettings }) => class VegaVisualization { constructor(el, vis) { this._el = el; @@ -102,6 +102,7 @@ export const createVegaVisualization = ({ serviceSettings }) => this._vegaView = null; } + const serviceSettings = await getServiceSettings(); const { filterManager } = this.dataPlugin.query; const { timefilter } = this.dataPlugin.query.timefilter; const vegaViewParams = { diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index 1bf625af76207a..dcf1722768075a 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -32,16 +32,9 @@ import { SearchAPI } from './data_model/search_api'; import { createVegaTypeDefinition } from './vega_type'; -import { - setInjectedVars, - setData, - setSavedObjects, - setNotifications, - setKibanaMapFactory, -} from './services'; +import { setInjectedVars, setData, setSavedObjects, setNotifications } from './services'; import { coreMock } from '../../../core/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; -import { KibanaMap } from '../../maps_legacy/public/map/kibana_map'; jest.mock('./default_spec', () => ({ getDefaultSpec: () => jest.requireActual('./test_utils/default.spec.json'), @@ -77,8 +70,11 @@ describe('VegaVisualizations', () => { mockHeight = jest.spyOn($.prototype, 'height').mockImplementation(() => mockedHeightValue); }; + const mockGetServiceSettings = async () => { + return {}; + }; + beforeEach(() => { - setKibanaMapFactory((...args) => new KibanaMap(...args)); setInjectedVars({ emsTileLayerId: {}, enableExternalUrls: true, @@ -92,6 +88,7 @@ describe('VegaVisualizations', () => { plugins: { data: dataPluginMock.createSetupContract(), }, + getServiceSettings: mockGetServiceSettings, }; vegaVisType = createVegaTypeDefinition(vegaVisualizationDependencies); @@ -128,7 +125,10 @@ describe('VegaVisualizations', () => { search: dataPluginStart.search, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, - }) + }), + 0, + 0, + mockGetServiceSettings ); await vegaParser.parseAsync(); await vegaVis.render(vegaParser); @@ -155,7 +155,10 @@ describe('VegaVisualizations', () => { search: dataPluginStart.search, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, - }) + }), + 0, + 0, + mockGetServiceSettings ); await vegaParser.parseAsync(); @@ -176,7 +179,10 @@ describe('VegaVisualizations', () => { search: dataPluginStart.search, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, - }) + }), + 0, + 0, + mockGetServiceSettings ); await vegaParser.parseAsync(); diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index daf1659f0cfe13..f5fb54c72177fb 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -474,6 +474,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { return parseInt(scrollSize, 10); } + public async scrollTop() { + await driver.executeScript('document.documentElement.scrollTop = 0'); + } + // return promise with REAL scroll position public async setScrollTop(scrollSize: number | string) { await driver.executeScript('document.body.scrollTop = ' + scrollSize); diff --git a/x-pack/package.json b/x-pack/package.json index 3702e1a49cbe56..984843e5b6360d 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -277,7 +277,7 @@ "@babel/register": "^7.10.5", "@babel/runtime": "^7.11.2", "@elastic/datemath": "5.0.3", - "@elastic/ems-client": "7.9.3", + "@elastic/ems-client": "7.10.0", "@elastic/eui": "29.0.0", "@elastic/filesaver": "1.1.2", "@elastic/node-crypto": "1.2.1", diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/enterprise_search_url.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/enterprise_search_url.mock.ts new file mode 100644 index 00000000000000..47660d0a317207 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/enterprise_search_url.mock.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. + */ + +import { externalUrl } from '../shared/enterprise_search_url'; + +externalUrl.enterpriseSearchUrl = 'http://localhost:3002'; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts index ea3c3923cc4720..ee77b0937cd82a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalUrl } from '../shared/enterprise_search_url'; - /** * A set of default Kibana context values to use across component tests. * @see enterprise_search/public/index.tsx for the KibanaContext definition/import @@ -15,5 +13,4 @@ export const mockKibanaContext = { setBreadcrumbs: jest.fn(), setDocTitle: jest.fn(), config: { host: 'http://localhost:3002' }, - externalUrl: new ExternalUrl('http://localhost:3002'), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx index 233db7d4c59177..53f50822cf6535 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx @@ -5,7 +5,6 @@ */ import '../../../../__mocks__/kea.mock'; -import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx index 5ed1f0b277306b..cfe88d00ce14eb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { useValues } from 'kea'; import { EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; import { HttpLogic } from '../../../../shared/http'; +import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; -import { KibanaContext, IKibanaContext } from '../../../../index'; import { CREATE_ENGINES_PATH } from '../../../routes'; import { EngineOverviewHeader } from './header'; @@ -21,9 +21,6 @@ import './empty_state.scss'; export const EmptyState: React.FC = () => { const { http } = useValues(HttpLogic); - const { - externalUrl: { getAppSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { href: getAppSearchUrl(CREATE_ENGINES_PATH), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx index 8c7dfa2b7c3d63..78ee5764be5a97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx @@ -5,7 +5,7 @@ */ import '../../../../__mocks__/kea.mock'; -import '../../../../__mocks__/shallow_usecontext.mock'; +import '../../../../__mocks__/enterprise_search_url.mock'; import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx index dca0d45a207b4a..6ebb2c5bf453da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { useValues } from 'kea'; import { EuiPageHeader, @@ -18,13 +18,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; import { HttpLogic } from '../../../../shared/http'; -import { KibanaContext, IKibanaContext } from '../../../../index'; +import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; export const EngineOverviewHeader: React.FC = () => { const { http } = useValues(HttpLogic); - const { - externalUrl: { getAppSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { fill: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx index 8e92f21f8ffeda..c66fd24fee12a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -5,7 +5,7 @@ */ import '../../../__mocks__/kea.mock'; -import '../../../__mocks__/shallow_usecontext.mock'; +import '../../../__mocks__/enterprise_search_url.mock'; import { mockHttpValues } from '../../../__mocks__/'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx index 6888be1dc2b5b7..40fb313f30b31b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { useValues } from 'kea'; import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { sendTelemetry } from '../../../shared/telemetry'; import { HttpLogic } from '../../../shared/http'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { getEngineRoute } from '../../routes'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; @@ -43,9 +43,6 @@ export const EngineTable: React.FC = ({ pagination: { totalEngines, pageIndex, onPaginate }, }) => { const { http } = useValues(HttpLogic); - const { - externalUrl: { getAppSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; const engineLinkProps = (name: string) => ({ href: getAppSearchUrl(getEngineRoute(name)), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 350bc97085d7be..052f4446e4409b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -6,6 +6,7 @@ import '../__mocks__/shallow_usecontext.mock'; import '../__mocks__/kea.mock'; +import '../__mocks__/enterprise_search_url.mock'; import React, { useContext } from 'react'; import { Redirect } from 'react-router-dom'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index c848415daf6123..410f6eb524822c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -11,6 +11,7 @@ import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; import { KibanaContext, IKibanaContext } from '../index'; +import { getAppSearchUrl } from '../shared/enterprise_search_url'; import { HttpLogic } from '../shared/http'; import { AppLogic } from './app_logic'; import { IInitialAppData } from '../../../common/types'; @@ -86,10 +87,6 @@ export const AppSearchConfigured: React.FC = (props) => { }; export const AppSearchNav: React.FC = () => { - const { - externalUrl: { getAppSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; - const { myRole: { canViewSettings, canViewAccountCredentials, canViewRoleMappings }, } = useValues(AppLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 6ee63ee22cae2b..66772f96671e81 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -54,7 +54,7 @@ describe('renderHeaderActions', () => { const mockHeaderEl = document.createElement('header'); const MockHeaderActions = () => ; - const unmount = renderHeaderActions(MockHeaderActions, mockHeaderEl, {} as any); + const unmount = renderHeaderActions(MockHeaderActions, mockHeaderEl); expect(mockHeaderEl.querySelector('.hello-world')).not.toBeNull(); unmount(); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 4a25ecf6067cc3..2c6bc787923e36 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -18,12 +18,11 @@ import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; import { mountLicensingLogic } from './shared/licensing'; import { mountHttpLogic } from './shared/http'; import { mountFlashMessagesLogic } from './shared/flash_messages'; -import { IExternalUrl } from './shared/enterprise_search_url'; +import { externalUrl } from './shared/enterprise_search_url'; import { IInitialAppData } from '../../common/types'; export interface IKibanaContext { config: { host?: string }; - externalUrl: IExternalUrl; navigateToUrl: ApplicationStart['navigateToUrl']; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setDocTitle(title: string): void; @@ -42,7 +41,8 @@ export const renderApp = ( { params, core, plugins }: { params: AppMountParameters; core: CoreStart; plugins: PluginsStart }, { config, data }: { config: ClientConfigType; data: ClientData } ) => { - const { externalUrl, errorConnecting, ...initialData } = data; + const { publicUrl, errorConnecting, ...initialData } = data; + externalUrl.enterpriseSearchUrl = publicUrl || config.host || ''; resetContext({ createStore: true }); const store = getContext().store as Store; @@ -64,7 +64,6 @@ export const renderApp = ( , - kibanaHeaderEl: HTMLElement, - externalUrl: IExternalUrl -) => { - ReactDOM.render(, kibanaHeaderEl); +export const renderHeaderActions = (HeaderActions: React.FC, kibanaHeaderEl: HTMLElement) => { + ReactDOM.render(, kibanaHeaderEl); return () => ReactDOM.unmountComponentAtNode(kibanaHeaderEl); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/external_url.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/external_url.test.ts new file mode 100644 index 00000000000000..55c4f465d9ed42 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/external_url.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { externalUrl, getEnterpriseSearchUrl, getAppSearchUrl, getWorkplaceSearchUrl } from './'; + +describe('Enterprise Search external URL helpers', () => { + describe('getter/setter tests', () => { + it('defaults to an empty string', () => { + expect(externalUrl.enterpriseSearchUrl).toEqual(''); + }); + + it('sets the internal enterpriseSearchUrl value', () => { + externalUrl.enterpriseSearchUrl = 'http://localhost:3002'; + expect(externalUrl.enterpriseSearchUrl).toEqual('http://localhost:3002'); + }); + + it('does not allow mutating enterpriseSearchUrl once set', () => { + externalUrl.enterpriseSearchUrl = 'hello world'; + expect(externalUrl.enterpriseSearchUrl).toEqual('http://localhost:3002'); + }); + }); + + describe('function helpers', () => { + it('generates a public Enterprise Search URL', () => { + expect(getEnterpriseSearchUrl()).toEqual('http://localhost:3002'); + expect(getEnterpriseSearchUrl('/login')).toEqual('http://localhost:3002/login'); + }); + + it('generates a public App Search URL', () => { + expect(getAppSearchUrl()).toEqual('http://localhost:3002/as'); + expect(getAppSearchUrl('/path')).toEqual('http://localhost:3002/as/path'); + }); + + it('generates a public Workplace Search URL', () => { + expect(getWorkplaceSearchUrl()).toEqual('http://localhost:3002/ws'); + expect(getWorkplaceSearchUrl('/path')).toEqual('http://localhost:3002/ws/path'); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/external_url.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/external_url.ts new file mode 100644 index 00000000000000..80b506f31ad614 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/external_url.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/** + * NOTE: The externalUrl obj holds the reference to externalUrl, which should + * only ever be updated once on plugin init. We're using a getter and setter + * here to ensure it isn't accidentally mutated. + * + * Someday (8.x+), when our UI is entirely on Kibana and no longer on + * Enterprise Search's standalone UI, we can potentially deprecate this helper. + */ +export const externalUrl = { + _enterpriseSearchUrl: '', + get enterpriseSearchUrl() { + return this._enterpriseSearchUrl; + }, + set enterpriseSearchUrl(value) { + if (this._enterpriseSearchUrl) { + // enterpriseSearchUrl is set once on plugin init - we should not mutate it + return; + } + this._enterpriseSearchUrl = value; + }, +}; + +export const getEnterpriseSearchUrl = (path: string = ''): string => { + return externalUrl.enterpriseSearchUrl + path; +}; +export const getAppSearchUrl = (path: string = ''): string => { + return getEnterpriseSearchUrl('/as' + path); +}; +export const getWorkplaceSearchUrl = (path: string = ''): string => { + return getEnterpriseSearchUrl('/ws' + path); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.test.ts deleted file mode 100644 index 1092c88cbbc113..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.test.ts +++ /dev/null @@ -1,25 +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 { ExternalUrl } from './'; - -describe('Enterprise Search external URL helper', () => { - const externalUrl = new ExternalUrl('http://localhost:3002'); - - it('exposes a public enterpriseSearchUrl string', () => { - expect(externalUrl.enterpriseSearchUrl).toEqual('http://localhost:3002'); - }); - - it('generates a public App Search URL', () => { - expect(externalUrl.getAppSearchUrl()).toEqual('http://localhost:3002/as'); - expect(externalUrl.getAppSearchUrl('/path')).toEqual('http://localhost:3002/as/path'); - }); - - it('generates a public Workplace Search URL', () => { - expect(externalUrl.getWorkplaceSearchUrl()).toEqual('http://localhost:3002/ws'); - expect(externalUrl.getWorkplaceSearchUrl('/path')).toEqual('http://localhost:3002/ws/path'); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.ts deleted file mode 100644 index 9db48d197f3bc3..00000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.ts +++ /dev/null @@ -1,38 +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. - */ - -/** - * Small helper for generating external public-facing URLs - * to the legacy/standalone Enterprise Search app - */ -export interface IExternalUrl { - enterpriseSearchUrl?: string; - getAppSearchUrl(path?: string): string; - getWorkplaceSearchUrl(path?: string): string; -} - -export class ExternalUrl { - public enterpriseSearchUrl: string; - - constructor(externalUrl: string) { - this.enterpriseSearchUrl = externalUrl; - - this.getAppSearchUrl = this.getAppSearchUrl.bind(this); - this.getWorkplaceSearchUrl = this.getWorkplaceSearchUrl.bind(this); - } - - private getExternalUrl(path: string): string { - return this.enterpriseSearchUrl + path; - } - - public getAppSearchUrl(path: string = ''): string { - return this.getExternalUrl('/as' + path); - } - - public getWorkplaceSearchUrl(path: string = ''): string { - return this.getExternalUrl('/ws' + path); - } -} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts index d2d82a43c6dd90..177d8e0535c72a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ExternalUrl, IExternalUrl } from './generate_external_url'; +export { + externalUrl, + getEnterpriseSearchUrl, + getAppSearchUrl, + getWorkplaceSearchUrl, +} from './external_url'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx index a006c5e3775d52..0ebd59eda5be7f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx @@ -4,26 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { externalUrl } from '../../../shared/enterprise_search_url'; + import React from 'react'; import { shallow } from 'enzyme'; - import { EuiButtonEmpty } from '@elastic/eui'; -import { ExternalUrl } from '../../../shared/enterprise_search_url'; import { WorkplaceSearchHeaderActions } from './'; describe('WorkplaceSearchHeaderActions', () => { - const externalUrl = new ExternalUrl('http://localhost:3002'); + it('does not render without an Enterprise Search URL set', () => { + const wrapper = shallow(); - it('renders a link to the search application', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual('http://localhost:3002/ws/search'); + expect(wrapper.isEmptyRender()).toBe(true); }); - it('does not render without an Enterprise Search host URL set', () => { - const wrapper = shallow(); + it('renders a link to the search application', () => { + externalUrl.enterpriseSearchUrl = 'http://localhost:3002'; - expect(wrapper.isEmptyRender()).toBe(true); + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual('http://localhost:3002/ws/search'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx index fa32d598f848db..b7da5b4281aa0f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx @@ -8,15 +8,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty } from '@elastic/eui'; -import { IExternalUrl } from '../../../shared/enterprise_search_url'; +import { externalUrl, getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; -interface IProps { - externalUrl: IExternalUrl; -} - -export const WorkplaceSearchHeaderActions: React.FC = ({ externalUrl }) => { - const { enterpriseSearchUrl, getWorkplaceSearchUrl } = externalUrl; - if (!enterpriseSearchUrl) return null; +export const WorkplaceSearchHeaderActions: React.FC = () => { + if (!externalUrl.enterpriseSearchUrl) return null; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx index 0e85d8467cff0d..2553284744e4d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; +import '../../../__mocks__/enterprise_search_url.mock'; + import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index 9fb627ed097910..55727163911128 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { SideNav, SideNavLink } from '../../../shared/layout'; import { @@ -22,10 +22,6 @@ import { } from '../../routes'; export const WorkplaceSearchNav: React.FC = () => { - const { - externalUrl: { getWorkplaceSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; - // TODO: icons return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx index c73eb05ccec16a..2013b2609f33bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx @@ -5,7 +5,6 @@ */ import '../../../../__mocks__/kea.mock'; -import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx index a80de9fd6ac82c..344b442d9a6781 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { useValues } from 'kea'; import { EuiButton, EuiButtonProps, EuiLinkProps } from '@elastic/eui'; @@ -12,13 +12,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; import { HttpLogic } from '../../../../shared/http'; -import { KibanaContext, IKibanaContext } from '../../../../index'; +import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url'; export const ProductButton: React.FC = () => { const { http } = useValues(HttpLogic); - const { - externalUrl: { getWorkplaceSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { fill: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx index c890adb8ea0438..6be033d7225a87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx @@ -5,7 +5,7 @@ */ import '../../../__mocks__/kea.mock'; -import '../../../__mocks__/shallow_usecontext.mock'; +import '../../../__mocks__/enterprise_search_url.mock'; import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx index 79be7ef1cb1587..c1070d57f28567 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { useValues } from 'kea'; import { @@ -21,7 +21,7 @@ import { import { sendTelemetry } from '../../../shared/telemetry'; import { HttpLogic } from '../../../shared/http'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; interface IOnboardingCardProps { title: React.ReactNode; @@ -43,9 +43,6 @@ export const OnboardingCard: React.FC = ({ complete, }) => { const { http } = useValues(HttpLogic); - const { - externalUrl: { getWorkplaceSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; const onClick = () => sendTelemetry({ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx index 0f3eee074caefa..37b3340b96a6a5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; import './__mocks__/overview_logic.mock'; import { setMockValues } from './__mocks__'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index 079d981533e012..132824833909de 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useValues } from 'kea'; @@ -24,7 +24,7 @@ import { import sharedSourcesIcon from '../../components/shared/assets/share_circle.svg'; import { sendTelemetry } from '../../../shared/telemetry'; import { HttpLogic } from '../../../shared/http'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; import { ContentSection } from '../../components/shared/content_section'; @@ -137,9 +137,6 @@ export const OnboardingSteps: React.FC = () => { export const OrgNameOnboarding: React.FC = () => { const { http } = useValues(HttpLogic); - const { - externalUrl: { getWorkplaceSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; const onClick = () => sendTelemetry({ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx index 31613098f9fcce..989ff800483f60 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; import './__mocks__/overview_logic.mock'; import { setMockValues } from './__mocks__'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index dd62e6de7c046d..d1b5228123d947 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import moment from 'moment'; import { useValues } from 'kea'; @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ContentSection } from '../../components/shared/content_section'; import { sendTelemetry } from '../../../shared/telemetry'; import { HttpLogic } from '../../../shared/http'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; import { AppLogic } from '../../app_logic'; @@ -95,9 +95,6 @@ export const RecentActivityItem: React.FC = ({ sourceId, }) => { const { http } = useValues(HttpLogic); - const { - externalUrl: { getWorkplaceSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; const onClick = () => sendTelemetry({ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx index edf266231b39ef..013b23d2a9ec0d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/shallow_usecontext.mock'; +import '../../../__mocks__/enterprise_search_url.mock'; import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx index 3e1d285698c0c1..6c4f43b1a3a22b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; - +import React from 'react'; import { EuiCard, EuiFlexItem, EuiTitle, EuiTextColor } from '@elastic/eui'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; interface IStatisticCardProps { title: string; @@ -17,10 +16,6 @@ interface IStatisticCardProps { } export const StatisticCard: React.FC = ({ title, count = 0, actionPath }) => { - const { - externalUrl: { getWorkplaceSearchUrl }, - } = useContext(KibanaContext) as IKibanaContext; - const linkProps = actionPath ? { href: getWorkplaceSearchUrl(actionPath), diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index f59ec830c812fb..d870127f297b4d 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -23,13 +23,13 @@ import { WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; import { IInitialAppData } from '../common/types'; -import { ExternalUrl, IExternalUrl } from './applications/shared/enterprise_search_url'; +import { externalUrl } from './applications/shared/enterprise_search_url'; export interface ClientConfigType { host?: string; } export interface ClientData extends IInitialAppData { - externalUrl: IExternalUrl; + publicUrl?: string; errorConnecting?: boolean; } @@ -47,7 +47,6 @@ export class EnterpriseSearchPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); - this.data.externalUrl = new ExternalUrl(this.config.host || ''); } public setup(core: CoreSetup, plugins: PluginsSetup) { @@ -59,11 +58,11 @@ export class EnterpriseSearchPlugin implements Plugin { category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { const kibanaDeps = await this.getKibanaDeps(core, params); - const pluginData = this.getPluginData(); - const { chrome, http } = kibanaDeps.core; chrome.docTitle.change(ENTERPRISE_SEARCH_PLUGIN.NAME); + await this.getInitialData(http); + const pluginData = this.getPluginData(); const { renderApp } = await import('./applications'); const { EnterpriseSearch } = await import('./applications/enterprise_search'); @@ -80,11 +79,11 @@ export class EnterpriseSearchPlugin implements Plugin { category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { const kibanaDeps = await this.getKibanaDeps(core, params); - const pluginData = this.getPluginData(); - const { chrome, http } = kibanaDeps.core; chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME); + await this.getInitialData(http); + const pluginData = this.getPluginData(); const { renderApp } = await import('./applications'); const { AppSearch } = await import('./applications/app_search'); @@ -101,11 +100,11 @@ export class EnterpriseSearchPlugin implements Plugin { category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { const kibanaDeps = await this.getKibanaDeps(core, params); - const pluginData = this.getPluginData(); - const { chrome, http } = kibanaDeps.core; chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME); + await this.getInitialData(http); + const pluginData = this.getPluginData(); const { renderApp, renderHeaderActions } = await import('./applications'); const { WorkplaceSearch } = await import('./applications/workplace_search'); @@ -114,7 +113,7 @@ export class EnterpriseSearchPlugin implements Plugin { './applications/workplace_search/components/layout' ); params.setHeaderActionMenu((element) => - renderHeaderActions(WorkplaceSearchHeaderActions, element, this.data.externalUrl) + renderHeaderActions(WorkplaceSearchHeaderActions, element) ); return renderApp(WorkplaceSearch, kibanaDeps, pluginData); @@ -174,14 +173,14 @@ export class EnterpriseSearchPlugin implements Plugin { if (this.hasInitialized) return; // We've already made an initial call try { - const { publicUrl, ...initialData } = await http.get('/api/enterprise_search/config_data'); - this.data = { ...this.data, ...initialData }; - if (publicUrl) this.data.externalUrl = new ExternalUrl(publicUrl); - + this.data = await http.get('/api/enterprise_search/config_data'); this.hasInitialized = true; + + // TODO: This is a temporary workaround to keep the WorkplaceSearchHeaderActions working. + // We'll solve this shortly by ensuring the main app store loads before the header actions. + externalUrl.enterpriseSearchUrl = this.data.publicUrl || this.config.host; } catch { this.data.errorConnecting = true; - // The plugin will attempt to re-fetch config data on page change } } } diff --git a/x-pack/plugins/global_search/server/services/context.mock.ts b/x-pack/plugins/global_search/server/services/context.mock.ts index 7c72686529c157..325d6ea5c6d19b 100644 --- a/x-pack/plugins/global_search/server/services/context.mock.ts +++ b/x-pack/plugins/global_search/server/services/context.mock.ts @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { of } from 'rxjs'; +import { Capabilities } from 'src/core/server'; import { savedObjectsTypeRegistryMock, savedObjectsClientMock, elasticsearchServiceMock, uiSettingsServiceMock, + capabilitiesServiceMock, } from '../../../../../src/core/server/mocks'; -const createContextMock = () => { +const createContextMock = (capabilities: Partial = {}) => { return { core: { savedObjects: { @@ -26,6 +29,10 @@ const createContextMock = () => { uiSettings: { client: uiSettingsServiceMock.createClient(), }, + capabilities: of({ + ...capabilitiesServiceMock.createCapabilities(), + ...capabilities, + } as Capabilities), }, }; }; diff --git a/x-pack/plugins/global_search/server/services/context.test.ts b/x-pack/plugins/global_search/server/services/context.test.ts index 397a1ea1703490..640f4c11b363f6 100644 --- a/x-pack/plugins/global_search/server/services/context.test.ts +++ b/x-pack/plugins/global_search/server/services/context.test.ts @@ -27,11 +27,15 @@ describe('getContextFactory', () => { expect(coreStart.uiSettings.asScopedToClient).toHaveBeenCalledTimes(1); expect(coreStart.uiSettings.asScopedToClient).toHaveBeenCalledWith(soClient); + expect(coreStart.capabilities.resolveCapabilities).toHaveBeenCalledTimes(1); + expect(coreStart.capabilities.resolveCapabilities).toHaveBeenCalledWith(request); + expect(context).toEqual({ core: { savedObjects: expect.any(Object), elasticsearch: expect.any(Object), uiSettings: expect.any(Object), + capabilities: expect.any(Object), }, }); }); diff --git a/x-pack/plugins/global_search/server/services/context.ts b/x-pack/plugins/global_search/server/services/context.ts index b15deccaae018f..62fddcfb152b34 100644 --- a/x-pack/plugins/global_search/server/services/context.ts +++ b/x-pack/plugins/global_search/server/services/context.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { from } from 'rxjs'; import { CoreStart, KibanaRequest } from 'src/core/server'; import { GlobalSearchProviderContext } from '../types'; @@ -30,6 +31,7 @@ export const getContextFactory = (coreStart: CoreStart) => ( uiSettings: { client: coreStart.uiSettings.asScopedToClient(soClient), }, + capabilities: from(coreStart.capabilities.resolveCapabilities(request)), }, }; }; diff --git a/x-pack/plugins/global_search/server/types.ts b/x-pack/plugins/global_search/server/types.ts index 7d3f5ebc5d0792..07d21f54d7bf59 100644 --- a/x-pack/plugins/global_search/server/types.ts +++ b/x-pack/plugins/global_search/server/types.ts @@ -10,6 +10,7 @@ import { ILegacyScopedClusterClient, IUiSettingsClient, SavedObjectsClientContract, + Capabilities, } from 'src/core/server'; import { GlobalSearchBatchedResults, @@ -52,6 +53,7 @@ export interface GlobalSearchProviderContext { uiSettings: { client: IUiSettingsClient; }; + capabilities: Observable; }; } diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.test.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.test.ts index 0085331c5be5f2..8798fe6694c96c 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.test.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.test.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsFindResult, SavedObjectsType, SavedObjectTypeRegistry } from 'src/core/server'; +import { + SavedObjectsFindResult, + SavedObjectsType, + SavedObjectTypeRegistry, + Capabilities, +} from 'src/core/server'; import { mapToResult, mapToResults } from './map_object_to_result'; const createType = (props: Partial): SavedObjectsType => { @@ -111,18 +116,17 @@ describe('mapToResult', () => { describe('mapToResults', () => { let typeRegistry: SavedObjectTypeRegistry; + let capabilities: Capabilities; beforeEach(() => { typeRegistry = new SavedObjectTypeRegistry(); - }); - it('converts savedObjects to results', () => { typeRegistry.registerType( createType({ name: 'typeA', management: { defaultSearchField: 'title', - getInAppUrl: (obj) => ({ path: `/type-a/${obj.id}`, uiCapabilitiesPath: '' }), + getInAppUrl: (obj) => ({ path: `/type-a/${obj.id}`, uiCapabilitiesPath: 'test.typeA' }), }, }) ); @@ -131,7 +135,7 @@ describe('mapToResults', () => { name: 'typeB', management: { defaultSearchField: 'description', - getInAppUrl: (obj) => ({ path: `/type-b/${obj.id}`, uiCapabilitiesPath: 'foo' }), + getInAppUrl: (obj) => ({ path: `/type-b/${obj.id}`, uiCapabilitiesPath: 'test.typeB' }), }, }) ); @@ -140,11 +144,37 @@ describe('mapToResults', () => { name: 'typeC', management: { defaultSearchField: 'excerpt', - getInAppUrl: (obj) => ({ path: `/type-c/${obj.id}`, uiCapabilitiesPath: 'bar' }), + getInAppUrl: (obj) => ({ path: `/type-c/${obj.id}`, uiCapabilitiesPath: 'test.typeC' }), }, }) ); + typeRegistry.registerType( + createType({ + name: 'inaccessibleType', + management: { + defaultSearchField: 'excerpt', + getInAppUrl: (obj) => ({ + path: `/inaccessible-type/${obj.id}`, + uiCapabilitiesPath: 'test.inaccessibleType', + }), + }, + }) + ); + + capabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + test: { + typeA: true, + typeB: true, + typeC: true, + inacessibleType: false, + }, + }; + }); + it('converts savedObjects to results', () => { const results = [ createObject( { @@ -181,7 +211,7 @@ describe('mapToResults', () => { ), ]; - expect(mapToResults(results, typeRegistry)).toEqual([ + expect(mapToResults(results, typeRegistry, capabilities)).toEqual([ { id: 'resultA', title: 'titleA', @@ -205,4 +235,41 @@ describe('mapToResults', () => { }, ]); }); + + it('filters results without permissions', () => { + const results = [ + createObject( + { + id: 'resultA', + type: 'typeA', + score: 100, + }, + { + title: 'titleA', + field: 'noise', + } + ), + createObject( + { + id: 'inaccessibleResult', + type: 'inaccessibleType', + score: 92, + }, + { + excerpt: 'inaccessibleTitle', + title: 'inaccessible', + } + ), + ]; + + expect(mapToResults(results, typeRegistry, capabilities)).toEqual([ + { + id: 'resultA', + title: 'titleA', + type: 'typeA', + url: '/type-a/resultA', + score: 100, + }, + ]); + }); }); diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts index c93558b1a3cf4c..14641e1aaffff4 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/map_object_to_result.ts @@ -4,18 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ +import get from 'lodash/get'; import { SavedObjectsType, ISavedObjectTypeRegistry, SavedObjectsFindResult, + Capabilities, } from 'src/core/server'; import { GlobalSearchProviderResult } from '../../../../global_search/server'; export const mapToResults = ( objects: Array>, - registry: ISavedObjectTypeRegistry + registry: ISavedObjectTypeRegistry, + capabilities: Capabilities ): GlobalSearchProviderResult[] => { - return objects.map((obj) => mapToResult(obj, registry.getType(obj.type)!)); + return objects + .filter((obj) => isAccessible(obj, registry.getType(obj.type)!, capabilities)) + .map((obj) => mapToResult(obj, registry.getType(obj.type)!)); +}; + +const isAccessible = ( + object: SavedObjectsFindResult, + type: SavedObjectsType, + capabilities: Capabilities +): boolean => { + const { getInAppUrl } = type.management ?? {}; + if (getInAppUrl === undefined) { + throw new Error('Trying to map an object from a type without management metadata'); + } + const { uiCapabilitiesPath } = getInAppUrl(object); + return Boolean(get(capabilities, uiCapabilitiesPath) ?? false); }; export const mapToResult = ( diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts index 11e3a40ddee17a..352191658ed0d9 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.test.ts @@ -33,16 +33,20 @@ const createFindResponse = ( total: results.length, }); -const createType = (props: Partial): SavedObjectsType => { +const createType = ( + props: Pick & Partial +): SavedObjectsType => { return { - name: 'type', hidden: false, namespaceType: 'single', mappings: { properties: {} }, ...props, management: { defaultSearchField: 'field', - getInAppUrl: (obj) => ({ path: `/object/${obj.id}`, uiCapabilitiesPath: '' }), + getInAppUrl: (obj) => ({ + path: `/object/${obj.id}`, + uiCapabilitiesPath: `types.${props.name}`, + }), ...props.management, }, }; @@ -82,7 +86,7 @@ describe('savedObjectsResultProvider', () => { name: 'typeA', management: { defaultSearchField: 'title', - getInAppUrl: (obj) => ({ path: `/type-a/${obj.id}`, uiCapabilitiesPath: '' }), + getInAppUrl: (obj) => ({ path: `/type-a/${obj.id}`, uiCapabilitiesPath: 'test.typeA' }), }, }) ); @@ -91,12 +95,17 @@ describe('savedObjectsResultProvider', () => { name: 'typeB', management: { defaultSearchField: 'description', - getInAppUrl: (obj) => ({ path: `/type-b/${obj.id}`, uiCapabilitiesPath: 'foo' }), + getInAppUrl: (obj) => ({ path: `/type-b/${obj.id}`, uiCapabilitiesPath: 'test.typeB' }), }, }) ); - context = globalSearchPluginMock.createProviderContext(); + context = globalSearchPluginMock.createProviderContext({ + test: { + typeA: true, + typeB: true, + }, + }); context.core.savedObjects.client.find.mockResolvedValue(createFindResponse([])); context.core.savedObjects.typeRegistry = registry as any; }); diff --git a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts index 8a3d3d732531fc..1c79380fe17fd1 100644 --- a/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts +++ b/x-pack/plugins/global_search_providers/server/providers/saved_objects/provider.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { from } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { from, combineLatest } from 'rxjs'; +import { map, takeUntil, first } from 'rxjs/operators'; import { GlobalSearchResultProvider } from '../../../../global_search/server'; import { mapToResults } from './map_object_to_result'; @@ -13,7 +13,10 @@ export const createSavedObjectsResultProvider = (): GlobalSearchResultProvider = return { id: 'savedObjects', find: (term, { aborted$, maxResults, preference }, { core }) => { - const { typeRegistry, client } = core.savedObjects; + const { + capabilities, + savedObjects: { client, typeRegistry }, + } = core; const searchableTypes = typeRegistry .getVisibleTypes() @@ -31,9 +34,9 @@ export const createSavedObjectsResultProvider = (): GlobalSearchResultProvider = type: searchableTypes.map((type) => type.name), }); - return from(responsePromise).pipe( + return combineLatest([from(responsePromise), capabilities.pipe(first())]).pipe( takeUntil(aborted$), - map((res) => mapToResults(res.saved_objects, typeRegistry)) + map(([res, cap]) => mapToResults(res.saved_objects, typeRegistry, cap)) ); }, }; diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.tsx index 928c9738c4761b..4bf618923a138e 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_job_status/quality_warning_notices.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import groupBy from 'lodash/groupBy'; +import { groupBy } from 'lodash'; import React, { Fragment, useState } from 'react'; import { euiStyled } from '../../../../../observability/public'; import { diff --git a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts b/x-pack/plugins/maps_legacy_licensing/public/plugin.ts index eaf527f856bc5f..ed7321b2c0e745 100644 --- a/x-pack/plugins/maps_legacy_licensing/public/plugin.ts +++ b/x-pack/plugins/maps_legacy_licensing/public/plugin.ts @@ -26,12 +26,10 @@ export type MapsLegacyLicensingStart = ReturnType; export class MapsLegacyLicensing implements Plugin { public setup(core: CoreSetup, plugins: MapsLegacyLicensingSetupDependencies) { - const { - licensing, - mapsLegacy: { serviceSettings }, - } = plugins; + const { licensing, mapsLegacy } = plugins; if (licensing) { - licensing.license$.subscribe((license: ILicense) => { + licensing.license$.subscribe(async (license: ILicense) => { + const serviceSettings = await mapsLegacy.getServiceSettings(); const { uid, isActive } = license; if (isActive && license.hasAtLeast('basic')) { serviceSettings.setQueryParams({ license: uid }); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 6aad5d53c3a3c9..1949a3c3391612 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -188,6 +188,40 @@ export const DataGrid: FC = memo( ); } + let errorCallout; + + if (status === INDEX_STATUS.ERROR) { + // if it's a searchBar syntax error leave the table visible so they can try again + if (errorMessage && !errorMessage.includes('failed to create query')) { + errorCallout = ( + +

{errorMessage}

+
+ ); + } else { + errorCallout = ( + + + {errorMessage} + + + ); + } + } + return (
{isWithHeader(props) && ( @@ -211,19 +245,9 @@ export const DataGrid: FC = memo( )} - {status === INDEX_STATUS.ERROR && ( + {errorCallout !== undefined && (
- - - {errorMessage} - - + {errorCallout}
)} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 84b1c4241aaf20..07a15b01fca93f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -6,15 +6,7 @@ import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiSpacer, - EuiText, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; @@ -25,7 +17,6 @@ import { getToastNotifications } from '../../../../../util/dependency_cache'; import { DataFrameAnalyticsConfig, MAX_COLUMNS, - INDEX_STATUS, SEARCH_SIZE, defaultSearchQuery, getAnalysisType, @@ -95,43 +86,11 @@ export const ExplorationResultsTable: FC = React.memo( ); const docFieldsCount = classificationData.columnsWithCharts.length; - const { - columnsWithCharts, - errorMessage, - status, - tableItems, - visibleColumns, - } = classificationData; + const { columnsWithCharts, tableItems, visibleColumns } = classificationData; if (jobConfig === undefined || classificationData === undefined) { return null; } - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('failed to create query')) { - return ( - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - -

{errorMessage}

-
-
- ); - } return ( = React.memo(({ jobId }) = const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); - const { columnsWithCharts, errorMessage, status, tableItems } = outlierData; + const { columnsWithCharts, tableItems } = outlierData; const colorRange = useColorRange( COLOR_RANGE.BLUE, @@ -62,24 +55,6 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 ); - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('failed to create query')) { - return ( - - - -

{errorMessage}

-
-
- ); - } - return ( {jobConfig !== undefined && needsDestIndexPattern && ( diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 294f52cc3678ff..8d3248ddf43697 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -20,6 +20,7 @@ import { CoreStart, CustomHttpResponseOptions, ResponseError, + IClusterClient, } from 'kibana/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { @@ -73,6 +74,7 @@ export class Plugin { private monitoringCore = {} as MonitoringCore; private legacyShimDependencies = {} as LegacyShimDependencies; private bulkUploader: IBulkUploader = {} as IBulkUploader; + private telemetryElasticsearchClient: IClusterClient | undefined; constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; @@ -143,9 +145,14 @@ export class Plugin { // Initialize telemetry if (plugins.telemetryCollectionManager) { - registerMonitoringCollection(plugins.telemetryCollectionManager, this.cluster, { - maxBucketSize: config.ui.max_bucket_size, - }); + registerMonitoringCollection( + plugins.telemetryCollectionManager, + this.cluster, + () => this.telemetryElasticsearchClient, + { + maxBucketSize: config.ui.max_bucket_size, + } + ); } // Register collector objects for stats to show up in the APIs @@ -229,7 +236,13 @@ export class Plugin { }; } - start() {} + start({ elasticsearch }: CoreStart) { + // TODO: For the telemetry plugin to work, we need to provide the new ES client. + // The new client should be inititalized with a similar config to `this.cluster` but, since we're not using + // the new client in Monitoring Telemetry collection yet, setting the local client allos progress for now. + // We will update the client in a follow up PR. + this.telemetryElasticsearchClient = elasticsearch.client; + } stop() { if (this.cluster) { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index f0ad6399c6c72d..89f09d349014ff 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -15,6 +15,7 @@ describe('get_all_stats', () => { const start = 0; const end = 1; const callCluster = sinon.stub(); + const esClient = sinon.stub(); const esClusters = [ { cluster_uuid: 'a' }, @@ -176,6 +177,7 @@ describe('get_all_stats', () => { [{ clusterUuid: 'a' }], { callCluster: callCluster as any, + esClient: esClient as any, usageCollection: {} as any, start, end, @@ -201,6 +203,7 @@ describe('get_all_stats', () => { [], { callCluster: callCluster as any, + esClient: esClient as any, usageCollection: {} as any, start, end, diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index 726db1706758d9..1170380b26ac8c 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -21,7 +21,6 @@ type PromiseReturnType any> = ReturnType extend export interface CustomContext { maxBucketSize: number; } - /** * Get statistics for all products joined by Elasticsearch cluster. * Returns the array of clusters joined with the Kibana and Logstash instances. @@ -29,7 +28,7 @@ export interface CustomContext { */ export const getAllStats: StatsGetter = async ( clustersDetails, - { callCluster, start, end }, + { callCluster, start, end, esClient }, { maxBucketSize } ) => { const clusterUuids = clustersDetails.map((clusterDetails) => clusterDetails.clusterUuid); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts index 519dcc38875f50..b2f3cb6c61526c 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.test.ts @@ -5,6 +5,7 @@ */ import sinon from 'sinon'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { getClusterUuids, fetchClusterUuids, @@ -13,6 +14,7 @@ import { describe('get_cluster_uuids', () => { const callCluster = sinon.stub(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const response = { aggregations: { cluster_uuids: { @@ -30,7 +32,7 @@ describe('get_cluster_uuids', () => { it('returns cluster UUIDs', async () => { callCluster.withArgs('search').returns(Promise.resolve(response)); expect( - await getClusterUuids({ callCluster, start, end, usageCollection: {} as any }, { + await getClusterUuids({ callCluster, esClient, start, end, usageCollection: {} as any }, { maxBucketSize: 1, } as any) ).toStrictEqual(expectedUuids); @@ -41,7 +43,7 @@ describe('get_cluster_uuids', () => { it('searches for clusters', async () => { callCluster.returns(Promise.resolve(response)); expect( - await fetchClusterUuids({ callCluster, start, end, usageCollection: {} as any }, { + await fetchClusterUuids({ callCluster, esClient, start, end, usageCollection: {} as any }, { maxBucketSize: 1, } as any) ).toStrictEqual(response); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts index 1e5549fe4d8003..3648ae4bd8551b 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_collection.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyCustomClusterClient } from 'kibana/server'; +import { ILegacyCustomClusterClient, IClusterClient } from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { getAllStats, CustomContext } from './get_all_stats'; import { getClusterUuids } from './get_cluster_uuids'; @@ -13,10 +13,12 @@ import { getLicenses } from './get_licenses'; export function registerMonitoringCollection( telemetryCollectionManager: TelemetryCollectionManagerPluginSetup, esCluster: ILegacyCustomClusterClient, + esClientGetter: () => IClusterClient | undefined, customContext: CustomContext ) { telemetryCollectionManager.setCollection({ esCluster, + esClientGetter, title: 'monitoring', priority: 2, statsGetter: getAllStats, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index a93d2817fbbb3e..2910f02a187f48 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -34,6 +34,7 @@ export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled'; +export const GLOBAL_HEADER_HEIGHT = 98; // px export const FILTERS_GLOBAL_HEIGHT = 109; // px export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled'; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; 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 311aa0c04c9abf..a6d59615794a62 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts @@ -26,7 +26,7 @@ export const validateTree = { /** * Used to validate GET requests for non process events for a specific event. */ -export const validateEvents = { +export const validateRelatedEvents = { params: schema.object({ id: schema.string({ minLength: 1 }) }), query: schema.object({ events: schema.number({ defaultValue: 1000, min: 1, max: 10000 }), @@ -40,6 +40,22 @@ export const validateEvents = { ), }; +/** + * Used to validate POST requests for `/resolver/events` api. + */ +export const validateEvents = { + query: schema.object({ + // keeping the max as 10k because the limit in ES for a single query is also 10k + limit: schema.number({ defaultValue: 1000, min: 1, max: 10000 }), + afterEvent: schema.maybe(schema.string()), + }), + body: schema.nullable( + schema.object({ + filter: schema.maybe(schema.string()), + }) + ), +}; + /** * Used to validate GET requests for alerts for a specific process. */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index d97fdfbf7d186f..abb0ccee8d9097 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -276,6 +276,15 @@ export interface SafeResolverRelatedEvents { nextEvent: string | null; } +/** + * Response structure for the events route. + * `nextEvent` will be set to null when at the time of querying there were no more results to retrieve from ES. + */ +export interface ResolverPaginatedEvents { + events: SafeResolverEvent[]; + nextEvent: string | null; +} + /** * Response structure for the alerts route. */ diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index fdfa042e8fcc98..7f02d41ad1b0ca 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,8 +30,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -// FLAKY: https://github.com/elastic/kibana/issues/77957 -describe.skip('Alerts', () => { +describe('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); @@ -141,20 +140,20 @@ describe.skip('Alerts', () => { waitForAlerts(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeClosed; - cy.get(NUMBER_OF_ALERTS).invoke('text').should('eq', expectedNumberOfAlerts.toString()); - cy.get(SHOWING_ALERTS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfAlerts.toString()} alerts`); + cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(SHOWING_ALERTS).should( + 'have.text', + `Showing ${expectedNumberOfAlerts.toString()} alerts` + ); goToClosedAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS) - .invoke('text') - .should('eql', numberOfAlertsToBeClosed.toString()); - cy.get(SHOWING_ALERTS) - .invoke('text') - .should('eql', `Showing ${numberOfAlertsToBeClosed.toString()} alert`); + cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeClosed.toString()); + cy.get(SHOWING_ALERTS).should( + 'have.text', + `Showing ${numberOfAlertsToBeClosed.toString()} alert` + ); cy.get(ALERTS).should('have.length', numberOfAlertsToBeClosed); }); }); @@ -187,20 +186,20 @@ describe.skip('Alerts', () => { waitForAlerts(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeOpened; - cy.get(NUMBER_OF_ALERTS).invoke('text').should('eq', expectedNumberOfAlerts.toString()); - cy.get(SHOWING_ALERTS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfAlerts.toString()} alerts`); + cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(SHOWING_ALERTS).should( + 'have.text', + `Showing ${expectedNumberOfAlerts.toString()} alerts` + ); goToOpenedAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS) - .invoke('text') - .should('eql', numberOfAlertsToBeOpened.toString()); - cy.get(SHOWING_ALERTS) - .invoke('text') - .should('eql', `Showing ${numberOfAlertsToBeOpened.toString()} alert`); + cy.get(NUMBER_OF_ALERTS).should('have.text', numberOfAlertsToBeOpened.toString()); + cy.get(SHOWING_ALERTS).should( + 'have.text', + `Showing ${numberOfAlertsToBeOpened.toString()} alert` + ); cy.get(ALERTS).should('have.length', numberOfAlertsToBeOpened); }); }); @@ -229,23 +228,25 @@ describe.skip('Alerts', () => { cy.reload(); goToOpenedAlerts(); waitForAlertsToBeLoaded(); - waitForAlerts(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeMarkedInProgress; - cy.get(NUMBER_OF_ALERTS).invoke('text').should('eq', expectedNumberOfAlerts.toString()); - cy.get(SHOWING_ALERTS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfAlerts.toString()} alerts`); + cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts.toString()); + cy.get(SHOWING_ALERTS).should( + 'have.text', + `Showing ${expectedNumberOfAlerts.toString()} alerts` + ); goToInProgressAlerts(); waitForAlerts(); - cy.get(NUMBER_OF_ALERTS) - .invoke('text') - .should('eql', numberOfAlertsToBeMarkedInProgress.toString()); - cy.get(SHOWING_ALERTS) - .invoke('text') - .should('eql', `Showing ${numberOfAlertsToBeMarkedInProgress.toString()} alert`); + cy.get(NUMBER_OF_ALERTS).should( + 'have.text', + numberOfAlertsToBeMarkedInProgress.toString() + ); + cy.get(SHOWING_ALERTS).should( + 'have.text', + `Showing ${numberOfAlertsToBeMarkedInProgress.toString()} alert` + ); cy.get(ALERTS).should('have.length', numberOfAlertsToBeMarkedInProgress); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index d193330dc54ff4..4e2edcb282cfc4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -10,7 +10,6 @@ import { FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, } from '../screens/fields_browser'; import { - EVENTS_PAGE, HEADER_SUBTITLE, HOST_GEO_CITY_NAME_HEADER, HOST_GEO_COUNTRY_NAME_HEADER, @@ -173,7 +172,7 @@ describe.skip('Events Viewer', () => { const expectedOrderAfterDragAndDrop = 'message@timestamphost.nameevent.moduleevent.datasetevent.actionuser.namesource.ipdestination.ip'; - cy.get(EVENTS_PAGE).scrollTo('bottom'); + cy.scrollTo('bottom'); cy.get(HEADERS_GROUP).invoke('text').should('equal', originalColumnOrder); dragAndDropColumn({ column: 0, newPosition: 1 }); cy.get(HEADERS_GROUP).invoke('text').should('equal', expectedOrderAfterDragAndDrop); diff --git a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts index c19e51c3ada408..8414b4ef8f1a27 100644 --- a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts @@ -21,7 +21,8 @@ import { import { HOSTS_URL, NETWORK_URL } from '../urls/navigation'; -describe('Inspect', () => { +// FLAKY: https://github.com/elastic/kibana/issues/78496 +describe.skip('Inspect', () => { context('Hosts stats and tables', () => { before(() => { loginAndWaitForPage(HOSTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts index 7bdc461a7c73d9..3b89163392626e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts @@ -24,7 +24,20 @@ import { mlNetworkSingleIpNullKqlQuery, } from '../urls/ml_conditional_links'; -describe('ml conditional links', () => { +// FLAKY: https://github.com/elastic/kibana/issues/78512 +// FLAKY: https://github.com/elastic/kibana/issues/78511 +// FLAKY: https://github.com/elastic/kibana/issues/78510 +// FLAKY: https://github.com/elastic/kibana/issues/78509 +// FLAKY: https://github.com/elastic/kibana/issues/78508 +// FLAKY: https://github.com/elastic/kibana/issues/78507 +// FLAKY: https://github.com/elastic/kibana/issues/78506 +// FLAKY: https://github.com/elastic/kibana/issues/78505 +// FLAKY: https://github.com/elastic/kibana/issues/78504 +// FLAKY: https://github.com/elastic/kibana/issues/78503 +// FLAKY: https://github.com/elastic/kibana/issues/78502 +// FLAKY: https://github.com/elastic/kibana/issues/78501 +// FLAKY: https://github.com/elastic/kibana/issues/78500 +describe.skip('ml conditional links', () => { it('sets the KQL from a single IP with a value for the query', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); cy.get(KQL_INPUT) diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts index 05f517b5de6624..4b1ca19bd96fef 100644 --- a/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/events.ts @@ -6,8 +6,6 @@ export const CLOSE_MODAL = '[data-test-subj="modal-inspect-close"]'; -export const EVENTS_PAGE = '[data-test-subj="pageContainer"]'; - export const EVENTS_VIEWER_FIELDS_BUTTON = '[data-test-subj="events-viewer-panel"] [data-test-subj="show-field-browser"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index ebd37b522924fd..f7ef5a904de990 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -40,7 +40,7 @@ export const expandFirstAlert = () => { }; export const goToClosedAlerts = () => { - cy.get(CLOSED_ALERTS_FILTER_BTN).click({ force: true }); + cy.get(CLOSED_ALERTS_FILTER_BTN).click(); }; export const goToManageAlertsDetectionRules = () => { @@ -62,7 +62,7 @@ export const openAlerts = () => { }; export const goToInProgressAlerts = () => { - cy.get(IN_PROGRESS_ALERTS_FILTER_BTN).click({ force: true }); + cy.get(IN_PROGRESS_ALERTS_FILTER_BTN).click(); }; export const markInProgressFirstAlert = () => { @@ -86,7 +86,7 @@ export const investigateFirstAlertInTimeline = () => { }; export const waitForAlerts = () => { - cy.get(REFRESH_BUTTON).invoke('text').should('not.equal', 'Updating'); + cy.get(REFRESH_BUTTON).should('not.have.text', 'Updating'); }; export const waitForAlertsIndexToBeCreated = () => { diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index e0dea199e78ff4..24e25470feb3b3 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components'; import { TimelineId } from '../../../common/types/timeline'; import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout } from '../../timelines/components/flyout'; +import { SecuritySolutionAppWrapper } from '../../common/components/page'; import { HeaderGlobal } from '../../common/components/header_global'; import { HelpMenu } from '../../common/components/help_menu'; import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning'; @@ -20,18 +21,17 @@ import { useInitSourcerer, useSourcererScope } from '../../common/containers/sou import { useKibana } from '../../common/lib/kibana'; import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; +import { useThrottledResizeObserver } from '../../common/components/utils'; -const SecuritySolutionAppWrapper = styled.div` +const Main = styled.main.attrs<{ paddingTop: number }>(({ paddingTop }) => ({ + style: { + paddingTop: `${paddingTop}px`, + }, +}))<{ paddingTop: number }>` + overflow: auto; display: flex; flex-direction: column; - height: 100%; - width: 100%; -`; -SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper'; - -const Main = styled.main` - overflow: auto; - flex: 1; + flex: 1 1 auto; `; Main.displayName = 'Main'; @@ -45,7 +45,7 @@ interface HomePageProps { const HomePageComponent: React.FC = ({ children }) => { const { application } = useKibana().services; const subPluginId = useRef(''); - + const { ref, height = 0 } = useThrottledResizeObserver(300); application.currentAppId$.subscribe((appId) => { subPluginId.current = appId ?? ''; }); @@ -61,9 +61,9 @@ const HomePageComponent: React.FC = ({ children }) => { return ( - + -
+
{indicesExist && showTimeline && ( diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index ad113d3e7e7372..750ff49cd700cd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -402,7 +402,7 @@ export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { } if (isLoading) { return ( - + diff --git a/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx b/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx index 06715514e01bf6..b89a7e8eefec32 100644 --- a/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/wrappers/index.tsx @@ -10,8 +10,7 @@ import { gutterTimeline } from '../../../common/lib/helpers'; export const WhitePageWrapper = styled.div` background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; border-top: ${({ theme }) => theme.eui.euiBorderThin}; - height: 100%; - min-height: 100vh; + flex: 1 1 auto; `; export const SectionWrapper = styled.div` diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index cd43c7e4930657..c53d311dc1361c 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -29,6 +29,7 @@ const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : `${DEFAULT_EVENTS_VIEWER_HEIGHT}px`)}; + flex: 1 1 auto; display: flex; width: 100%; `; diff --git a/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx index 8c5ad95a8de0e9..cd4740bc8c4649 100644 --- a/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx @@ -6,11 +6,16 @@ import { EuiButton, EuiWindowEvent } from '@elastic/eui'; import React, { useCallback } from 'react'; +import styled from 'styled-components'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from './translations'; +const StyledEuiButton = styled(EuiButton)` + margin: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + export const ExitFullScreen: React.FC = () => { const { globalFullScreen, setGlobalFullScreen } = useFullScreen(); @@ -36,14 +41,14 @@ export const ExitFullScreen: React.FC = () => { return ( <> - {i18n.EXIT_FULL_SCREEN} - + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index 5b4dd2e9728bba..0c6a54d4434d21 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -6,7 +6,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { pickBy } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { forwardRef, useCallback } from 'react'; import styled from 'styled-components'; import { OutPortal } from 'react-reverse-portal'; @@ -24,30 +24,37 @@ import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/c import { useGlobalHeaderPortal } from '../../hooks/use_global_header_portal'; import { LinkAnchor } from '../links'; -const Wrapper = styled.header<{ $globalFullScreen: boolean }>` - ${({ $globalFullScreen, theme }) => ` +const Wrapper = styled.header` + ${({ theme }) => ` background: ${theme.eui.euiColorEmptyShade}; border-bottom: ${theme.eui.euiBorderThin}; - padding-top: ${$globalFullScreen ? theme.eui.paddingSizes.s : theme.eui.paddingSizes.m}; width: 100%; z-index: ${theme.eui.euiZNavigation}; + position: fixed; `} `; Wrapper.displayName = 'Wrapper'; +const WrapperContent = styled.div<{ $globalFullScreen: boolean }>` + display: ${({ $globalFullScreen }) => ($globalFullScreen ? 'none' : 'block')}; + padding-top: ${({ $globalFullScreen, theme }) => + $globalFullScreen ? theme.eui.paddingSizes.s : theme.eui.paddingSizes.m}; +`; + +WrapperContent.displayName = 'WrapperContent'; + const FlexItem = styled(EuiFlexItem)` min-width: 0; `; FlexItem.displayName = 'FlexItem'; -const FlexGroup = styled(EuiFlexGroup)<{ $globalFullScreen: boolean; $hasSibling: boolean }>` - ${({ $globalFullScreen, $hasSibling, theme }) => ` +const FlexGroup = styled(EuiFlexGroup)<{ $hasSibling: boolean }>` + ${({ $hasSibling, theme }) => ` border-bottom: ${theme.eui.euiBorderThin}; margin-bottom: 1px; padding-bottom: 4px; padding-left: ${theme.eui.paddingSizes.l}; padding-right: ${gutterTimeline}; - ${$globalFullScreen ? 'display: none;' : ''} ${$hasSibling ? `border-bottom: ${theme.eui.euiBorderThin};` : 'border-bottom-width: 0px;'} `} `; @@ -56,77 +63,74 @@ FlexGroup.displayName = 'FlexGroup'; interface HeaderGlobalProps { hideDetectionEngine?: boolean; } -export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { - const { globalHeaderPortalNode } = useGlobalHeaderPortal(); - const { globalFullScreen } = useFullScreen(); - const search = useGetUrlSearch(navTabs.overview); - const { application, http } = useKibana().services; - const { navigateToApp } = application; - const basePath = http.basePath.get(); - const goToOverview = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { path: search }); - }, - [navigateToApp, search] - ); - - return ( - - - <> - - - - - - - +export const HeaderGlobal = React.memo( + forwardRef(({ hideDetectionEngine = false }, ref) => { + const { globalHeaderPortalNode } = useGlobalHeaderPortal(); + const { globalFullScreen } = useFullScreen(); + const search = useGetUrlSearch(navTabs.overview); + const { application, http } = useKibana().services; + const { navigateToApp } = application; + const basePath = http.basePath.get(); + const goToOverview = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { path: search }); + }, + [navigateToApp, search] + ); + return ( + + + + + + + + + + - - key !== SecurityPageName.detections, navTabs) - : navTabs - } - /> - - - + + key !== SecurityPageName.detections, navTabs) + : navTabs + } + /> + + + + + + {window.location.pathname.includes(APP_DETECTIONS_PATH) && ( + + + + )} - - - {window.location.pathname.includes(APP_DETECTIONS_PATH) && ( - + + {i18n.BUTTON_ADD_DATA} + - )} - - - - {i18n.BUTTON_ADD_DATA} - - - - - - -
- -
-
- ); -}); + + + + + + + ); + }) +); HeaderGlobal.displayName = 'HeaderGlobal'; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index 140429dc4abd78..8a8eda3e20185f 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -8,27 +8,36 @@ import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@e import styled, { createGlobalStyle } from 'styled-components'; import { + GLOBAL_HEADER_HEIGHT, FULL_SCREEN_TOGGLED_CLASS_NAME, SCROLLING_DISABLED_CLASS_NAME, } from '../../../../common/constants'; +export const SecuritySolutionAppWrapper = styled.div` + display: flex; + flex-direction: column; + flex: 1 1 auto; + width: 100%; +`; +SecuritySolutionAppWrapper.displayName = 'SecuritySolutionAppWrapper'; + /* SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly and `EuiPopover`, `EuiToolTip` global styles */ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimary: string } } }>` - /* dirty hack to fix draggables with tooltip on FF */ - body#siem-app { - position: static; - } - /* end of dirty hack to fix draggables with tooltip on FF */ - div.app-wrapper { background-color: rgba(0,0,0,0); } div.application { background-color: rgba(0,0,0,0); + + // Security App wrapper + > div { + display: flex; + flex: 1 1 auto; + } } .euiPopover__panel.euiPopover__panel-isOpen { @@ -67,37 +76,8 @@ export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimar ${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`}; } - body { - overflow-y: hidden; - } - - #kibana-body { - height: 100%; - overflow-y: hidden; - - > .content { - height: 100%; - - > .app-wrapper { - height: 100%; - - > .app-wrapper-panel { - height: 100%; - - > .application { - height: 100%; - - > div { - height: 100%; - } - } - } - } - } - } - - .${SCROLLING_DISABLED_CLASS_NAME} #kibana-body { - overflow-y: hidden; + .${SCROLLING_DISABLED_CLASS_NAME} ${SecuritySolutionAppWrapper} { + max-height: calc(100vh - ${GLOBAL_HEADER_HEIGHT}px); } `; diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx index f3136b0a40b3e4..0908c887d25f6e 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx @@ -28,6 +28,9 @@ const Wrapper = styled.div` &.siemWrapperPage--fullHeight { height: 100%; + display: flex; + flex-direction: column; + flex: 1 1 auto; } &.siemWrapperPage--withTimeline { @@ -36,6 +39,9 @@ const Wrapper = styled.div` &.siemWrapperPage--noPadding { padding: 0; + display: flex; + flex-direction: column; + flex: 1 1 auto; } `; diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts index 55d52d4ba32524..7b09e748c0c286 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/factory.ts @@ -11,6 +11,8 @@ import { ResolverRelatedEvents, ResolverTree, ResolverEntityIndex, + ResolverPaginatedEvents, + SafeResolverEvent, } from '../../../common/endpoint/types'; /** @@ -22,12 +24,54 @@ export function dataAccessLayerFactory( const dataAccessLayer: DataAccessLayer = { /** * Used to get non-process related events for a node. + * @deprecated use the new API (eventsWithEntityIDAndCategory & event) instead */ async relatedEvents(entityID: string): Promise { - return context.services.http.post(`/api/endpoint/resolver/${entityID}/events`, { - query: { events: 100 }, + const response: ResolverPaginatedEvents = await context.services.http.post( + '/api/endpoint/resolver/events', + { + query: {}, + body: JSON.stringify({ + filter: `process.entity_id:"${entityID}" and not event.category:"process"`, + }), + } + ); + + return { ...response, entityID }; + }, + + /** + * Return events that have `process.entity_id` that includes `entityID` and that have + * a `event.category` that includes `category`. + */ + async eventsWithEntityIDAndCategory( + entityID: string, + category: string, + after?: string + ): Promise { + return context.services.http.post('/api/endpoint/resolver/events', { + query: { afterEvent: after }, + body: JSON.stringify({ + filter: `process.entity_id:"${entityID}" and event.category:"${category}"`, + }), }); }, + + /** + * Return up to one event that has an `event.id` that includes `eventID`. + */ + async event(eventID: string): Promise { + const response: ResolverPaginatedEvents = await context.services.http.post( + '/api/endpoint/resolver/events', + { + query: {}, + body: JSON.stringify({ filter: `event.id:"${eventID}"` }), + } + ); + const [oneEvent] = response.events; + return oneEvent ?? null; + }, + /** * Used to get descendant and ancestor process events for a node. */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts index 631eab18fc014e..88a3052a61f743 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/emptify_mock.ts @@ -44,6 +44,7 @@ export function emptifyMock( return { metadata, dataAccessLayer: { + ...dataAccessLayer, /** * Fetch related events for an entity ID */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts index fd086bd9b984e6..09625e5726b1d5 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts @@ -6,6 +6,7 @@ import { ResolverRelatedEvents, + SafeResolverEvent, ResolverTree, ResolverEntityIndex, } from '../../../../common/endpoint/types'; @@ -58,6 +59,29 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me }); }, + /** + * Return events that have `process.entity_id` that includes `entityID` and that have + * a `event.category` that includes `category`. + */ + async eventsWithEntityIDAndCategory( + entityID: string, + category: string, + after?: string + ): Promise<{ + events: SafeResolverEvent[]; + nextEvent: string | null; + }> { + const events: SafeResolverEvent[] = []; + return { + events, + nextEvent: null, + }; + }, + + async event(_eventID: string): Promise { + return null; + }, + /** * Fetch a ResolverTree for a entityID */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts index 86450b25eb1dad..3bbe4bcf510607 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts @@ -8,6 +8,7 @@ import { ResolverRelatedEvents, ResolverTree, ResolverEntityIndex, + SafeResolverEvent, } from '../../../../common/endpoint/types'; import { mockEndpointEvent } from '../../mocks/endpoint_event'; import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree'; @@ -69,6 +70,32 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { }); }, + async eventsWithEntityIDAndCategory( + entityID: string, + category, + after?: string + ): Promise<{ + events: SafeResolverEvent[]; + nextEvent: string | null; + }> { + return { + events: [ + mockEndpointEvent({ + entityID, + eventCategory: category, + }), + ], + nextEvent: null, + }; + }, + + async event(eventID: string): Promise { + return mockEndpointEvent({ + entityID: metadata.entityIDs.origin, + eventID, + }); + }, + /** * Fetch a ResolverTree for a entityID */ diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts index ec773a09ae8e04..6fb84eaf7fda63 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts @@ -10,7 +10,9 @@ import { ResolverRelatedEvents, ResolverTree, ResolverEntityIndex, + SafeResolverEvent, } from '../../../../common/endpoint/types'; +import * as eventModel from '../../../../common/endpoint/models/event'; interface Metadata { /** @@ -56,31 +58,62 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { /** * Fetch related events for an entity ID */ - relatedEvents(entityID: string): Promise { + async relatedEvents(entityID: string): Promise { /** * Respond with the mocked related events when the origin's related events are fetched. **/ const events = entityID === metadata.entityIDs.origin ? tree.relatedEvents.events : []; - return Promise.resolve({ + return { entityID, events, nextEvent: null, - }); + }; + }, + + /** + * Any of the origin's related events by category. + * `entityID` must match the origin node's `process.entity_id`. + * Does not respect the `_after` parameter. + */ + async eventsWithEntityIDAndCategory( + entityID: string, + category: string, + after?: string + ): Promise<{ events: SafeResolverEvent[]; nextEvent: string | null }> { + const events = + entityID === metadata.entityIDs.origin + ? tree.relatedEvents.events.filter((event) => + eventModel.eventCategory(event).includes(category) + ) + : []; + return { + events, + nextEvent: null, + }; + }, + + /** + * Any of the origin's related events by event.id + */ + async event(eventID: string): Promise { + return ( + tree.relatedEvents.events.find((event) => eventModel.eventID(event) === eventID) ?? null + ); }, /** * Fetch a ResolverTree for a entityID */ - resolverTree(): Promise { - return Promise.resolve(tree); + async resolverTree(): Promise { + return tree; }, /** * Get entities matching a document. */ - entities(): Promise { - return Promise.resolve([{ entity_id: metadata.entityIDs.origin }]); + async entities(): Promise { + return [{ entity_id: metadata.entityIDs.origin }]; }, }, }; diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts index 6a4955b104b8fb..a3ec667385470a 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/pausify_mock.ts @@ -89,6 +89,7 @@ export function pausifyMock({ } }, dataAccessLayer: { + ...dataAccessLayer, /** * Fetch related events for an entity ID */ diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/get_ui_settings.ts b/x-pack/plugins/security_solution/public/resolver/mocks/get_ui_settings.ts new file mode 100644 index 00000000000000..ab1a5c86859ac5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/mocks/get_ui_settings.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function getUiSettings(key: string): string | undefined { + if (key === 'dateFormat') { + return 'MMM D, YYYY @ HH:mm:ss.SSS'; + } + if (key === 'dateFormat:tz') { + return 'America/New_York'; + } +} diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index 8691ecac4d1ccf..3f7c58efc762b9 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -21,19 +21,19 @@ export function mockTreeWith2AncestorsAndNoChildren({ entityID: secondAncestorID, processName: 'a', parentEntityID: 'none', - timestamp: 0, + timestamp: 1600863932316, }); const firstAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, processName: 'b', parentEntityID: secondAncestorID, - timestamp: 1, + timestamp: 1600863932317, }); const originEvent: SafeResolverEvent = mockEndpointEvent({ entityID: originID, processName: 'c', parentEntityID: firstAncestorID, - timestamp: 2, + timestamp: 1600863932318, }); return { entityID: originID, @@ -68,39 +68,39 @@ export function mockTreeWithAllProcessesTerminated({ entityID: secondAncestorID, processName: 'a', parentEntityID: 'none', - timestamp: 0, + timestamp: 1600863932316, }); const firstAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, processName: 'b', parentEntityID: secondAncestorID, - timestamp: 1, + timestamp: 1600863932317, }); const originEvent: SafeResolverEvent = mockEndpointEvent({ entityID: originID, processName: 'c', parentEntityID: firstAncestorID, - timestamp: 2, + timestamp: 1600863932318, }); const secondAncestorTermination: SafeResolverEvent = mockEndpointEvent({ entityID: secondAncestorID, processName: 'a', parentEntityID: 'none', - timestamp: 0, + timestamp: 1600863932316, eventType: 'end', }); const firstAncestorTermination: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, processName: 'b', parentEntityID: secondAncestorID, - timestamp: 1, + timestamp: 1600863932317, eventType: 'end', }); const originEventTermination: SafeResolverEvent = mockEndpointEvent({ entityID: originID, processName: 'c', parentEntityID: firstAncestorID, - timestamp: 2, + timestamp: 1600863932318, eventType: 'end', }); return ({ @@ -162,21 +162,21 @@ export function mockTreeWithNoAncestorsAnd2Children({ entityID: originID, processName: 'c.ext', parentEntityID: 'none', - timestamp: 0, + timestamp: 1600863932316, }); const firstChild: SafeResolverEvent = mockEndpointEvent({ pid: 1, entityID: firstChildID, processName: 'd', parentEntityID: originID, - timestamp: 1, + timestamp: 1600863932317, }); const secondChild: SafeResolverEvent = mockEndpointEvent({ pid: 2, entityID: secondChildID, processName: 'e', parentEntityID: originID, - timestamp: 2, + timestamp: 1600863932318, }); return { @@ -216,50 +216,50 @@ export function mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents const ancestor: SafeResolverEvent = mockEndpointEvent({ entityID: ancestorID, processName: ancestorID, - timestamp: 1, + timestamp: 1600863932317, parentEntityID: undefined, }); const ancestorClone: SafeResolverEvent = mockEndpointEvent({ entityID: ancestorID, processName: ancestorID, - timestamp: 1, + timestamp: 1600863932317, parentEntityID: undefined, }); const origin: SafeResolverEvent = mockEndpointEvent({ entityID: originID, processName: originID, parentEntityID: ancestorID, - timestamp: 0, + timestamp: 1600863932316, }); const originClone: SafeResolverEvent = mockEndpointEvent({ entityID: originID, processName: originID, parentEntityID: ancestorID, - timestamp: 0, + timestamp: 1600863932316, }); const firstChild: SafeResolverEvent = mockEndpointEvent({ entityID: firstChildID, processName: firstChildID, parentEntityID: originID, - timestamp: 1, + timestamp: 1600863932317, }); const firstChildClone: SafeResolverEvent = mockEndpointEvent({ entityID: firstChildID, processName: firstChildID, parentEntityID: originID, - timestamp: 1, + timestamp: 1600863932317, }); const secondChild: SafeResolverEvent = mockEndpointEvent({ entityID: secondChildID, processName: secondChildID, parentEntityID: originID, - timestamp: 2, + timestamp: 1600863932318, }); const secondChildClone: SafeResolverEvent = mockEndpointEvent({ entityID: secondChildID, processName: secondChildID, parentEntityID: originID, - timestamp: 2, + timestamp: 1600863932318, }); return ({ diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 9d10d1c2b64a77..ea603f25834313 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { Store, createStore, applyMiddleware } from 'redux'; import { mount, ReactWrapper } from 'enzyme'; import { History as HistoryPackageHistoryInterface, createMemoryHistory } from 'history'; -import { CoreStart } from '../../../../../../../src/core/public'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { spyMiddlewareFactory } from '../spy_middleware_factory'; import { resolverMiddlewareFactory } from '../../store/middleware'; @@ -17,6 +16,7 @@ import { MockResolver } from './mock_resolver'; import { ResolverState, DataAccessLayer, SpyMiddleware, SideEffectSimulator } from '../../types'; import { ResolverAction } from '../../store/actions'; import { sideEffectSimulatorFactory } from '../../view/side_effect_simulator_factory'; +import { getUiSettings } from '../../mocks/get_ui_settings'; /** * Test a Resolver instance using jest, enzyme, and a mock data layer. @@ -91,7 +91,9 @@ export class Simulator { this.history = history ?? createMemoryHistory(); // Used for `KibanaContextProvider` - const coreStart: CoreStart = coreMock.createStart(); + const coreStart = coreMock.createStart(); + + coreStart.uiSettings.get.mockImplementation(getUiSettings); this.sideEffectSimulator = sideEffectSimulatorFactory(); @@ -296,10 +298,7 @@ export class Simulator { const title = titles.at(index).text(); const description = descriptions.at(index).text(); - // Exclude timestamp since we can't currently calculate the expected description for it from tests - if (title !== '@timestamp') { - entries.push([title, description]); - } + entries.push([title, description]); } return entries; } diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 4dc614abe3345d..64147dd8feb75c 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -16,6 +16,7 @@ import { ResolverTree, ResolverEntityIndex, SafeResolverEvent, + ResolverPaginatedEvents, } from '../../common/endpoint/types'; /** @@ -503,6 +504,21 @@ export interface DataAccessLayer { */ relatedEvents: (entityID: string) => Promise; + /** + * Return events that have `process.entity_id` that includes `entityID` and that have + * a `event.category` that includes `category`. + */ + eventsWithEntityIDAndCategory: ( + entityID: string, + category: string, + after?: string + ) => Promise; + + /** + * Return up to one event that has an `event.id` that includes `eventID`. + */ + event: (eventID: string) => Promise; + /** * Fetch a ResolverTree for a entityID */ diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 2f23469606acab..63a70716b2d417 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -88,6 +88,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and title: 'c.ext', titleIcon: 'Running Process', detailEntries: [ + ['@timestamp', 'Sep 23, 2020 @ 08:25:32.316'], ['process.executable', 'executable'], ['process.pid', '0'], ['user.name', 'user.name'], @@ -128,6 +129,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and await expect( simulator().map(() => simulator().nodeDetailDescriptionListEntries()) ).toYieldEqualTo([ + ['@timestamp', 'Sep 23, 2020 @ 08:25:32.317'], ['process.executable', 'executable'], ['process.pid', '1'], ['user.name', 'user.name'], @@ -168,6 +170,7 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children and await expect( simulator().map(() => simulator().nodeDetailDescriptionListEntries()) ).toYieldEqualTo([ + ['@timestamp', 'Sep 23, 2020 @ 08:25:32.316'], ['process.executable', 'executable'], ['process.pid', '0'], ['user.name', 'user.name'], diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index 72f0d54d51fa3b..168752c507d5a9 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -15,7 +15,12 @@ import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import { StyledPanel } from '../styles'; -import { BoldCode, StyledTime, GeneratedText, formatDate } from './panel_content_utilities'; +import { + BoldCode, + StyledTime, + GeneratedText, + noTimestampRetrievedText, +} from './panel_content_utilities'; import { Breadcrumbs } from './breadcrumbs'; import * as eventModel from '../../../../common/endpoint/models/event'; import * as selectors from '../../store/selectors'; @@ -25,6 +30,7 @@ import { DescriptiveName } from './descriptive_name'; import { useLinkProps } from '../use_link_props'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { deepObjectEntries } from './deep_object_entries'; +import { useFormattedDate } from './use_formatted_date'; export const EventDetail = memo(function EventDetail({ nodeID, @@ -78,12 +84,8 @@ const EventDetailContents = memo(function ({ eventType: string; processEvent: SafeResolverEvent; }) { - const formattedDate = useMemo(() => { - const timestamp = eventModel.timestampSafeVersion(event); - if (timestamp !== undefined) { - return formatDate(new Date(timestamp)); - } - }, [event]); + const timestamp = eventModel.timestampSafeVersion(event); + const formattedDate = useFormattedDate(timestamp) || noTimestampRetrievedText; return ( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx index 04e9de61f62568..181c9ac8ab8a08 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx @@ -16,7 +16,7 @@ import { EuiDescriptionListProps } from '@elastic/eui/src/components/description import { StyledDescriptionList, StyledTitle } from './styles'; import * as selectors from '../../store/selectors'; import * as eventModel from '../../../../common/endpoint/models/event'; -import { formatDate, GeneratedText } from './panel_content_utilities'; +import { GeneratedText } from './panel_content_utilities'; import { Breadcrumbs } from './breadcrumbs'; import { processPath, processPID } from '../../models/process_event'; import { CubeForProcess } from './cube_for_process'; @@ -26,6 +26,7 @@ import { ResolverState } from '../../types'; import { PanelLoading } from './panel_loading'; import { StyledPanel } from '../styles'; import { useLinkProps } from '../use_link_props'; +import { useFormattedDate } from './use_formatted_date'; const StyledCubeForProcess = styled(CubeForProcess)` position: relative; @@ -65,10 +66,10 @@ const NodeDetailView = memo(function ({ const relatedEventTotal = useSelector((state: ResolverState) => { return selectors.relatedEventTotalCount(state)(nodeID); }); - const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { - const eventTime = eventModel.eventTimestamp(processEvent); - const dateTime = eventTime === undefined ? null : formatDate(eventTime); + const eventTime = eventModel.eventTimestamp(processEvent); + const dateTime = useFormattedDate(eventTime); + const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { const createdEntry = { title: '@timestamp', description: dateTime, @@ -131,7 +132,7 @@ const NodeDetailView = memo(function ({ }); return processDescriptionListData; - }, [processEvent]); + }, [dateTime, processEvent]); const nodesLinkNavProps = useLinkProps({ panelView: 'nodes', diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index 281794ac24d244..771a143a9c0cde 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -10,7 +10,7 @@ import { EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from '@elastic/ import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import { StyledPanel } from '../styles'; -import { formatDate, BoldCode, StyledTime } from './panel_content_utilities'; +import { BoldCode, noTimestampRetrievedText, StyledTime } from './panel_content_utilities'; import { Breadcrumbs } from './breadcrumbs'; import * as eventModel from '../../../../common/endpoint/models/event'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; @@ -19,6 +19,7 @@ import { ResolverState } from '../../types'; import { PanelLoading } from './panel_loading'; import { DescriptiveName } from './descriptive_name'; import { useLinkProps } from '../use_link_props'; +import { useFormattedDate } from './use_formatted_date'; /** * Render a list of events that are related to `nodeID` and that have a category of `eventType`. @@ -83,7 +84,7 @@ const NodeEventsListItem = memo(function ({ eventType: string; }) { const timestamp = eventModel.eventTimestamp(event); - const date = timestamp !== undefined ? formatDate(timestamp) : timestamp; + const date = useFormattedDate(timestamp) || noTimestampRetrievedText; const linkProps = useLinkProps({ panelView: 'eventDetail', panelParameters: { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx index 8fc6e7cc66c790..78d3477301539f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx @@ -32,7 +32,6 @@ import { } from './styles'; import * as eventModel from '../../../../common/endpoint/models/event'; import * as selectors from '../../store/selectors'; -import { formatter } from './panel_content_utilities'; import { Breadcrumbs } from './breadcrumbs'; import { CubeForProcess } from './cube_for_process'; import { LimitWarning } from '../limit_warnings'; @@ -41,6 +40,8 @@ import { useLinkProps } from '../use_link_props'; import { useColors } from '../use_colors'; import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { ResolverAction } from '../../store/actions'; +import { useFormattedDate } from './use_formatted_date'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; interface ProcessTableView { name?: string; @@ -80,18 +81,7 @@ export const NodeList = memo(() => { dataType: 'date', sortable: true, render(eventDate?: Date) { - return eventDate ? ( - formatter.format(eventDate) - ) : ( - - {i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.table.row.timestampInvalidLabel', - { - defaultMessage: 'invalid', - } - )} - - ); + return ; }, }, ], @@ -220,3 +210,9 @@ function NodeDetailLink({ ); } + +const NodeDetailTimestamp = memo(({ eventDate }: { eventDate: Date | undefined }) => { + const formattedDate = useFormattedDate(eventDate); + + return formattedDate ? <>{formattedDate} : getEmptyTagValue(); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 5ca34b33b2396e..a20498cbfb67b2 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -6,11 +6,22 @@ /* eslint-disable react/display-name */ -import { i18n } from '@kbn/i18n'; import { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import React, { memo } from 'react'; +/** + * Text to use in place of an undefined timestamp value + */ + +export const noTimestampRetrievedText = i18n.translate( + 'xpack.securitySolution.enpdoint.resolver.panelutils.noTimestampRetrieved', + { + defaultMessage: 'No timestamp retrieved', + } +); + /** * A bold version of EuiCode to display certain titles with */ @@ -59,33 +70,3 @@ export const StyledTime = memo(styled('time')` display: inline-block; text-align: start; `); - -/** - * Long formatter (to second) for DateTime - */ -export const formatter = new Intl.DateTimeFormat(i18n.getLocale(), { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', -}); - -/** - * @returns {string} A nicely formatted string for a date - */ -export function formatDate( - /** To be passed through Date->Intl.DateTimeFormat */ timestamp: ConstructorParameters< - typeof Date - >[0] -): string { - const date = new Date(timestamp); - if (isFinite(date.getTime())) { - return formatter.format(date); - } else { - return i18n.translate('xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate', { - defaultMessage: 'Invalid Date', - }); - } -} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.test.tsx new file mode 100644 index 00000000000000..9e9ae26900efaf --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, RenderResult } from '@testing-library/react'; +import { useFormattedDate } from './use_formatted_date'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { getUiSettings } from '../../mocks/get_ui_settings'; + +describe('useFormattedDate', () => { + let element: HTMLElement; + const testID = 'formattedDate'; + let reactRenderResult: ( + date: ConstructorParameters[0] | Date | undefined + ) => RenderResult; + + beforeEach(async () => { + const mockCoreStart = coreMock.createStart(); + mockCoreStart.uiSettings.get.mockImplementation(getUiSettings); + + function Test({ date }: { date: ConstructorParameters[0] | Date | undefined }) { + const formattedDate = useFormattedDate(date); + return
{formattedDate}
; + } + + reactRenderResult = ( + date: ConstructorParameters[0] | Date | undefined + ): RenderResult => + render( + + + + ); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('when the provided date is undefined', () => { + it('should return undefined', async () => { + const { findByTestId } = reactRenderResult(undefined); + element = await findByTestId(testID); + + expect(element).toBeEmpty(); + }); + }); + + describe('when the provided date is empty', () => { + it('should return undefined', async () => { + const { findByTestId } = reactRenderResult(''); + element = await findByTestId(testID); + + expect(element).toBeEmpty(); + }); + }); + + describe('when the provided date is an invalid date', () => { + it('should return the string invalid date', async () => { + const { findByTestId } = reactRenderResult('randomString'); + element = await findByTestId(testID); + + expect(element).toHaveTextContent('Invalid Date'); + }); + }); + + describe('when the provided date is a stringified unix timestamp', () => { + it('should return the string invalid date', async () => { + const { findByTestId } = reactRenderResult('1600863932316'); + element = await findByTestId(testID); + + expect(element).toHaveTextContent('Invalid Date'); + }); + }); + + describe('when the provided date is a valid numerical timestamp', () => { + it('should return the string invalid date', async () => { + const { findByTestId } = reactRenderResult(1600863932316); + element = await findByTestId(testID); + + expect(element).toHaveTextContent('Sep 23, 2020 @ 08:25:32.316'); + }); + }); + + describe('when the provided date is a date string', () => { + it('should return the string invalid date', async () => { + const { findByTestId } = reactRenderResult('2020-09-23T12:25:32Z'); + element = await findByTestId(testID); + + expect(element).toHaveTextContent('Sep 23, 2020 @ 08:25:32.000'); + }); + }); + + describe('when the provided date is a valid date', () => { + it('should return the string invalid date', async () => { + const validDate = new Date(1600863932316); + const { findByTestId } = reactRenderResult(validDate); + element = await findByTestId(testID); + + expect(element).toHaveTextContent('Sep 23, 2020 @ 08:25:32.316'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.ts b/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.ts new file mode 100644 index 00000000000000..05e7154dd6fdd3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/use_formatted_date.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import moment from 'moment-timezone'; +import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; + +const invalidDateText = i18n.translate( + 'xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate', + { + defaultMessage: 'Invalid Date', + } +); + +/** + * Long formatter (to second) for DateTime + */ +const formatter = new Intl.DateTimeFormat(i18n.getLocale(), { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', +}); + +/** + * + * @description formats a given time based on the user defined format in the advanced settings section of kibana under dateFormat + * @export + * @param {(ConstructorParameters[0] | undefined)} timestamp + * @returns {(string | null)} - Either a formatted date or the text 'Invalid Date' + */ +export function useFormattedDate( + timestamp: ConstructorParameters[0] | Date | undefined +): string | undefined { + const dateFormatSetting: string = useUiSetting('dateFormat'); + const timezoneSetting: string = useUiSetting('dateFormat:tz'); + const usableTimezoneSetting = timezoneSetting === 'Browser' ? moment.tz.guess() : timezoneSetting; + + if (!timestamp) return undefined; + + const date = new Date(timestamp); + if (date && Number.isFinite(date.getTime())) { + return dateFormatSetting + ? moment.tz(date, usableTimezoneSetting).format(dateFormatSetting) + : formatter.format(date); + } + + return invalidDateText; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index 3ec968e4a0e1a1..c9159032a7917b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -8,29 +8,42 @@ import { IRouter } from 'kibana/server'; import { EndpointAppContext } from '../types'; import { validateTree, + validateRelatedEvents, validateEvents, validateChildren, validateAncestry, validateAlerts, validateEntities, } from '../../../common/endpoint/schema/resolver'; -import { handleEvents } from './resolver/events'; +import { handleRelatedEvents } from './resolver/related_events'; import { handleChildren } from './resolver/children'; import { handleAncestry } from './resolver/ancestry'; import { handleTree } from './resolver/tree'; import { handleAlerts } from './resolver/alerts'; import { handleEntities } from './resolver/entity'; +import { handleEvents } from './resolver/events'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); + // this route will be removed in favor of the one below router.post( { + // @deprecated use `/resolver/events` instead path: '/api/endpoint/resolver/{id}/events', + validate: validateRelatedEvents, + options: { authRequired: true }, + }, + handleRelatedEvents(log, endpointAppContext) + ); + + router.post( + { + path: '/api/endpoint/resolver/events', validate: validateEvents, options: { authRequired: true }, }, - handleEvents(log, endpointAppContext) + handleEvents(log) ); router.post( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts index 80d21ae1182842..a212215fe18a82 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/events.ts @@ -6,32 +6,39 @@ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; -import { eventsIndexPattern, alertsIndexPattern } from '../../../../common/endpoint/constants'; +import { eventsIndexPattern } from '../../../../common/endpoint/constants'; import { validateEvents } from '../../../../common/endpoint/schema/resolver'; -import { Fetcher } from './utils/fetch'; -import { EndpointAppContext } from '../../types'; +import { EventsQuery } from './queries/events'; +import { createEvents } from './utils/node'; +import { PaginationBuilder } from './utils/pagination'; +/** + * This function handles the `/events` api and returns an array of events and a cursor if more events exist than were + * requested. + * @param log a logger object + */ export function handleEvents( - log: Logger, - endpointAppContext: EndpointAppContext + log: Logger ): RequestHandler< - TypeOf, + unknown, TypeOf, TypeOf > { return async (context, req, res) => { const { - params: { id }, - query: { events, afterEvent, legacyEndpointID: endpointID }, + query: { limit, afterEvent }, body, } = req; try { - const client = context.core.elasticsearch.legacy.client; - - const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); + const client = context.core.elasticsearch.client; + const query = new EventsQuery( + PaginationBuilder.createBuilder(limit, afterEvent), + eventsIndexPattern + ); + const results = await query.search(client, body?.filter); return res.ok({ - body: await fetcher.events(events, afterEvent, body?.filter), + body: createEvents(results, PaginationBuilder.buildCursorRequestLimit(limit, results)), }); } catch (err) { log.warn(err); 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 bd054d548a93aa..3be0cc5daff792 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 @@ -4,86 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; +import { IScopedClusterClient } from 'kibana/server'; +import { ApiResponse } from '@elastic/elasticsearch'; import { esKuery } from '../../../../../../../../src/plugins/data/server'; import { SafeResolverEvent } from '../../../../../common/endpoint/types'; -import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** - * Builds a query for retrieving related events for a node. + * Builds a query for retrieving events. */ -export class EventsQuery extends ResolverQuery { - private readonly kqlQuery: JsonObject[] = []; - +export class EventsQuery { constructor( private readonly pagination: PaginationBuilder, - indexPattern: string | string[], - endpointID?: string, - kql?: string - ) { - super(indexPattern, endpointID); - if (kql) { - this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); - } - } + private readonly indexPattern: string | string[] + ) {} - protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { + private query(kqlQuery: JsonObject[]): JsonObject { return { query: { bool: { filter: [ - ...this.kqlQuery, - { - terms: { 'endgame.unique_pid': uniquePIDs }, - }, - { - term: { 'agent.id': endpointID }, - }, + ...kqlQuery, { term: { 'event.kind': 'event' }, }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, ], }, }, - ...this.pagination.buildQueryFields('endgame.serial_event_id', 'desc'), + ...this.pagination.buildQueryFields('event.id', 'desc'), }; } - protected query(entityIDs: string[]): JsonObject { + private buildSearch(kql: JsonObject[]) { return { - query: { - bool: { - filter: [ - ...this.kqlQuery, - { - terms: { 'process.entity_id': entityIDs }, - }, - { - term: { 'event.kind': 'event' }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - ...this.pagination.buildQueryFields('event.id', 'desc'), + body: this.query(kql), + index: this.indexPattern, }; } - formatResponse(response: SearchResponse): SafeResolverEvent[] { - return this.getResults(response); + /** + * Searches ES for the specified events and format the response. + * + * @param client a client for searching ES + * @param kql an optional kql string for filtering the results + */ + async search(client: IScopedClusterClient, kql?: string): Promise { + const kqlQuery: JsonObject[] = []; + if (kql) { + kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); + } + const response: ApiResponse> = await client.asCurrentUser.search(this.buildSearch(kqlQuery)); + return response.body.hits.hits.map((hit) => hit._source); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts similarity index 93% rename from x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.test.ts rename to x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts index 00d7570b2b65e2..3ddf8fa4090d6e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.test.ts @@ -3,7 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EventsQuery } from './events'; +/** + * @deprecated use the `events.ts` file's query instead + */ +import { EventsQuery } from './related_events'; import { PaginationBuilder } from '../utils/pagination'; import { legacyEventIndexPattern } from './legacy_event_index_pattern'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts new file mode 100644 index 00000000000000..f419c1fb6e1d5b --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/related_events.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/** + * @deprecated use the `events.ts` file's query instead + */ +import { SearchResponse } from 'elasticsearch'; +import { esKuery } from '../../../../../../../../src/plugins/data/server'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; +import { ResolverQuery } from './base'; +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 { + private readonly kqlQuery: JsonObject[] = []; + + constructor( + private readonly pagination: PaginationBuilder, + indexPattern: string | string[], + endpointID?: string, + kql?: string + ) { + super(indexPattern, endpointID); + if (kql) { + this.kqlQuery.push(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kql))); + } + } + + protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { + return { + query: { + bool: { + filter: [ + ...this.kqlQuery, + { + terms: { 'endgame.unique_pid': uniquePIDs }, + }, + { + term: { 'agent.id': endpointID }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + ...this.pagination.buildQueryFields('endgame.serial_event_id', 'desc'), + }; + } + + protected query(entityIDs: string[]): JsonObject { + return { + query: { + bool: { + filter: [ + ...this.kqlQuery, + { + terms: { 'process.entity_id': entityIDs }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + ...this.pagination.buildQueryFields('event.id', 'desc'), + }; + } + + formatResponse(response: SearchResponse): SafeResolverEvent[] { + return this.getResults(response); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts new file mode 100644 index 00000000000000..8fd9ab9a5ccd32 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/related_events.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * @deprecated use the `resolver/events` route and handler instead + */ +import { TypeOf } from '@kbn/config-schema'; +import { RequestHandler, Logger } from 'kibana/server'; +import { eventsIndexPattern, alertsIndexPattern } from '../../../../common/endpoint/constants'; +import { validateRelatedEvents } from '../../../../common/endpoint/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; + +export function handleRelatedEvents( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler< + TypeOf, + TypeOf, + TypeOf +> { + return async (context, req, res) => { + const { + params: { id }, + query: { events, afterEvent, legacyEndpointID: endpointID }, + body, + } = req; + try { + const client = context.core.elasticsearch.legacy.client; + + const fetcher = new Fetcher(client, id, eventsIndexPattern, alertsIndexPattern, endpointID); + + return res.ok({ + body: await fetcher.events(events, afterEvent, body?.filter), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: err }); + } + }; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts index 5c4d9a4741ad72..a5aa9b6c288c82 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts @@ -3,12 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +/** + * @deprecated msearch functionality for querying events will be removed shortly + */ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; import { SafeResolverRelatedEvents, SafeResolverEvent } from '../../../../../common/endpoint/types'; import { createRelatedEvents } from './node'; -import { EventsQuery } from '../queries/events'; +import { EventsQuery } from '../queries/related_events'; import { PaginationBuilder } from './pagination'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts index 535ee37f3db321..cecdc8a4789585 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/node.ts @@ -13,6 +13,7 @@ import { SafeResolverEvent, SafeResolverChildNode, SafeResolverRelatedEvents, + ResolverPaginatedEvents, } from '../../../../../common/endpoint/types'; /** @@ -30,6 +31,19 @@ export function createRelatedEvents( return { entityID, events, nextEvent }; } +/** + * Creates an object that the events handler would return + * + * @param events array of events + * @param nextEvent the cursor to retrieve the next event + */ +export function createEvents( + events: SafeResolverEvent[] = [], + nextEvent: string | null = null +): ResolverPaginatedEvents { + return { events, nextEvent }; +} + /** * Creates an alert object that the alerts handler would return * diff --git a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts index 3f01d7423eded1..6ef44e325b0a70 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/plugin.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/server'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + IClusterClient, +} from 'kibana/server'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { getClusterUuids, getLocalLicense } from '../../../../src/plugins/telemetry/server'; import { getStatsWithXpack } from './telemetry_collection'; @@ -14,11 +20,13 @@ interface TelemetryCollectionXpackDepsSetup { } export class TelemetryCollectionXpackPlugin implements Plugin { + private elasticsearchClient?: IClusterClient; constructor(initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, { telemetryCollectionManager }: TelemetryCollectionXpackDepsSetup) { telemetryCollectionManager.setCollection({ esCluster: core.elasticsearch.legacy.client, + esClientGetter: () => this.elasticsearchClient, title: 'local_xpack', priority: 1, statsGetter: getStatsWithXpack, @@ -27,5 +35,7 @@ export class TelemetryCollectionXpackPlugin implements Plugin { }); } - public start(core: CoreStart) {} + public start(core: CoreStart) { + this.elasticsearchClient = core.elasticsearch.client; + } } diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__tests__/get_xpack.js b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__tests__/get_xpack.js deleted file mode 100644 index eb03701fd195b5..00000000000000 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__tests__/get_xpack.js +++ /dev/null @@ -1,46 +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 expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { TIMEOUT } from '../constants'; -import { getXPackUsage } from '../get_xpack'; - -function mockGetXPackUsage(callCluster, usage, req) { - callCluster - .withArgs(req, 'transport.request', { - method: 'GET', - path: '/_xpack/usage', - query: { - master_timeout: TIMEOUT, - }, - }) - .returns(usage); - - callCluster - .withArgs('transport.request', { - method: 'GET', - path: '/_xpack/usage', - query: { - master_timeout: TIMEOUT, - }, - }) - .returns(usage); -} - -describe('get_xpack', () => { - describe('getXPackUsage', () => { - it('uses callCluster to get /_xpack/usage API', () => { - const response = Promise.resolve({}); - const callCluster = sinon.stub(); - - mockGetXPackUsage(callCluster, response); - - expect(getXPackUsage(callCluster)).to.be(response); - }); - }); -}); 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 24382fb89d3373..a4806cefeef3df 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { coreMock } from '../../../../../src/core/server/mocks'; +import { coreMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { getStatsWithXpack } from './get_stats_with_xpack'; const kibana = { @@ -55,32 +55,34 @@ const mockUsageCollection = (kibanaUsage = kibana) => ({ describe('Telemetry Collection: Get Aggregated Stats', () => { test('OSS-like telemetry (no license nor X-Pack telemetry)', async () => { - const callCluster = jest.fn(async (method: string, options: { path?: string }) => { - switch (method) { - case 'transport.request': - if (options.path === '/_license' || options.path === '/_xpack/usage') { - // eslint-disable-next-line no-throw-literal - throw { statusCode: 404 }; - } else if (options.path === '/_nodes/usage') { - return { - cluster_name: 'test cluster', - nodes: nodesUsage, - }; - } - return {}; - case 'info': - return { cluster_uuid: 'test', cluster_name: 'test', version: { number: '8.0.0' } }; - default: - return {}; + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // mock for xpack.usage should throw a 404 for this test + esClient.xpack.usage.mockRejectedValue(new Error('Not Found')); + // mock for license should throw a 404 for this test + esClient.license.get.mockRejectedValue(new Error('Not Found')); + // mock for nodes usage should resolve for this test + esClient.nodes.usage.mockResolvedValue( + // @ts-ignore we only care about the response body + { body: { cluster_name: 'test cluster', nodes: nodesUsage } } + ); + // mock for info should resolve for this test + esClient.info.mockResolvedValue( + // @ts-ignore we only care about the response body + { + body: { + cluster_uuid: 'test', + cluster_name: 'test', + version: { number: '8.0.0' }, + }, } - }); + ); const usageCollection = mockUsageCollection(); const context = getContext(); const stats = await getStatsWithXpack( [{ clusterUuid: '1234' }], { - callCluster, + esClient, usageCollection, } as any, context @@ -93,36 +95,39 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { }); test('X-Pack telemetry (license + X-Pack)', async () => { - const callCluster = jest.fn(async (method: string, options: { path?: string }) => { - switch (method) { - case 'transport.request': - if (options.path === '/_license') { - return { - license: { type: 'basic' }, - }; - } - if (options.path === '/_xpack/usage') { - return {}; - } - if (options.path === '/_nodes/usage') { - return { - cluster_name: 'test cluster', - nodes: nodesUsage, - }; - } - case 'info': - return { cluster_uuid: 'test', cluster_name: 'test', version: { number: '8.0.0' } }; - default: - return {}; + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // mock for license should return a basic license + esClient.license.get.mockResolvedValue( + // @ts-ignore we only care about the response body + { body: { license: { type: 'basic' } } } + ); + // mock for xpack usage should return an empty object + esClient.xpack.usage.mockResolvedValue( + // @ts-ignore we only care about the response body + { body: {} } + ); + // mock for nodes usage should return the cluster name and nodes usage + esClient.nodes.usage.mockResolvedValue( + // @ts-ignore we only care about the response body + { body: { cluster_name: 'test cluster', nodes: nodesUsage } } + ); + esClient.info.mockResolvedValue( + // @ts-ignore we only care about the response body + { + body: { + cluster_uuid: 'test', + cluster_name: 'test', + version: { number: '8.0.0' }, + }, } - }); + ); const usageCollection = mockUsageCollection(); const context = getContext(); const stats = await getStatsWithXpack( [{ clusterUuid: '1234' }], { - callCluster, + esClient, usageCollection, } as any, context diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts index 3fcd25c31e71e2..87e3d0a9613da1 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.ts @@ -17,9 +17,9 @@ export const getStatsWithXpack: StatsGetter<{}, TelemetryAggregatedStats> = asyn config, context ) { - const { callCluster } = config; + const { esClient } = config; const clustersLocalStats = await getLocalStats(clustersDetails, config, context); - const xpack = await getXPackUsage(callCluster).catch(() => undefined); // We want to still report something (and do not lose the license) even when this method fails. + const xpack = await getXPackUsage(esClient).catch(() => undefined); // We want to still report something (and do not lose the license) even when this method fails. return clustersLocalStats.map((localStats) => { if (xpack) { diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.test.ts new file mode 100644 index 00000000000000..106df71e46c7e1 --- /dev/null +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.test.ts @@ -0,0 +1,19 @@ +/* + * 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 { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { getXPackUsage } from './get_xpack'; + +describe('get_xpack', () => { + describe('getXPackUsage', () => { + it('uses esClient to get /_xpack/usage API', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + // @ts-ignore we only care about the response body + esClient.xpack.usage.mockResolvedValue({ body: {} }); + const result = await getXPackUsage(esClient); + expect(result).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts index 0ff75717c8b651..4dbf2052be28cd 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_xpack.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller } from 'kibana/server'; +import { ElasticsearchClient } from 'src/core/server'; import { TIMEOUT } from './constants'; /** @@ -14,12 +14,7 @@ import { TIMEOUT } from './constants'; * * Like any X-Pack related API, X-Pack must installed for this to work. */ -export function getXPackUsage(callCluster: LegacyAPICaller) { - return callCluster('transport.request', { - method: 'GET', - path: '/_xpack/usage', - query: { - master_timeout: TIMEOUT, - }, - }); +export async function getXPackUsage(esClient: ElasticsearchClient) { + const { body } = await esClient.xpack.usage({ master_timeout: TIMEOUT }); + return body; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 42e695788448fb..fd743400133a7d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10780,7 +10780,6 @@ "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "機能影響スコア", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "実験的", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", - "xpack.ml.dataframe.analytics.exploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "外れ値検出ジョブID {jobId}", "xpack.ml.dataframe.analytics.exploration.title": "分析の探索", "xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText": "予測があるドキュメントを示す", @@ -10796,7 +10795,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.generalizationFilterText": ".学習データをフィルタリングしています。", "xpack.ml.dataframe.analytics.regressionExploration.huberLinkText": "Pseudo Huber損失関数", "xpack.ml.dataframe.analytics.regressionExploration.huberText": "{wikiLink}", - "xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "回帰分析モデルの実行の効果を測定します。真値と予測値の間の差異の二乗平均合計。", "xpack.ml.dataframe.analytics.regressionExploration.msleText": "平均二乗対数誤差", @@ -15885,7 +15883,6 @@ "xpack.securitySolution.endpoint.resolver.panel.table.row.count": "カウント", "xpack.securitySolution.endpoint.resolver.panel.table.row.eventType": "イベントタイプ", "xpack.securitySolution.endpoint.resolver.panel.table.row.processNameTitle": "プロセス名", - "xpack.securitySolution.endpoint.resolver.panel.table.row.timestampInvalidLabel": "無効", "xpack.securitySolution.endpoint.resolver.panel.table.row.timestampTitle": "タイムスタンプ", "xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription": "値が見つかりません", "xpack.securitySolution.endpoint.resolver.relatedEventLimitExceeded": "{numberOfEventsMissing} {category}件のイベントを表示できませんでした。データの上限に達しました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 394acbf65d1b50..104fc70f5dd715 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10786,7 +10786,6 @@ "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "功能影响分数", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "实验性", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", - "xpack.ml.dataframe.analytics.exploration.indexError": "加载索引数据时出错。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "离群值检测作业 ID {jobId}", "xpack.ml.dataframe.analytics.exploration.title": "分析浏览", "xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText": "正在显示有相关预测存在的文档", @@ -10802,7 +10801,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.generalizationFilterText": ".筛留训练数据。", "xpack.ml.dataframe.analytics.regressionExploration.huberLinkText": "Pseudo Huber 损失函数", "xpack.ml.dataframe.analytics.regressionExploration.huberText": "{wikiLink}", - "xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "度量回归分析模型的表现。真实值与预测值之差的平均平方和。", "xpack.ml.dataframe.analytics.regressionExploration.msleText": "均方根对数误差", @@ -15895,7 +15893,6 @@ "xpack.securitySolution.endpoint.resolver.panel.table.row.count": "计数", "xpack.securitySolution.endpoint.resolver.panel.table.row.eventType": "事件类型", "xpack.securitySolution.endpoint.resolver.panel.table.row.processNameTitle": "进程名称", - "xpack.securitySolution.endpoint.resolver.panel.table.row.timestampInvalidLabel": "无效", "xpack.securitySolution.endpoint.resolver.panel.table.row.timestampTitle": "时间戳", "xpack.securitySolution.endpoint.resolver.panel.table.row.valueMissingDescription": "值缺失", "xpack.securitySolution.endpoint.resolver.relatedEventLimitExceeded": "{numberOfEventsMissing} 个{category}事件无法显示,因为已达到数据限制。", diff --git a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts index f7c8c1f6f85f5b..e738b50a2fe053 100644 --- a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts +++ b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts @@ -20,7 +20,8 @@ export default function ({ getService }: FtrProviderContext) { const jobConfigs = [createJobConfig('test_calendar_ad_1'), createJobConfig('test_calendar_ad_2')]; const newJobGroups = ['farequote']; - describe('calendar edit', function () { + // FLAKY: https://github.com/elastic/kibana/issues/78288 + describe.skip('calendar edit', function () { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts b/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts index 8692f79d5aea9b..a9a1b31072e7ee 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts @@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { filter(({ statusCode }) => statusCode === 500), map(({ message }) => message), first(), - timeout(15000) + timeout(120000) ); const reportFailed = await fails$.toPromise(); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts index 3f2b2e7116206e..b216f137e27b5d 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts @@ -33,7 +33,7 @@ export default function ({ getService }: FtrProviderContext) { tap(() => log.debug(`report at ${downloadPath} is done`)), map((response) => response.text), first(), - timeout(15000) + timeout(120000) ); }; @@ -105,8 +105,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/76551 - it.skip('should complete a job of PNG export of a dashboard in non-default space', async () => { + it('should complete a job of PNG export of a dashboard in non-default space', async () => { const downloadPath = await reportingAPI.postJobJSON( `/s/non_default_space/api/reporting/generate/png`, { @@ -119,8 +118,7 @@ export default function ({ getService }: FtrProviderContext) { expect(reportCompleted).to.not.be(null); }); - // FLAKY: https://github.com/elastic/kibana/issues/76551 - it.skip('should complete a job of PDF export of a dashboard in non-default space', async () => { + it('should complete a job of PDF export of a dashboard in non-default space', async () => { const downloadPath = await reportingAPI.postJobJSON( `/s/non_default_space/api/reporting/generate/printablePdf`, { diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts index 99a46684d8a67a..e2c6e170643736 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts @@ -21,7 +21,8 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const usageAPI = getService('usageAPI'); - describe('Usage', () => { + // FLAKY: https://github.com/elastic/kibana/issues/78494 + describe.skip('Usage', () => { before(async () => { await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); await esArchiver.load(OSS_DATA_ARCHIVE_PATH); @@ -132,8 +133,7 @@ export default function ({ getService }: FtrProviderContext) { reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 0); }); - // FLAKY: https://github.com/elastic/kibana/issues/76581 - it.skip('should handle preserve_layout pdf', async () => { + it('should handle preserve_layout pdf', async () => { await reportingAPI.expectAllJobsToFinishSuccessfully( await Promise.all([ reportingAPI.postJob(GenerationUrls.PDF_PRESERVE_DASHBOARD_FILTER_6_3), @@ -150,8 +150,7 @@ export default function ({ getService }: FtrProviderContext) { reportingAPI.expectRecentJobTypeTotalStats(usage, 'printable_pdf', 2); }); - // FLAKY: https://github.com/elastic/kibana/issues/76581 - it.skip('should handle print_layout pdf', async () => { + it('should handle print_layout pdf', async () => { await reportingAPI.expectAllJobsToFinishSuccessfully( await Promise.all([ reportingAPI.postJob(GenerationUrls.PDF_PRINT_DASHBOARD_6_3), diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index d661b3097bd354..92571e5c275665 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'header']); const testSubjects = getService('testSubjects'); + const browser = getService('browser'); return { /** @@ -88,6 +89,7 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr */ async confirmAndSave() { await this.ensureIsOnDetailsPage(); + await browser.scrollTop(); await (await this.findSaveButton()).click(); await testSubjects.existOrFail('policyDetailsConfirmModal'); await pageObjects.common.clickConfirmOnModal(); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts index 4e248f52ec2971..b57486ee55ca4d 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -5,7 +5,10 @@ */ import expect from '@kbn/expect'; import { eventIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; -import { SafeResolverRelatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; +import { + ResolverPaginatedEvents, + SafeResolverRelatedEvents, +} from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Tree, @@ -41,97 +44,221 @@ export default function ({ getService }: FtrProviderContext) { ancestryArraySize: 2, }; - describe('related events route', () => { - before(async () => { - await esArchiver.load('endpoint/resolver/api_feature'); - resolverTrees = await resolver.createTrees(treeOptions); - // we only requested a single alert so there's only 1 tree - tree = resolverTrees.trees[0]; - }); - after(async () => { - await resolver.deleteData(resolverTrees); - await esArchiver.unload('endpoint/resolver/api_feature'); - }); + describe('event routes', () => { + describe('related events route', () => { + before(async () => { + await esArchiver.load('endpoint/resolver/api_feature'); + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + }); + after(async () => { + await resolver.deleteData(resolverTrees); + await esArchiver.unload('endpoint/resolver/api_feature'); + }); - describe('legacy events', () => { - const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; - const entityID = '94042'; - const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; + describe('legacy events', () => { + const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; + const entityID = '94042'; + const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; - it('should return details for the root node', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events.length).to.eql(1); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); + it('should return details for the root node', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(1); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('returns no values when there is no more data', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + // after is set to the document id of the last event so there shouldn't be any more after it + .post( + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events).be.empty(); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post( + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` + ) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.entityID).to.eql(entityID); + expect(body.nextEvent).to.eql(null); + }); + + it('should return no results for an invalid endpoint ID', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.entityID).to.eql(entityID); + expect(body.events).to.be.empty(); + }); + + it('should error on invalid pagination values', async () => { + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=0`) + .set('kbn-xsrf', 'xxx') + .expect(400); + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=20000`) + .set('kbn-xsrf', 'xxx') + .expect(400); + await supertest + .post(`/api/endpoint/resolver/${entityID}/events?events=-1`) + .set('kbn-xsrf', 'xxx') + .expect(400); + }); }); - it('returns no values when there is no more data', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - // after is set to the document id of the last event so there shouldn't be any more after it - .post( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` - ) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.events).be.empty(); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); + describe('endpoint events', () => { + it('should not find any events', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/5555/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.nextEvent).to.eql(null); + expect(body.events).to.be.empty(); + }); + + it('should return details for the root node', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); + + it('should allow for the events to be filtered', async () => { + const filter = `event.category:"${RelatedEventCategory.Driver}"`; + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .send({ + filter, + }) + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.eql(null); + for (const event of body.events) { + expect(event.event?.category).to.be(RelatedEventCategory.Driver); + } + }); + + it('should return paginated results for the root node', async () => { + let { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).not.to.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.events.length).to.eql(2); + compareArrays(tree.origin.relatedEvents, body.events); + expect(body.nextEvent).to.not.eql(null); + + ({ body } = await supertest + .post( + `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` + ) + .set('kbn-xsrf', 'xxx') + .expect(200)); + expect(body.events).to.be.empty(); + expect(body.nextEvent).to.eql(null); + }); + + it('should return the first page of information when the cursor is invalid', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + compareArrays(tree.origin.relatedEvents, body.events, true); + expect(body.nextEvent).to.eql(null); + }); + + it('should sort the events in descending order', async () => { + const { body }: { body: SafeResolverRelatedEvents } = await supertest + .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + .set('kbn-xsrf', 'xxx') + .expect(200); + expect(body.events.length).to.eql(4); + // these events are created in the order they are defined in the array so the newest one is + // the last element in the array so let's reverse it + const relatedEvents = tree.origin.relatedEvents.reverse(); + for (let i = 0; i < body.events.length; i++) { + expect(body.events[i].event?.category).to.equal(relatedEvents[i].event?.category); + expect(eventIDSafeVersion(body.events[i])).to.equal(relatedEvents[i].event?.id); + } + }); }); + }); - it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post( - `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` - ) - .set('kbn-xsrf', 'xxx') - .expect(200); - expect(body.entityID).to.eql(entityID); - expect(body.nextEvent).to.eql(null); + describe('kql events route', () => { + let entityIDFilter: string | undefined; + before(async () => { + resolverTrees = await resolver.createTrees(treeOptions); + // we only requested a single alert so there's only 1 tree + tree = resolverTrees.trees[0]; + entityIDFilter = `process.entity_id:"${tree.origin.id}" and not event.category:"process"`; + }); + after(async () => { + await resolver.deleteData(resolverTrees); }); - it('should return no results for an invalid endpoint ID', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) + it('should filter events by event.id', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') + .send({ + filter: `event.id:"${tree.origin.relatedEvents[0]?.event?.id}"`, + }) .expect(200); + expect(body.events.length).to.eql(1); + expect(tree.origin.relatedEvents[0]?.event?.id).to.eql(body.events[0].event?.id); expect(body.nextEvent).to.eql(null); - expect(body.entityID).to.eql(entityID); - expect(body.events).to.be.empty(); }); - it('should error on invalid pagination values', async () => { - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=0`) - .set('kbn-xsrf', 'xxx') - .expect(400); - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=20000`) - .set('kbn-xsrf', 'xxx') - .expect(400); - await supertest - .post(`/api/endpoint/resolver/${entityID}/events?events=-1`) - .set('kbn-xsrf', 'xxx') - .expect(400); - }); - }); - - describe('endpoint events', () => { - it('should not find any events', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/5555/events`) + it('should not find any events when given an invalid entity id', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') + .send({ + filter: 'process.entity_id:"5555"', + }) .expect(200); expect(body.nextEvent).to.eql(null); expect(body.events).to.be.empty(); }); - it('should return details for the root node', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + it('should return related events for the root node', async () => { + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200); expect(body.events.length).to.eql(4); compareArrays(tree.origin.relatedEvents, body.events, true); @@ -139,9 +266,9 @@ export default function ({ getService }: FtrProviderContext) { }); it('should allow for the events to be filtered', async () => { - const filter = `event.category:"${RelatedEventCategory.Driver}"`; - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + const filter = `event.category:"${RelatedEventCategory.Driver}" and ${entityIDFilter}`; + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') .send({ filter, @@ -156,38 +283,46 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return paginated results for the root node', async () => { - let { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) + let { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events?limit=2`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200); expect(body.events.length).to.eql(2); compareArrays(tree.origin.relatedEvents, body.events); expect(body.nextEvent).not.to.eql(null); ({ body } = await supertest - .post( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) + .post(`/api/endpoint/resolver/events?limit=2&afterEvent=${body.nextEvent}`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200)); expect(body.events.length).to.eql(2); compareArrays(tree.origin.relatedEvents, body.events); expect(body.nextEvent).to.not.eql(null); ({ body } = await supertest - .post( - `/api/endpoint/resolver/${tree.origin.id}/events?events=2&afterEvent=${body.nextEvent}` - ) + .post(`/api/endpoint/resolver/events?limit=2&afterEvent=${body.nextEvent}`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200)); expect(body.events).to.be.empty(); expect(body.nextEvent).to.eql(null); }); it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events?afterEvent=blah`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200); expect(body.events.length).to.eql(4); compareArrays(tree.origin.relatedEvents, body.events, true); @@ -195,9 +330,12 @@ export default function ({ getService }: FtrProviderContext) { }); it('should sort the events in descending order', async () => { - const { body }: { body: SafeResolverRelatedEvents } = await supertest - .post(`/api/endpoint/resolver/${tree.origin.id}/events`) + const { body }: { body: ResolverPaginatedEvents } = await supertest + .post(`/api/endpoint/resolver/events`) .set('kbn-xsrf', 'xxx') + .send({ + filter: entityIDFilter, + }) .expect(200); expect(body.events.length).to.eql(4); // these events are created in the order they are defined in the array so the newest one is diff --git a/yarn.lock b/yarn.lock index 182eb90d5f7a46..7aa88bcc883487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1128,15 +1128,6 @@ enabled "2.0.x" kuler "^2.0.0" -"@elastic/apm-rum-core@^5.6.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.6.0.tgz#d1f643eb00e590d5884598a20bb54efb1490ee13" - integrity sha512-hG+lITWBQd0mw00BQ1zYVRKDCh5b9FKFiht9fMXcT0SENOsT5J37RIbQHPdVawluT7/mhDF07t4fR8V0xRB1/g== - dependencies: - error-stack-parser "^1.3.5" - opentracing "^0.14.3" - promise-polyfill "^8.1.3" - "@elastic/apm-rum-core@^5.6.1": version "5.6.1" resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.6.1.tgz#0870e654e84e1f2ffea7c8a247a2da1b72918bcd" @@ -1154,13 +1145,6 @@ "@elastic/apm-rum" "^5.6.0" hoist-non-react-statics "^3.3.0" -"@elastic/apm-rum@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.5.0.tgz#24a8b4db0fa328c1e54710d18837e1adba7e51e0" - integrity sha512-uEOJG7Lm0CLtGfXOLXSsiPLpTPvrNUqlWQEKf/D77lpHRVWxBb56xa4X4CK2on8V1XzHDufcYBPcBcKSGozTLw== - dependencies: - "@elastic/apm-rum-core" "^5.6.0" - "@elastic/apm-rum@^5.6.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.6.0.tgz#0af2acb55091b9eb315cf38c6422a83cddfecb6f" @@ -1215,13 +1199,13 @@ pump "^3.0.0" secure-json-parse "^2.1.0" -"@elastic/ems-client@7.9.3": - version "7.9.3" - resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.9.3.tgz#71b79914f76e347f050ead8474ad65d761e94a8a" - integrity sha512-aun5rW9TQgWLVH77xBLLhempT3P+6AeQIEyK/CWYuVfCDpHfDxzMKWgQ076a7rSUqF059ayDGZbyOxf7l0M2Sw== +"@elastic/ems-client@7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.10.0.tgz#6d0e12ce99acd122d8066aa0a8685ecfd21637d3" + integrity sha512-84XqAhY4iaKwo2PnDwskNLvnprR3EYcS1AhN048xa8mIZlRJuycB4DwWnB699qvUTQqKcg5qLS0o5sEUs2HDeA== dependencies: lodash "^4.17.15" - semver "^6.3.0" + semver "7.3.2" "@elastic/eslint-plugin-eui@0.0.2": version "0.0.2" @@ -25861,6 +25845,11 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== +semver@7.3.2, semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + semver@^5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" @@ -25881,11 +25870,6 @@ semver@^7.1.3: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.0.tgz#91f7c70ec944a63e5dc7a74cde2da375d8e0853c" integrity sha512-uyvgU/igkrMgNHwLgXvlpD9jEADbJhB0+JXSywoO47JgJ6c16iau9F9cjtc/E5o0PoqRYTiTIAPRKaYe84z6eQ== -semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== - semver@~5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"