From 7fa5c2face8211328417d7915dc05134e6fed805 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 16 Mar 2020 09:23:58 -0600 Subject: [PATCH 01/17] [skip-ci] Service Status RFC (#59621) --- rfcs/text/0010_service_status.md | 373 +++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 rfcs/text/0010_service_status.md diff --git a/rfcs/text/0010_service_status.md b/rfcs/text/0010_service_status.md new file mode 100644 index 00000000000000..ded594930a3677 --- /dev/null +++ b/rfcs/text/0010_service_status.md @@ -0,0 +1,373 @@ +- Start Date: 2020-03-07 +- RFC PR: https://github.com/elastic/kibana/pull/59621 +- Kibana Issue: https://github.com/elastic/kibana/issues/41983 + +# Summary + +A set API for describing the current status of a system (Core service or plugin) +in Kibana. + +# Basic example + +```ts +// Override default behavior and only elevate severity when elasticsearch is not available +core.status.set( + core.status.core$.pipe(core => core.elasticsearch); +) +``` + +# Motivation + +Kibana should do as much possible to help users keep their installation in a working state. This includes providing as much detail about components that are not working as well as ensuring that failures in one part of the application do not block using other portions of the application. + +In order to provide the user with as much detail as possible about any systems that are not working correctly, the status mechanism should provide excellent defaults in terms of expressing relationships between services and presenting detailed information to the user. + +# Detailed design + +## Failure Guidelines + +While this RFC primarily describes how status information is signaled from individual services and plugins to Core, it's first important to define how Core expects these services and plugins to behave in the face of failure more broadly. + +Core is designed to be resilient and adaptive to change. When at all possible, Kibana should automatically recover from failure, rather than requiring any kind of intervention by the user or administrator. + +Given this goal, Core expects the following from plugins: +- During initialization, `setup`, and `start` plugins should only throw an exception if a truly unrecoverable issue is encountered. Examples: HTTP port is unavailable, server does not have the appropriate file permissions. +- Temporary error conditions should always be retried automatically. A user should not have to restart Kibana in order to resolve a problem when avoidable. This means all initialization code should include error handling and automated retries. Examples: creating an Elasticsearch index, connecting to an external service. + - It's important to note that some issues do require manual intervention in _other services_ (eg. Elasticsearch). Kibana should still recover without restarting once that external issue is resolved. +- Unhandled promise rejections are not permitted. In the future, Node.js will crash on unhandled promise rejections. It is impossible for Core to be able to properly handle and retry these situations, so all services and plugins should handle all rejected promises and retry when necessary. +- Plugins should only crash the Kibana server when absolutely necessary. Some features are considered "mission-critical" to customers and may need to halt Kibana if they are not functioning correctly. Example: audit logging. + +## API Design + +### Types + +```ts +/** + * The current status of a service at a point in time. + * + * @typeParam Meta - JSON-serializable object. Plugins should export this type to allow other plugins to read the `meta` + * field in a type-safe way. + */ +type ServiceStatus = unknown> = { + /** + * The current availability level of the service. + */ + level: ServiceStatusLevel.available; + /** + * A high-level summary of the service status. + */ + summary?: string; + /** + * A more detailed description of the service status. + */ + detail?: string; + /** + * A URL to open in a new tab about how to resolve or troubleshoot the problem. + */ + documentationUrl?: string; + /** + * Any JSON-serializable data to be included in the HTTP API response. Useful for providing more fine-grained, + * machine-readable information about the service status. May include status information for underlying features. + */ + meta?: Meta; +} | { + level: ServiceStatusLevel; + summary: string; // required when level !== available + detail?: string; + documentationUrl?: string; + meta?: Meta; +} + +/** + * The current "level" of availability of a service. + */ +enum ServiceStatusLevel { + /** + * Everything is working! + */ + available, + /** + * Some features may not be working. + */ + degraded, + /** + * The service is unavailable, but other functions that do not depend on this service should work. + */ + unavailable, + /** + * Block all user functions and display the status page, reserved for Core services only. + * Note: In the real implementation, this will be split out to a different type. Kept as a single type here to make + * the RFC easier to follow. + */ + critical +} + +/** + * Status of core services. Only contains entries for backend services that could have a non-available `status`. + * For example, `context` cannot possibly be broken, so it is not included. + */ +interface CoreStatus { + elasticsearch: ServiceStatus; + http: ServiceStatus; + savedObjects: ServiceStatus; + uiSettings: ServiceStatus; + metrics: ServiceStatus; +} +``` + +### Plugin API + +```ts +/** + * The API exposed to plugins on CoreSetup.status + */ +interface StatusSetup { + /** + * Allows a plugin to specify a custom status dependent on its own criteria. + * Completely overrides the default inherited status. + */ + set(status$: Observable): void; + + /** + * Current status for all Core services. + */ + core$: Observable; + + /** + * Current status for all dependencies of the current plugin. + * Each key of the `Record` is a plugin id. + */ + plugins$: Observable>; + + /** + * The status of this plugin as derived from its dependencies. + * + * @remarks + * By default, plugins inherit this derived status from their dependencies. + * Calling {@link StatusSetup.set} overrides this default status. + */ + derivedStatus$: Observable; +} +``` + +### HTTP API + +The HTTP endpoint should return basic information about the Kibana node as well as the overall system status and the status of each individual system. + +This API does not need to include UI-specific details like the existing API such as `uiColor` and `icon`. + +```ts +/** + * Response type for the endpoint: GET /api/status + */ +interface StatusResponse { + /** server.name */ + name: string; + /** server.uuid */ + uuid: string; + /** Currently exposed by existing status API */ + version: { + number: string; + build_hash: string; + build_number: number; + build_snapshot: boolean; + }; + /** Similar format to existing API, but slightly different shape */ + status: { + /** See "Overall status calculation" section below */ + overall: ServiceStatus; + core: CoreStatus; + plugins: Record; + } +} +``` + +## Behaviors + +### Levels + +Each member of the `ServiceStatusLevel` enum has specific behaviors associated with it: +- **`available`**: + - All endpoints and apps associated with the service are accessible +- **`degraded`**: + - All endpoints and apps are available by default + - Some APIs may return `503 Unavailable` responses. This is not automatic, must be implemented directly by the service. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. +- **`unavailable`**: + - All endpoints (with some exceptions in Core) in Kibana return a `503 Unavailable` responses by default. This is automatic. + - When trying to access any app associated with the unavailable service, the user is presented with an error UI with detail about the outage. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. +- **`critical`**: + - All endpoints (with some exceptions in Core) in Kibana return a `503 Unavailable` response by default. This is automatic. + - All applications redirect to the system-wide status page with detail about which services are down and any relevant detail. This is automatic. + - Some plugin contract APIs may throw errors. This is not automatic, must be implemented directly by the service. + - This level is reserved for Core services only. + +### Overall status calculation + +The status level of the overall system is calculated to be the highest severity status of all core services and plugins. + +The `summary` property is calculated as follows: +- If the overall status level is `available`, the `summary` is `"Kibana is operating normally"` +- If a single core service or plugin is not `available`, the `summary` is `Kibana is ${level} due to ${serviceName}. See ${statusPageUrl} for more information.` +- If multiple core services or plugins are not `available`, the `summary` is `Kibana is ${level} due to multiple components. See ${statusPageUrl} for more information.` + +### Status inheritance + +By default, plugins inherit their status from all Core services and their dependencies on other plugins. + +This can be summarized by the following matrix: + +| core | required | optional | inherited | +|----------------|----------------|----------------|-------------| +| critical | _any_ | _any_ | critical | +| unavailable | <= unavailable | <= unavailable | unavailable | +| degraded | <= degraded | <= degraded | degraded | +| <= unavailable | unavailable | <= unavailable | unavailable | +| <= degraded | degraded | <= degraded | degraded | +| <= degraded | <= degraded | unavailable | degraded | +| <= degraded | <= degraded | degraded | degraded | +| available | available | available | available | + +If a plugin calls the `StatusSetup#set` API, the inherited status is completely overridden. They status the plugin specifies is the source of truth. If a plugin wishes to "merge" its custom status with the inherited status calculated by Core, it may do so by using the `StatusSetup#inherited$` property in its calculated status. + +If a plugin never calls the `StatusSetup#set` API, the plugin's status defaults to the inherited status. + +_Disabled_ plugins, that is plugins that are explicitly disabled in Kibana's configuration, do not have any status. They are not present in any status APIs and are **not** considered `unavailable`. Disabled plugins are excluded from the status inheritance calculation, even if a plugin has a optional dependency on a disabled plugin. In summary, if a plugin has an optional dependency on a disabled plugin, the plugin will not be considered `degraded` just because that optional dependency is disabled. + +### HTTP responses + +As specified in the [_Levels section_](#levels), a service's HTTP endpoints will respond with `503 Unavailable` responses in some status levels. + +In both the `critical` and `unavailable` levels, all of a service's endpoints will return 503s. However, in the `degraded` level, it is up to service authors to decide which endpoints should return a 503. This may be implemented directly in the route handler logic or by using any of the [utilities provided](#status-utilities). + +When a 503 is returned either via the default behavior or behavior implemented using the [provided utilities](#status-utilities), the HTTP response will include the following: +- `Retry-After` header, set to `60` seconds +- A body with mime type `application/json` containing the status of the service the HTTP route belongs to: + ```json5 + { + "error": "Unavailable", + // `ServiceStatus#summary` + "message": "Newsfeed API cannot be reached", + "attributes": { + "status": { + // Human readable form of `ServiceStatus#level` + "level": "critical", + // `ServiceStatus#summary` + "summary": "Newsfeed API cannot be reached", + // `ServiceStatus#detail` or null + "detail": null, + // `ServiceStatus#documentationUrl` or null + "documentationUrl": null, + // JSON-serialized from `ServiceStatus#meta` or null + "meta": {} + } + }, + "statusCode": 503 + } + ``` + +## Status Utilities + +Though many plugins should be able to rely on the default status inheritance and associated behaviors, there are common patterns and overrides that some plugins will need. The status service should provide some utilities for these common patterns out-of-the-box. + +```ts +/** + * Extension of the main Status API + */ +interface StatusSetup { + /** + * Helpers for expressing status in HTTP routes. + */ + http: { + /** + * High-order route handler function for wrapping routes with 503 logic based + * on a predicate. + * + * @remarks + * When a 503 is returned, it also includes detailed information from the service's + * current `ServiceStatus` including `meta` information. + * + * @example + * ```ts + * router.get( + * { path: '/my-api' } + * unavailableWhen( + * ServiceStatusLevel.degraded, + * async (context, req, res) => { + * return res.ok({ body: 'done' }); + * } + * ) + * ) + * ``` + * + * @param predicate When a level is specified, if the plugin's current status + * level is >= to the severity of the specified level, route + * returns a 503. When a function is specified, if that + * function returns `true`, a 503 is returned. + * @param handler The route handler to execute when a 503 is not returned. + * @param options.retryAfter Number of seconds to set the `Retry-After` + * header to when the endpoint is unavailable. + * Defaults to `60`. + */ + unavailableWhen( + predicate: ServiceStatusLevel | + (self: ServiceStatus, core: CoreStatus, plugins: Record) => boolean, + handler: RouteHandler, + options?: { retryAfter?: number } + ): RouteHandler; + } +} +``` + +## Additional Examples + +### Combine inherited status with check against external dependency +```ts +const getExternalDepHealth = async () => { + const resp = await window.fetch('https://myexternaldep.com/_healthz'); + return resp.json(); +} + +// Create an observable that checks the status of an external service every every 10s +const myExternalDependency$: Observable = interval(10000).pipe( + mergeMap(() => of(getExternalDepHealth())), + map(health => health.ok ? ServiceStatusLevel.available : ServiceStatusLevel.unavailable), + catchError(() => of(ServiceStatusLevel.unavailable)) +); + +// Merge the inherited status with the external check +core.status.set( + combineLatest( + core.status.inherited$, + myExternalDependency$ + ).pipe( + map(([inherited, external]) => ({ + level: Math.max(inherited.level, external) + })) + ) +); +``` + +# Drawbacks + +1. **The default behaviors and inheritance of statuses may appear to be "magic" to developers who do not read the documentation about how this works.** Compared to the legacy status mechanism, these defaults are much more opinionated and the resulting status is less explicit in plugin code compared to the legacy `mirrorPluginStatus` mechanism. +2. **The default behaviors and inheritance may not fit real-world status very well.** If many plugins must customize their status in order to opt-out of the defaults, this would be a step backwards from the legacy mechanism. + +# Alternatives + +We could somewhat reduce the complexity of the status inheritance by leveraging the dependencies between plugins to enable and disable plugins based on whether or not their upstream dependencies are available. This may simplify plugin code but would greatly complicate how Kibana fundamentally operates, requiring that plugins may get stopped and started multiple times within a single Kibana server process. We would be trading simplicity in one area for complexity in another. + +# Adoption strategy + +By default, most plugins would not need to do much at all. Today, very few plugins leverage the legacy status system. The majority of ones that do, simply call the `mirrorPluginStatus` utility to follow the status of the legacy elasticsearch plugin. + +Plugins that wish to expose more detail about their availability will easily be able to do so, including providing detailed information such as links to documentation to resolve the problem. + +# How we teach this + +This largely follows the same patterns we have used for other Core APIs: Observables, composable utilties, etc. + +This should be taught using the same channels we've leveraged for other Kibana Platform APIs: API documentation, additions to the [Migration Guide](../../src/core/MIGRATION.md) and [Migration Examples](../../src/core/MIGRATION_EXMAPLES.md). + +# Unresolved questions From 6a648658ce5d331bc781da707a5e66e8899e98ee Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Mon, 16 Mar 2020 16:58:17 +0100 Subject: [PATCH 02/17] Bump acorn sub-dependency to version ^7.1.1 (#60239) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5b13c8bd37aed8..ae55508cee886f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5921,9 +5921,9 @@ acorn@^6.2.1: integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== acorn@^7.0.0, acorn@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" + integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== address@1.1.0: version "1.1.0" From 3a4683ce70cbcae7a0c382c55c33b7d43a4db426 Mon Sep 17 00:00:00 2001 From: Chris Queen Date: Mon, 16 Mar 2020 12:37:51 -0400 Subject: [PATCH 03/17] Passing in a null value as a param to compareFilters no longer throws an exception (#59609) * The 'filter' parameter passed into the mapFilter function is now checked for nullish-ness via optional chaining. * Added tests in compare_filters.test.ts for when compareFilters accepts a null value for it's 'filter' parameter * Removed recently added optional chaining to the filters param in the mapFilter function. Instead, the compareFilters function now performs a null check on both filter params, and returns false if either are null * Updated null check in compareFilters function to use negation null check instead of checking strictly for a null value * fix tests types Co-authored-by: Elastic Machine Co-authored-by: Liza K --- .../filter_manager/lib/compare_filters.test.ts | 16 ++++++++++++++++ .../query/filter_manager/lib/compare_filters.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts index 5d6c25b0d96c1d..da8f5b3564948e 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.test.ts @@ -48,6 +48,22 @@ describe('filter manager utilities', () => { expect(compareFilters(f1, f2)).toBeTruthy(); }); + test('should compare filters, where one filter is null', () => { + const f1 = buildQueryFilter( + { _type: { match: { query: 'apache', type: 'phrase' } } }, + 'index', + '' + ); + const f2 = null; + expect(compareFilters(f1, f2 as any)).toBeFalsy(); + }); + + test('should compare a null filter with an empty filter', () => { + const f1 = null; + const f2 = buildEmptyFilter(true); + expect(compareFilters(f1 as any, f2)).toBeFalsy(); + }); + test('should compare duplicates, ignoring meta attributes', () => { const f1 = buildQueryFilter( { _type: { match: { query: 'apache', type: 'phrase' } } }, diff --git a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts index b4402885bc0beb..a2105fdc1d3efb 100644 --- a/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts +++ b/src/plugins/data/public/query/filter_manager/lib/compare_filters.ts @@ -74,6 +74,8 @@ export const compareFilters = ( second: Filter | Filter[], comparatorOptions: FilterCompareOptions = {} ) => { + if (!first || !second) return false; + let comparators: FilterCompareOptions = {}; const excludedAttributes: string[] = ['$$hashKey', 'meta']; From 8a578960c05f04879963927d4f80075736e73e8a Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Mon, 16 Mar 2020 17:28:07 +0000 Subject: [PATCH 04/17] [ML] Use real datafeed ID for datafeed preview (#60275) --- .../jobs_list/components/job_details/datafeed_preview_tab.js | 5 +++-- x-pack/plugins/ml/public/application/services/job_service.js | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js index 7a98ec5e5ce4a7..216c416f30a6b8 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js @@ -57,7 +57,8 @@ export class DatafeedPreviewPane extends Component { } componentDidMount() { - const canPreviewDatafeed = checkPermission('canPreviewDatafeed'); + const canPreviewDatafeed = + checkPermission('canPreviewDatafeed') && this.props.job.datafeed_config !== undefined; this.setState({ canPreviewDatafeed }); updateDatafeedPreview(this.props.job, canPreviewDatafeed) @@ -87,7 +88,7 @@ function updateDatafeedPreview(job, canPreviewDatafeed) { return new Promise((resolve, reject) => { if (canPreviewDatafeed) { mlJobService - .getDatafeedPreview(job.job_id) + .getDatafeedPreview(job.datafeed_config.datafeed_id) .then(resp => { if (Array.isArray(resp)) { resolve(JSON.stringify(resp.slice(0, ML_DATA_PREVIEW_COUNT), null, 2)); diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index fe3663d6a3ddb1..f092e85bef5cea 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -747,8 +747,7 @@ class JobService { return datafeedId; } - getDatafeedPreview(jobId) { - const datafeedId = this.getDatafeedId(jobId); + getDatafeedPreview(datafeedId) { return ml.datafeedPreview({ datafeedId }); } From dfff4fd6fa8a8063c2d30a8ebb16228da702f12d Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 16 Mar 2020 12:18:27 -0600 Subject: [PATCH 05/17] [SIEM][Detection Engine] Refactors signal rule alert type into smaller code by creating functions Refactors signal rule alert type into a smaller executor ## Summary * Breaks out the schema into its own file and function * Breaks out the action group into its own file and function * Moves misc types being added to this into the `./types` file * Breaks out all the writing of errors and success into their own functions * Uses destructuring to pull data out of some of the data types * Tweaks the gap detection to accept a date instead of moment to ease "ergonomics" * Updates unit tests for the gap detection ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../get_current_status_saved_object.ts | 56 ++++ .../signals/get_rule_status_saved_objects.ts | 31 ++ .../signals/siem_rule_action_groups.ts | 16 + .../signals/signal_params_schema.ts | 40 +++ .../signals/signal_rule_alert_type.ts | 300 +++++------------- .../lib/detection_engine/signals/types.ts | 12 + .../detection_engine/signals/utils.test.ts | 48 ++- .../lib/detection_engine/signals/utils.ts | 2 +- .../signals/write_current_status_succeeded.ts | 30 ++ .../write_gap_error_to_saved_object.ts | 61 ++++ ...e_signal_rule_exception_to_saved_object.ts | 58 ++++ 11 files changed, 416 insertions(+), 238 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts new file mode 100644 index 00000000000000..e5057b6b689972 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsFindResponse, SavedObject } from 'src/core/server'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; + +interface CurrentStatusSavedObjectParams { + alertId: string; + services: AlertServices; + ruleStatusSavedObjects: SavedObjectsFindResponse; +} + +export const getCurrentStatusSavedObject = async ({ + alertId, + services, + ruleStatusSavedObjects, +}: CurrentStatusSavedObjectParams): Promise> => { + if (ruleStatusSavedObjects.saved_objects.length === 0) { + // create + const date = new Date().toISOString(); + const currentStatusSavedObject = await services.savedObjectsClient.create< + IRuleSavedAttributesSavedObjectAttributes + >(ruleStatusSavedObjectType, { + alertId, // do a search for this id. + statusDate: date, + status: 'going to run', + lastFailureAt: null, + lastSuccessAt: null, + lastFailureMessage: null, + lastSuccessMessage: null, + }); + return currentStatusSavedObject; + } else { + // update 0th to executing. + const currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0]; + const sDate = new Date().toISOString(); + currentStatusSavedObject.attributes.status = 'going to run'; + currentStatusSavedObject.attributes.statusDate = sDate; + await services.savedObjectsClient.update( + ruleStatusSavedObjectType, + currentStatusSavedObject.id, + { + ...currentStatusSavedObject.attributes, + } + ); + return currentStatusSavedObject; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts new file mode 100644 index 00000000000000..5a59d0413cfb9d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsFindResponse } from 'kibana/server'; +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; + +interface GetRuleStatusSavedObject { + alertId: string; + services: AlertServices; +} + +export const getRuleStatusSavedObjects = async ({ + alertId, + services, +}: GetRuleStatusSavedObject): Promise> => { + return services.savedObjectsClient.find({ + type: ruleStatusSavedObjectType, + perPage: 6, // 0th element is current status, 1-5 is last 5 failures. + sortField: 'statusDate', + sortOrder: 'desc', + search: `${alertId}`, + searchFields: ['alertId'], + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts new file mode 100644 index 00000000000000..50c63df14996b3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/siem_rule_action_groups.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const siemRuleActionGroups = [ + { + id: 'default', + name: i18n.translate('xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default', { + defaultMessage: 'Default', + }), + }, +]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts new file mode 100644 index 00000000000000..d1726f93108c78 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; + +/** + * This is the schema for the Alert Rule that represents the SIEM alert for signals + * that index into the .siem-signals-${space-id} + */ +export const signalParamsSchema = () => + schema.object({ + description: schema.string(), + note: schema.nullable(schema.string()), + falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), + from: schema.string(), + ruleId: schema.string(), + immutable: schema.boolean({ defaultValue: false }), + index: schema.nullable(schema.arrayOf(schema.string())), + language: schema.nullable(schema.string()), + outputIndex: schema.nullable(schema.string()), + savedId: schema.nullable(schema.string()), + timelineId: schema.nullable(schema.string()), + timelineTitle: schema.nullable(schema.string()), + meta: schema.nullable(schema.object({}, { allowUnknowns: true })), + query: schema.nullable(schema.string()), + filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), + maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), + riskScore: schema.number(), + severity: schema.string(), + threat: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), + to: schema.string(), + type: schema.string(), + references: schema.arrayOf(schema.string(), { defaultValue: [] }), + version: schema.number({ defaultValue: 1 }), + }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index b467dfdaff305e..e3ea121a9ebb11 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -4,35 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema } from '@kbn/config-schema'; import { Logger } from 'src/core/server'; -import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import { - SIGNALS_ID, - DEFAULT_MAX_SIGNALS, - DEFAULT_SEARCH_AFTER_PAGE_SIZE, -} from '../../../../common/constants'; +import { SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; import { buildEventsSearchQuery } from './build_events_query'; import { getInputIndex } from './get_input_output_index'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { getFilter } from './get_filter'; -import { SignalRuleAlertTypeDefinition } from './types'; +import { SignalRuleAlertTypeDefinition, AlertAttributes } from './types'; import { getGapBetweenRuns } from './utils'; -import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; -interface AlertAttributes { - enabled: boolean; - name: string; - tags: string[]; - createdBy: string; - createdAt: string; - updatedBy: string; - schedule: { - interval: string; - }; -} +import { writeSignalRuleExceptionToSavedObject } from './write_signal_rule_exception_to_saved_object'; +import { signalParamsSchema } from './signal_params_schema'; +import { siemRuleActionGroups } from './siem_rule_action_groups'; +import { writeGapErrorToSavedObject } from './write_gap_error_to_saved_object'; +import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; +import { getCurrentStatusSavedObject } from './get_current_status_saved_object'; +import { writeCurrentStatusSucceeded } from './write_current_status_succeeded'; + export const signalRulesAlertType = ({ logger, version, @@ -43,43 +31,11 @@ export const signalRulesAlertType = ({ return { id: SIGNALS_ID, name: 'SIEM Signals', - actionGroups: [ - { - id: 'default', - name: i18n.translate('xpack.siem.detectionEngine.signalRuleAlert.actionGroups.default', { - defaultMessage: 'Default', - }), - }, - ], + actionGroups: siemRuleActionGroups, defaultActionGroupId: 'default', validate: { - params: schema.object({ - description: schema.string(), - note: schema.nullable(schema.string()), - falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), - from: schema.string(), - ruleId: schema.string(), - immutable: schema.boolean({ defaultValue: false }), - index: schema.nullable(schema.arrayOf(schema.string())), - language: schema.nullable(schema.string()), - outputIndex: schema.nullable(schema.string()), - savedId: schema.nullable(schema.string()), - timelineId: schema.nullable(schema.string()), - timelineTitle: schema.nullable(schema.string()), - meta: schema.nullable(schema.object({}, { allowUnknowns: true })), - query: schema.nullable(schema.string()), - filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), - riskScore: schema.number(), - severity: schema.string(), - threat: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), - to: schema.string(), - type: schema.string(), - references: schema.arrayOf(schema.string(), { defaultValue: [] }), - version: schema.number({ defaultValue: 1 }), - }), + params: signalParamsSchema(), }, - // fun fact: previousStartedAt is not actually a Date but a String of a date async executor({ previousStartedAt, alertId, services, params }) { const { from, @@ -93,89 +49,43 @@ export const signalRulesAlertType = ({ to, type, } = params; - // TODO: Remove this hard extraction of name once this is fixed: https://github.com/elastic/kibana/issues/50522 const savedObject = await services.savedObjectsClient.get('alert', alertId); - const ruleStatusSavedObjects = await services.savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, - perPage: 6, // 0th element is current status, 1-5 is last 5 failures. - sortField: 'statusDate', - sortOrder: 'desc', - search: `${alertId}`, - searchFields: ['alertId'], + + const ruleStatusSavedObjects = await getRuleStatusSavedObjects({ + alertId, + services, }); - let currentStatusSavedObject; - if (ruleStatusSavedObjects.saved_objects.length === 0) { - // create - const date = new Date().toISOString(); - currentStatusSavedObject = await services.savedObjectsClient.create< - IRuleSavedAttributesSavedObjectAttributes - >(ruleStatusSavedObjectType, { - alertId, // do a search for this id. - statusDate: date, - status: 'going to run', - lastFailureAt: null, - lastSuccessAt: null, - lastFailureMessage: null, - lastSuccessMessage: null, - }); - } else { - // update 0th to executing. - currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0]; - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'going to run'; - currentStatusSavedObject.attributes.statusDate = sDate; - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - } - const name = savedObject.attributes.name; - const tags = savedObject.attributes.tags; + const currentStatusSavedObject = await getCurrentStatusSavedObject({ + alertId, + services, + ruleStatusSavedObjects, + }); + + const { + name, + tags, + createdAt, + createdBy, + updatedBy, + enabled, + schedule: { interval }, + } = savedObject.attributes; - const createdBy = savedObject.attributes.createdBy; - const createdAt = savedObject.attributes.createdAt; - const updatedBy = savedObject.attributes.updatedBy; const updatedAt = savedObject.updated_at ?? ''; - const interval = savedObject.attributes.schedule.interval; - const enabled = savedObject.attributes.enabled; - const gap = getGapBetweenRuns({ - previousStartedAt: previousStartedAt != null ? moment(previousStartedAt) : null, // TODO: Remove this once previousStartedAt is no longer a string - interval, - from, - to, - }); - if (gap != null && gap.asMilliseconds() > 0) { - logger.warn( - `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.` - ); - // write a failure status whenever we have a time gap - // this is a temporary solution until general activity - // monitoring is developed as a feature - const gapDate = new Date().toISOString(); - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - alertId, - statusDate: gapDate, - status: 'failed', - lastFailureAt: gapDate, - lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt, - lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`, - lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage, - }); - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } - } + const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); + + await writeGapErrorToSavedObject({ + alertId, + logger, + ruleId: ruleId ?? '(unknown rule id)', + currentStatusSavedObject, + services, + gap, + ruleStatusSavedObjects, + name, + }); // set searchAfter page size to be the lesser of default page size or maxSignals. const searchAfterSize = DEFAULT_SEARCH_AFTER_PAGE_SIZE <= params.maxSignals @@ -243,107 +153,45 @@ export const signalRulesAlertType = ({ logger.debug( `Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'succeeded'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastSuccessAt = sDate; - currentStatusSavedObject.attributes.lastSuccessMessage = 'succeeded'; - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); + await writeCurrentStatusSucceeded({ + services, + currentStatusSavedObject, + }); } else { - logger.error( - `Error processing signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` - ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'failed'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastFailureAt = sDate; - currentStatusSavedObject.attributes.lastFailureMessage = `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`; - // current status is failing - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - // create new status for historical purposes - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - ...currentStatusSavedObject.attributes, + await writeSignalRuleExceptionToSavedObject({ + name, + alertId, + currentStatusSavedObject, + logger, + message: `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`, + services, + ruleStatusSavedObjects, + ruleId: ruleId ?? '(unknown rule id)', }); - - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } } } catch (err) { - logger.error( - `Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", ${err.message}` - ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'failed'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastFailureAt = sDate; - currentStatusSavedObject.attributes.lastFailureMessage = err.message; - // current status is failing - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - // create new status for historical purposes - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - ...currentStatusSavedObject.attributes, + await writeSignalRuleExceptionToSavedObject({ + name, + alertId, + currentStatusSavedObject, + logger, + message: err?.message ?? '(no error message given)', + services, + ruleStatusSavedObjects, + ruleId: ruleId ?? '(unknown rule id)', }); - - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } } } catch (exception) { - logger.error( - `Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" message: ${exception.message}` - ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'failed'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastFailureAt = sDate; - currentStatusSavedObject.attributes.lastFailureMessage = exception.message; - // current status is failing - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - // create new status for historical purposes - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - ...currentStatusSavedObject.attributes, + await writeSignalRuleExceptionToSavedObject({ + name, + alertId, + currentStatusSavedObject, + logger, + message: exception?.message ?? '(no error message given)', + services, + ruleStatusSavedObjects, + ruleId: ruleId ?? '(unknown rule id)', }); - - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } } }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 74425451173102..eaed3f2ead3a50 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -145,3 +145,15 @@ export interface SignalHit { event: object; signal: Partial; } + +export interface AlertAttributes { + enabled: boolean; + name: string; + tags: string[]; + createdBy: string; + createdAt: string; + updatedBy: string; + schedule: { + interval: string; + }; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts index bf25ab8bfd7ea0..873e06fcbb44ef 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.test.ts @@ -179,7 +179,10 @@ describe('utils', () => { describe('getGapBetweenRuns', () => { test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(5, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), interval: '5m', from: 'now-5m', to: 'now', @@ -191,7 +194,10 @@ describe('utils', () => { test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(5, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -203,7 +209,10 @@ describe('utils', () => { test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(5, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(5, 'minutes') + .toDate(), interval: '5m', from: 'now-10m', to: 'now', @@ -215,7 +224,10 @@ describe('utils', () => { test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(10, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(10, 'minutes') + .toDate(), interval: '10m', from: 'now-11m', to: 'now', @@ -230,7 +242,8 @@ describe('utils', () => { previousStartedAt: nowDate .clone() .subtract(5, 'minutes') - .subtract(30, 'seconds'), + .subtract(30, 'seconds') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -242,7 +255,10 @@ describe('utils', () => { test('it returns an exact 0 gap when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is one minute late', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(6, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(6, 'minutes') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -257,7 +273,8 @@ describe('utils', () => { previousStartedAt: nowDate .clone() .subtract(6, 'minutes') - .subtract(30, 'seconds'), + .subtract(30, 'seconds') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -269,7 +286,10 @@ describe('utils', () => { test('it returns a gap of 1 minute when the from overlaps with now by 1 minute, the interval is 5 minutes but the previous started is two minutes late', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(7, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), interval: '5m', from: 'now-6m', to: 'now', @@ -292,7 +312,7 @@ describe('utils', () => { test('it returns null if the interval is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone(), + previousStartedAt: nowDate.clone().toDate(), interval: 'invalid', // if not set to "x" where x is an interval such as 6m from: 'now-5m', to: 'now', @@ -303,7 +323,10 @@ describe('utils', () => { test('it returns the expected result when "from" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(7, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), interval: '5m', from: 'invalid', to: 'now', @@ -315,7 +338,10 @@ describe('utils', () => { test('it returns the expected result when "to" is an invalid string such as "invalid"', () => { const gap = getGapBetweenRuns({ - previousStartedAt: nowDate.clone().subtract(7, 'minutes'), + previousStartedAt: nowDate + .clone() + .subtract(7, 'minutes') + .toDate(), interval: '5m', from: 'now-6m', to: 'invalid', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts index 016aed9fabcd6b..8e7fb9c38d6583 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/utils.ts @@ -68,7 +68,7 @@ export const getGapBetweenRuns = ({ to, now = moment(), }: { - previousStartedAt: moment.Moment | undefined | null; + previousStartedAt: Date | undefined | null; interval: string; from: string; to: string; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts new file mode 100644 index 00000000000000..6b06235b29063c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts @@ -0,0 +1,30 @@ +/* + * 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 { SavedObject } from 'src/core/server'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; + +interface GetRuleStatusSavedObject { + services: AlertServices; + currentStatusSavedObject: SavedObject; +} + +export const writeCurrentStatusSucceeded = async ({ + services, + currentStatusSavedObject, +}: GetRuleStatusSavedObject): Promise => { + const sDate = new Date().toISOString(); + currentStatusSavedObject.attributes.status = 'succeeded'; + currentStatusSavedObject.attributes.statusDate = sDate; + currentStatusSavedObject.attributes.lastSuccessAt = sDate; + currentStatusSavedObject.attributes.lastSuccessMessage = 'succeeded'; + await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, { + ...currentStatusSavedObject.attributes, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts new file mode 100644 index 00000000000000..3650548c80ad5f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { Logger, SavedObject, SavedObjectsFindResponse } from 'src/core/server'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; + +interface WriteGapErrorToSavedObjectParams { + logger: Logger; + alertId: string; + ruleId: string; + currentStatusSavedObject: SavedObject; + ruleStatusSavedObjects: SavedObjectsFindResponse; + services: AlertServices; + gap: moment.Duration | null | undefined; + name: string; +} + +export const writeGapErrorToSavedObject = async ({ + alertId, + currentStatusSavedObject, + logger, + services, + ruleStatusSavedObjects, + ruleId, + gap, + name, +}: WriteGapErrorToSavedObjectParams): Promise => { + if (gap != null && gap.asMilliseconds() > 0) { + logger.warn( + `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.` + ); + // write a failure status whenever we have a time gap + // this is a temporary solution until general activity + // monitoring is developed as a feature + const gapDate = new Date().toISOString(); + await services.savedObjectsClient.create(ruleStatusSavedObjectType, { + alertId, + statusDate: gapDate, + status: 'failed', + lastFailureAt: gapDate, + lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt, + lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`, + lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage, + }); + + if (ruleStatusSavedObjects.saved_objects.length >= 6) { + // delete fifth status and prepare to insert a newer one. + const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); + await toDelete.forEach(async item => + services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) + ); + } + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts new file mode 100644 index 00000000000000..5ca0808902a524 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts @@ -0,0 +1,58 @@ +/* + * 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 { Logger, SavedObject, SavedObjectsFindResponse } from 'src/core/server'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; + +interface SignalRuleExceptionParams { + logger: Logger; + alertId: string; + ruleId: string; + currentStatusSavedObject: SavedObject; + ruleStatusSavedObjects: SavedObjectsFindResponse; + message: string; + services: AlertServices; + name: string; +} + +export const writeSignalRuleExceptionToSavedObject = async ({ + alertId, + currentStatusSavedObject, + logger, + message, + services, + ruleStatusSavedObjects, + ruleId, + name, +}: SignalRuleExceptionParams): Promise => { + logger.error( + `Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" message: ${message}` + ); + const sDate = new Date().toISOString(); + currentStatusSavedObject.attributes.status = 'failed'; + currentStatusSavedObject.attributes.statusDate = sDate; + currentStatusSavedObject.attributes.lastFailureAt = sDate; + currentStatusSavedObject.attributes.lastFailureMessage = message; + // current status is failing + await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, { + ...currentStatusSavedObject.attributes, + }); + // create new status for historical purposes + await services.savedObjectsClient.create(ruleStatusSavedObjectType, { + ...currentStatusSavedObject.attributes, + }); + + if (ruleStatusSavedObjects.saved_objects.length >= 6) { + // delete fifth status and prepare to insert a newer one. + const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); + await toDelete.forEach(async item => + services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) + ); + } +}; From 6cd888f75fefc6d3ec4405f7b937069e5ea9bf75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 16 Mar 2020 19:47:56 +0100 Subject: [PATCH 06/17] [Logs UI] Fix log rate table row expansion (#60096) This fixes the log rate table row expansion button, which broke in #54586 during a refactoring. --- .../sections/anomalies/table.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index 6eaa5de9000800..5cb5f3a993d48d 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo, useState } from 'react'; @@ -20,8 +20,8 @@ import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { AnomaliesTableExpandedRow } from './expanded_row'; interface TableItem { - id: string; - partition: string; + partitionName: string; + partitionId: string; topAnomalyScore: number; } @@ -55,11 +55,10 @@ export const AnomaliesTable: React.FunctionComponent<{ const tableItems: TableItem[] = useMemo(() => { return Object.entries(results.partitionBuckets).map(([key, value]) => { return { - // Note: EUI's table expanded rows won't work with a key of '' in itemIdToExpandedRowMap, so we have to use the friendly name here - id: getFriendlyNameForPartitionId(key), // The real ID partitionId: key, - partition: getFriendlyNameForPartitionId(key), + // Note: EUI's table expanded rows won't work with a key of '' in itemIdToExpandedRowMap, so we have to use the friendly name here + partitionName: getFriendlyNameForPartitionId(key), topAnomalyScore: formatAnomalyScore(value.topAnomalyScore), }; }); @@ -91,8 +90,8 @@ export const AnomaliesTable: React.FunctionComponent<{ const sortedTableItems = useMemo(() => { let sortedItems: TableItem[] = []; - if (sorting.sort.field === 'partition') { - sortedItems = tableItems.sort((a, b) => (a.partition > b.partition ? 1 : -1)); + if (sorting.sort.field === 'partitionName') { + sortedItems = tableItems.sort((a, b) => (a.partitionId > b.partitionId ? 1 : -1)); } else if (sorting.sort.field === 'topAnomalyScore') { sortedItems = tableItems.sort((a, b) => a.topAnomalyScore - b.topAnomalyScore); } @@ -100,10 +99,10 @@ export const AnomaliesTable: React.FunctionComponent<{ }, [tableItems, sorting]); const expandItem = useCallback( - item => { + (item: TableItem) => { const newItemIdToExpandedRowMap = { ...itemIdToExpandedRowMap, - [item.id]: ( + [item.partitionName]: ( { - if (itemIdToExpandedRowMap[item.id]) { - const { [item.id]: toggledItem, ...remainingExpandedRowMap } = itemIdToExpandedRowMap; + (item: TableItem) => { + if (itemIdToExpandedRowMap[item.partitionName]) { + const { + [item.partitionName]: toggledItem, + ...remainingExpandedRowMap + } = itemIdToExpandedRowMap; setItemIdToExpandedRowMap(remainingExpandedRowMap); } }, [itemIdToExpandedRowMap] ); - const columns = [ + const columns: Array> = [ { - field: 'partition', + field: 'partitionName', name: partitionColumnName, sortable: true, truncateText: true, @@ -149,8 +151,8 @@ export const AnomaliesTable: React.FunctionComponent<{ isExpander: true, render: (item: TableItem) => ( @@ -161,7 +163,7 @@ export const AnomaliesTable: React.FunctionComponent<{ return ( Date: Mon, 16 Mar 2020 13:33:40 -0600 Subject: [PATCH 07/17] [Maps] add draw control to create distance filter (#58163) * [Maps] add distance filter to draw controls * create distance filter * update jest snapshot * remove duplicated code * reset circle draw when user hits escape * i18n cleanup * ts MultiIndexGeoFieldSelect * ts DistanceFilterForm * remove unused prop * make interface a type * move geo_field_with_index to components folder * convert draw_circle to TS Co-authored-by: Elastic Machine --- .../geometry_filter_form.test.js.snap | 248 ++++++------------ .../components/distance_filter_form.tsx | 99 +++++++ .../public/components/geo_field_with_index.ts | 17 ++ .../public/components/geometry_filter_form.js | 95 ++----- .../multi_index_geo_field_select.tsx | 78 ++++++ .../map/mb/draw_control/draw_circle.ts | 137 ++++++++++ .../map/mb/draw_control/draw_control.js | 51 ++-- .../map/mb/draw_control/draw_tooltip.js | 26 +- .../__snapshots__/tools_control.test.js.snap | 46 ++++ .../tools_control/tools_control.js | 68 ++++- .../maps/public/elasticsearch_geo_utils.js | 33 +++ x-pack/package.json | 1 + x-pack/plugins/maps/common/constants.ts | 1 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - yarn.lock | 16 ++ 16 files changed, 636 insertions(+), 282 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/public/components/distance_filter_form.tsx create mode 100644 x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts create mode 100644 x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx create mode 100644 x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index c62b07a89e7a3e..85a073c8d9aced 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -17,49 +17,27 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> @@ -95,49 +73,27 @@ exports[`should not show "within" relation when filter geometry is not closed 1` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> @@ -281,49 +215,27 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` value="My shape" /> - - - - - My index - - -
- my geo field - , - "value": "My index/my geo field", - }, - ] + -
+ } + /> void; +} + +interface State { + selectedField: GeoFieldWithIndex | undefined; + filterLabel: string; +} + +export class DistanceFilterForm extends Component { + state = { + selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, + filterLabel: '', + }; + + _onGeoFieldChange = (selectedField: GeoFieldWithIndex | undefined) => { + this.setState({ selectedField }); + }; + + _onFilterLabelChange = (e: ChangeEvent) => { + this.setState({ + filterLabel: e.target.value, + }); + }; + + _onSubmit = () => { + if (!this.state.selectedField) { + return; + } + this.props.onSubmit({ + filterLabel: this.state.filterLabel, + indexPatternId: this.state.selectedField.indexPatternId, + geoFieldName: this.state.selectedField.geoFieldName, + }); + }; + + render() { + return ( + + + + + + + + + + + + {this.props.buttonLabel} + + + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts b/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts new file mode 100644 index 00000000000000..863e0adda8fb25 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +// Maps can contain geo fields from multiple index patterns. GeoFieldWithIndex is used to: +// 1) Combine the geo field along with associated index pattern state. +// 2) Package asynchronously looked up state via indexPatternService to avoid +// PITA of looking up async state in downstream react consumers. +export type GeoFieldWithIndex = { + geoFieldName: string; + geoFieldType: string; + indexPatternTitle: string; + indexPatternId: string; +}; diff --git a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js b/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js index 3308155caa3e40..ac6461345e8bf6 100644 --- a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js +++ b/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js @@ -9,9 +9,6 @@ import PropTypes from 'prop-types'; import { EuiForm, EuiFormRow, - EuiSuperSelect, - EuiTextColor, - EuiText, EuiFieldText, EuiButton, EuiSelect, @@ -22,20 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../../common/constants'; import { getEsSpatialRelationLabel } from '../../common/i18n_getters'; - -const GEO_FIELD_VALUE_DELIMITER = '/'; // `/` is not allowed in index pattern name so should not have collisions - -function createIndexGeoFieldName({ indexPatternTitle, geoFieldName }) { - return `${indexPatternTitle}${GEO_FIELD_VALUE_DELIMITER}${geoFieldName}`; -} - -function splitIndexGeoFieldName(value) { - const split = value.split(GEO_FIELD_VALUE_DELIMITER); - return { - indexPatternTitle: split[0], - geoFieldName: split[1], - }; -} +import { MultiIndexGeoFieldSelect } from './multi_index_geo_field_select'; export class GeometryFilterForm extends Component { static propTypes = { @@ -52,27 +36,13 @@ export class GeometryFilterForm extends Component { }; state = { - geoFieldTag: this.props.geoFields.length - ? createIndexGeoFieldName(this.props.geoFields[0]) - : '', + selectedField: this.props.geoFields.length ? this.props.geoFields[0] : undefined, geometryLabel: this.props.intitialGeometryLabel, relation: ES_SPATIAL_RELATIONS.INTERSECTS, }; - _getSelectedGeoField = () => { - if (!this.state.geoFieldTag) { - return null; - } - - const { indexPatternTitle, geoFieldName } = splitIndexGeoFieldName(this.state.geoFieldTag); - - return this.props.geoFields.find(option => { - return option.indexPatternTitle === indexPatternTitle && option.geoFieldName === geoFieldName; - }); - }; - - _onGeoFieldChange = selectedValue => { - this.setState({ geoFieldTag: selectedValue }); + _onGeoFieldChange = selectedField => { + this.setState({ selectedField }); }; _onGeometryLabelChange = e => { @@ -88,25 +58,21 @@ export class GeometryFilterForm extends Component { }; _onSubmit = () => { - const geoField = this._getSelectedGeoField(); this.props.onSubmit({ geometryLabel: this.state.geometryLabel, - indexPatternId: geoField.indexPatternId, - geoFieldName: geoField.geoFieldName, - geoFieldType: geoField.geoFieldType, + indexPatternId: this.state.selectedField.indexPatternId, + geoFieldName: this.state.selectedField.geoFieldName, + geoFieldType: this.state.selectedField.geoFieldType, relation: this.state.relation, }); }; _renderRelationInput() { - if (!this.state.geoFieldTag) { - return null; - } - - const { geoFieldType } = this._getSelectedGeoField(); - // relationship only used when filtering geo_shape fields - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { + if ( + !this.state.selectedField || + this.state.selectedField.geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT + ) { return null; } @@ -141,20 +107,6 @@ export class GeometryFilterForm extends Component { } render() { - const options = this.props.geoFields.map(({ indexPatternTitle, geoFieldName }) => { - return { - inputDisplay: ( - - - {indexPatternTitle} - -
- {geoFieldName} -
- ), - value: createIndexGeoFieldName({ indexPatternTitle, geoFieldName }), - }; - }); let error; if (this.props.errorMsg) { error = {this.props.errorMsg}; @@ -174,24 +126,11 @@ export class GeometryFilterForm extends Component { />
- - - + {this._renderRelationInput()} @@ -204,7 +143,7 @@ export class GeometryFilterForm extends Component { size="s" fill onClick={this._onSubmit} - isDisabled={!this.state.geometryLabel || !this.state.geoFieldTag} + isDisabled={!this.state.geometryLabel || !this.state.selectedField} isLoading={this.props.isLoading} > {this.props.buttonLabel} diff --git a/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx b/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx new file mode 100644 index 00000000000000..0e5b94f0c64273 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx @@ -0,0 +1,78 @@ +/* + * 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 { EuiFormRow, EuiSuperSelect, EuiTextColor, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { GeoFieldWithIndex } from './geo_field_with_index'; + +const OPTION_ID_DELIMITER = '/'; + +function createOptionId(geoField: GeoFieldWithIndex): string { + // Namespace field with indexPatterId to avoid collisions between field names + return `${geoField.indexPatternId}${OPTION_ID_DELIMITER}${geoField.geoFieldName}`; +} + +function splitOptionId(optionId: string) { + const split = optionId.split(OPTION_ID_DELIMITER); + return { + indexPatternId: split[0], + geoFieldName: split[1], + }; +} + +interface Props { + fields: GeoFieldWithIndex[]; + onChange: (newSelectedField: GeoFieldWithIndex | undefined) => void; + selectedField: GeoFieldWithIndex | undefined; +} + +export function MultiIndexGeoFieldSelect({ fields, onChange, selectedField }: Props) { + function onFieldSelect(selectedOptionId: string) { + const { indexPatternId, geoFieldName } = splitOptionId(selectedOptionId); + + const newSelectedField = fields.find(field => { + return field.indexPatternId === indexPatternId && field.geoFieldName === geoFieldName; + }); + onChange(newSelectedField); + } + + const options = fields.map((geoField: GeoFieldWithIndex) => { + return { + inputDisplay: ( + + + {geoField.indexPatternTitle} + +
+ {geoField.geoFieldName} +
+ ), + value: createOptionId(geoField), + }; + }); + + return ( + + + + ); +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts new file mode 100644 index 00000000000000..f2ceb8685d43e1 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +// @ts-ignore +import turf from 'turf'; +// @ts-ignore +import turfCircle from '@turf/circle'; + +type DrawCircleState = { + circle: { + properties: { + center: {} | null; + radiusKm: number; + }; + id: string | number; + incomingCoords: (coords: unknown[]) => void; + toGeoJSON: () => unknown; + }; +}; + +type MouseEvent = { + lngLat: { + lng: number; + lat: number; + }; +}; + +export const DrawCircle = { + onSetup() { + // @ts-ignore + const circle: unknown = this.newFeature({ + type: 'Feature', + properties: { + center: null, + radiusKm: 0, + }, + geometry: { + type: 'Polygon', + coordinates: [[]], + }, + }); + + // @ts-ignore + this.addFeature(circle); + // @ts-ignore + this.clearSelectedFeatures(); + // @ts-ignore + this.updateUIClasses({ mouse: 'add' }); + // @ts-ignore + this.setActionableState({ + trash: true, + }); + return { + circle, + }; + }, + onKeyUp(state: DrawCircleState, e: { keyCode: number }) { + if (e.keyCode === 27) { + // clear point when user hits escape + state.circle.properties.center = null; + state.circle.properties.radiusKm = 0; + state.circle.incomingCoords([[]]); + } + }, + onClick(state: DrawCircleState, e: MouseEvent) { + if (!state.circle.properties.center) { + // first click, start circle + state.circle.properties.center = [e.lngLat.lng, e.lngLat.lat]; + } else { + // second click, finish draw + // @ts-ignore + this.updateUIClasses({ mouse: 'pointer' }); + state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, [ + e.lngLat.lng, + e.lngLat.lat, + ]); + // @ts-ignore + this.changeMode('simple_select', { featuresId: state.circle.id }); + } + }, + onMouseMove(state: DrawCircleState, e: MouseEvent) { + if (!state.circle.properties.center) { + // circle not started, nothing to update + return; + } + + const mouseLocation = [e.lngLat.lng, e.lngLat.lat]; + state.circle.properties.radiusKm = turf.distance(state.circle.properties.center, mouseLocation); + const newCircleFeature = turfCircle( + state.circle.properties.center, + state.circle.properties.radiusKm + ); + state.circle.incomingCoords(newCircleFeature.geometry.coordinates); + }, + onStop(state: DrawCircleState) { + // @ts-ignore + this.updateUIClasses({ mouse: 'none' }); + // @ts-ignore + this.activateUIButton(); + + // @ts-ignore + if (this.getFeature(state.circle.id) === undefined) return; + + if (state.circle.properties.center && state.circle.properties.radiusKm > 0) { + // @ts-ignore + this.map.fire('draw.create', { + features: [state.circle.toGeoJSON()], + }); + } else { + // @ts-ignore + this.deleteFeature([state.circle.id], { silent: true }); + // @ts-ignore + this.changeMode('simple_select', {}, { silent: true }); + } + }, + toDisplayFeatures( + state: DrawCircleState, + geojson: { properties: { active: string } }, + display: (geojson: unknown) => unknown + ) { + if (state.circle.properties.center) { + geojson.properties.active = 'true'; + return display(geojson); + } + }, + onTrash(state: DrawCircleState) { + // @ts-ignore + this.deleteFeature([state.circle.id], { silent: true }); + // @ts-ignore + this.changeMode('simple_select'); + }, +}; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index f1b4fe2aad1f7f..99abe5d108b5a9 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -9,7 +9,9 @@ import React from 'react'; import { DRAW_TYPE } from '../../../../../common/constants'; import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified'; import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; +import { DrawCircle } from './draw_circle'; import { + createDistanceFilterWithMeta, createSpatialFilterWithBoundingBox, createSpatialFilterWithGeometry, getBoundingBoxGeometry, @@ -19,6 +21,7 @@ import { DrawTooltip } from './draw_tooltip'; const mbDrawModes = MapboxDraw.modes; mbDrawModes.draw_rectangle = DrawRectangle; +mbDrawModes.draw_circle = DrawCircle; export class DrawControl extends React.Component { constructor() { @@ -60,7 +63,21 @@ export class DrawControl extends React.Component { return; } - const isBoundingBox = this.props.drawState.drawType === DRAW_TYPE.BOUNDS; + if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + const circle = e.features[0]; + roundCoordinates(circle.properties.center); + const filter = createDistanceFilterWithMeta({ + alias: this.props.drawState.filterLabel, + distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2), + geoFieldName: this.props.drawState.geoFieldName, + indexPatternId: this.props.drawState.indexPatternId, + point: circle.properties.center, + }); + this.props.addFilters([filter]); + this.props.disableDrawState(); + return; + } + const geometry = e.features[0].geometry; // MapboxDraw returns coordinates with 12 decimals. Round to a more reasonable number roundCoordinates(geometry.coordinates); @@ -73,15 +90,16 @@ export class DrawControl extends React.Component { geometryLabel: this.props.drawState.geometryLabel, relation: this.props.drawState.relation, }; - const filter = isBoundingBox - ? createSpatialFilterWithBoundingBox({ - ...options, - geometry: getBoundingBoxGeometry(geometry), - }) - : createSpatialFilterWithGeometry({ - ...options, - geometry, - }); + const filter = + this.props.drawState.drawType === DRAW_TYPE.BOUNDS + ? createSpatialFilterWithBoundingBox({ + ...options, + geometry: getBoundingBoxGeometry(geometry), + }) + : createSpatialFilterWithGeometry({ + ...options, + geometry, + }); this.props.addFilters([filter]); } catch (error) { // TODO notify user why filter was not created @@ -109,11 +127,14 @@ export class DrawControl extends React.Component { this.props.mbMap.getCanvas().style.cursor = 'crosshair'; this.props.mbMap.on('draw.create', this._onDraw); } - const mbDrawMode = - this.props.drawState.drawType === DRAW_TYPE.POLYGON - ? this._mbDrawControl.modes.DRAW_POLYGON - : 'draw_rectangle'; - this._mbDrawControl.changeMode(mbDrawMode); + + if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + this._mbDrawControl.changeMode('draw_rectangle'); + } else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + this._mbDrawControl.changeMode('draw_circle'); + } else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) { + this._mbDrawControl.changeMode(this._mbDrawControl.modes.DRAW_POLYGON); + } } render() { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js index 463fe529814103..c8bde29b94fb68 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js @@ -42,14 +42,24 @@ export class DrawTooltip extends Component { } render() { - const instructions = - this.props.drawState.drawType === DRAW_TYPE.BOUNDS - ? i18n.translate('xpack.maps.drawTooltip.boundsInstructions', { - defaultMessage: 'Click to start rectangle. Click again to finish.', - }) - : i18n.translate('xpack.maps.drawTooltip.polygonInstructions', { - defaultMessage: 'Click to add vertex. Double click to finish.', - }); + let instructions; + if (this.props.drawState.drawType === DRAW_TYPE.BOUNDS) { + instructions = i18n.translate('xpack.maps.drawTooltip.boundsInstructions', { + defaultMessage: + 'Click to start rectangle. Move mouse to adjust rectangle size. Click again to finish.', + }); + } else if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { + instructions = i18n.translate('xpack.maps.drawTooltip.distanceInstructions', { + defaultMessage: 'Click to set point. Move mouse to adjust distance. Click to finish.', + }); + } else if (this.props.drawState.drawType === DRAW_TYPE.POLYGON) { + instructions = i18n.translate('xpack.maps.drawTooltip.polygonInstructions', { + defaultMessage: 'Click to start shape. Click to add vertex. Double click to finish.', + }); + } else { + // unknown draw type, tooltip not needed + return null; + } const tooltipAnchor = (
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap index 681c3f0fbfd612..d7fa099fe9dbe9 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap @@ -41,6 +41,10 @@ exports[`Should render cancel button when drawing 1`] = ` "name": "Draw bounds to filter data", "panel": 2, }, + Object { + "name": "Draw distance to filter data", + "panel": 3, + }, ], "title": "Tools", }, @@ -86,6 +90,25 @@ exports[`Should render cancel button when drawing 1`] = ` "id": 2, "title": "Draw bounds", }, + Object { + "content": , + "id": 3, + "title": "Draw distance", + }, ] } /> @@ -144,6 +167,10 @@ exports[`renders 1`] = ` "name": "Draw bounds to filter data", "panel": 2, }, + Object { + "name": "Draw distance to filter data", + "panel": 3, + }, ], "title": "Tools", }, @@ -189,6 +216,25 @@ exports[`renders 1`] = ` "id": 2, "title": "Draw bounds", }, + Object { + "content": , + "id": 3, + "title": "Draw distance", + }, ] } /> diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js index ea6ffe3ba14355..e7c125abe70c7b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js @@ -14,9 +14,10 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DRAW_TYPE } from '../../../../common/constants'; +import { DRAW_TYPE, ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; import { GeometryFilterForm } from '../../../components/geometry_filter_form'; +import { DistanceFilterForm } from '../../../components/distance_filter_form'; const DRAW_SHAPE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabel', { defaultMessage: 'Draw shape to filter data', @@ -26,6 +27,10 @@ const DRAW_BOUNDS_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawBoundsLa defaultMessage: 'Draw bounds to filter data', }); +const DRAW_DISTANCE_LABEL = i18n.translate('xpack.maps.toolbarOverlay.drawDistanceLabel', { + defaultMessage: 'Draw distance to filter data', +}); + const DRAW_SHAPE_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawShapeLabelShort', { defaultMessage: 'Draw shape', }); @@ -34,6 +39,13 @@ const DRAW_BOUNDS_LABEL_SHORT = i18n.translate('xpack.maps.toolbarOverlay.drawBo defaultMessage: 'Draw bounds', }); +const DRAW_DISTANCE_LABEL_SHORT = i18n.translate( + 'xpack.maps.toolbarOverlay.drawDistanceLabelShort', + { + defaultMessage: 'Draw distance', + } +); + export class ToolsControl extends Component { state = { isPopoverOpen: false, @@ -65,23 +77,43 @@ export class ToolsControl extends Component { this._closePopover(); }; + _initiateDistanceDraw = options => { + this.props.initiateDraw({ + drawType: DRAW_TYPE.DISTANCE, + ...options, + }); + this._closePopover(); + }; + _getDrawPanels() { + const tools = [ + { + name: DRAW_SHAPE_LABEL, + panel: 1, + }, + { + name: DRAW_BOUNDS_LABEL, + panel: 2, + }, + ]; + + const hasGeoPoints = this.props.geoFields.some(({ geoFieldType }) => { + return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + }); + if (hasGeoPoints) { + tools.push({ + name: DRAW_DISTANCE_LABEL, + panel: 3, + }); + } + return [ { id: 0, title: i18n.translate('xpack.maps.toolbarOverlay.tools.toolbarTitle', { defaultMessage: 'Tools', }), - items: [ - { - name: DRAW_SHAPE_LABEL, - panel: 1, - }, - { - name: DRAW_BOUNDS_LABEL, - panel: 2, - }, - ], + items: tools, }, { id: 1, @@ -119,6 +151,20 @@ export class ToolsControl extends Component { /> ), }, + { + id: 3, + title: DRAW_DISTANCE_LABEL_SHORT, + content: ( + { + return geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT; + })} + onSubmit={this._initiateDistanceDraw} + /> + ), + }, ]; } diff --git a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js index 9b33d3036785c9..79467e26ec3fad 100644 --- a/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/legacy/plugins/maps/public/elasticsearch_geo_utils.js @@ -344,6 +344,39 @@ function createGeometryFilterWithMeta({ return createGeoPolygonFilter(geometry.coordinates, geoFieldName, { meta }); } +export function createDistanceFilterWithMeta({ + alias, + distanceKm, + geoFieldName, + indexPatternId, + point, +}) { + const meta = { + type: SPATIAL_FILTER_TYPE, + negate: false, + index: indexPatternId, + key: geoFieldName, + alias: alias + ? alias + : i18n.translate('xpack.maps.es_geo_utils.distanceFilterAlias', { + defaultMessage: '{geoFieldName} within {distanceKm}km of {pointLabel}', + values: { + distanceKm, + geoFieldName, + pointLabel: point.join(','), + }, + }), + }; + + return { + geo_distance: { + distance: `${distanceKm}km`, + [geoFieldName]: point, + }, + meta, + }; +} + export function roundCoordinates(coordinates) { for (let i = 0; i < coordinates.length; i++) { const value = coordinates[i]; diff --git a/x-pack/package.json b/x-pack/package.json index b9c4f7c554e958..3c8aa435c3e432 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -196,6 +196,7 @@ "@scant/router": "^0.1.0", "@slack/webhook": "^5.0.0", "@turf/boolean-contains": "6.0.1", + "@turf/circle": "6.0.1", "angular": "^1.7.9", "angular-resource": "1.7.9", "angular-sanitize": "1.7.9", diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index ae3e164ffb2bc1..b1483cefa43bc0 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -120,6 +120,7 @@ export const EMPTY_FEATURE_COLLECTION = { export const DRAW_TYPE = { BOUNDS: 'BOUNDS', + DISTANCE: 'DISTANCE', POLYGON: 'POLYGON', }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3763020a0b692c..ad49f0242e8e6e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7073,7 +7073,6 @@ "xpack.maps.feature.appDescription": "Elasticsearch と Elastic Maps Service の地理空間データを閲覧します", "xpack.maps.featureRegistry.mapsFeatureName": "マップ", "xpack.maps.geoGrid.resolutionLabel": "グリッド解像度", - "xpack.maps.geometryFilterForm.geoFieldLabel": "フィルタリングされたフィールド", "xpack.maps.geometryFilterForm.geometryLabelLabel": "ジオメトリラベル", "xpack.maps.geometryFilterForm.relationLabel": "空間関係", "xpack.maps.heatmap.colorRampLabel": "色の範囲", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0477470a4b8a1d..76ecf333eb10a4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7073,7 +7073,6 @@ "xpack.maps.feature.appDescription": "从 Elasticsearch 和 Elastic 地图服务浏览地理空间数据", "xpack.maps.featureRegistry.mapsFeatureName": "Maps", "xpack.maps.geoGrid.resolutionLabel": "网格分辨率", - "xpack.maps.geometryFilterForm.geoFieldLabel": "已筛选字段", "xpack.maps.geometryFilterForm.geometryLabelLabel": "几何标签", "xpack.maps.geometryFilterForm.relationLabel": "空间关系", "xpack.maps.heatmap.colorRampLabel": "颜色范围", diff --git a/yarn.lock b/yarn.lock index ae55508cee886f..dcb360587e4ed0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4086,6 +4086,22 @@ "@turf/helpers" "6.x" "@turf/invariant" "6.x" +"@turf/circle@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/circle/-/circle-6.0.1.tgz#0ab72083373ae3c76b700c17a504ab1b5c0910b9" + integrity sha512-pF9XsYtCvY9ZyNqJ3hFYem9VaiGdVNQb0SFq/zzDMwH3iWZPPJQHnnDB/3e8RD1VDtBBov9p5uO2k7otsfezjw== + dependencies: + "@turf/destination" "6.x" + "@turf/helpers" "6.x" + +"@turf/destination@6.x": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.0.1.tgz#5275887fa96ec463f44864a2c17f0b712361794a" + integrity sha512-MroK4nRdp7as174miCAugp8Uvorhe6rZ7MJiC9Hb4+hZR7gNFJyVKmkdDDXIoCYs6MJQsx0buI+gsCpKwgww0Q== + dependencies: + "@turf/helpers" "6.x" + "@turf/invariant" "6.x" + "@turf/helpers@6.x": version "6.1.4" resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.1.4.tgz#d6fd7ebe6782dd9c87dca5559bda5c48ae4c3836" From dccfa593dc3c0e2d95dc77709647e6313ac9c1b2 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 16 Mar 2020 15:37:42 -0400 Subject: [PATCH 08/17] Embeddable API cleanup (#60207) * wip * Remove test in legacy functional plugin --- .../hello_world_embeddable_factory.ts | 2 +- .../public/list_container/list_container.tsx | 7 +- .../list_container/list_container_factory.ts | 13 ++-- .../multi_task_todo_embeddable_factory.ts | 2 +- examples/embeddable_examples/public/plugin.ts | 47 +++++++------ .../searchable_list_container.tsx | 7 +- .../searchable_list_container_factory.ts | 13 ++-- .../public/todo/todo_embeddable_factory.tsx | 11 ++- examples/embeddable_explorer/public/app.tsx | 4 +- .../public/embeddable_panel_example.tsx | 7 +- .../public/hello_world_embeddable_example.tsx | 4 +- .../public/list_container_example.tsx | 7 +- .../embeddable_explorer/public/plugin.tsx | 4 +- .../public/todo_embeddable_example.tsx | 4 +- .../public/dashboard/np_ready/application.ts | 4 +- .../kibana/public/dashboard/plugin.ts | 6 +- .../embeddable/search_embeddable_factory.ts | 19 +++-- .../kibana/public/discover/plugin.ts | 31 ++++---- .../public/visualize/kibana_services.ts | 4 +- .../public/visualize/np_ready/types.d.ts | 4 +- .../kibana/public/visualize/plugin.ts | 6 +- .../visualize_embeddable_factory.tsx | 5 +- .../public/np_ready/public/mocks.ts | 2 +- .../public/np_ready/public/plugin.ts | 4 +- .../ui/public/new_platform/new_platform.ts | 6 +- .../actions/expand_panel_action.test.tsx | 3 +- .../actions/open_replace_panel_flyout.tsx | 4 +- .../actions/replace_panel_action.test.tsx | 3 +- .../public/actions/replace_panel_action.tsx | 4 +- .../public/actions/replace_panel_flyout.tsx | 4 +- .../public/embeddable/dashboard_container.tsx | 4 +- .../dashboard_container_factory.tsx | 48 ++++++------- .../embeddable/grid/dashboard_grid.test.tsx | 4 +- src/plugins/dashboard/public/plugin.tsx | 70 +++++++++++-------- .../public/api/get_embeddable_factories.ts | 26 ------- .../public/api/get_embeddable_factory.ts | 34 --------- src/plugins/embeddable/public/api/index.ts | 47 ------------- .../public/api/register_embeddable_factory.ts | 32 --------- .../embeddable/public/api/tests/helpers.ts | 27 ------- src/plugins/embeddable/public/api/types.ts | 43 ------------ src/plugins/embeddable/public/index.ts | 4 +- .../lib/actions/edit_panel_action.test.tsx | 12 ++-- .../public/lib/actions/edit_panel_action.ts | 5 +- .../public/lib/containers/container.ts | 4 +- .../embeddable_child_panel.test.tsx | 5 +- .../lib/containers/embeddable_child_panel.tsx | 6 +- .../lib/embeddables/embeddable_factory.ts | 6 +- .../embeddable_factory_renderer.test.tsx | 8 +-- .../embeddable_factory_renderer.tsx | 4 +- .../lib/panel/embeddable_panel.test.tsx | 4 +- .../public/lib/panel/embeddable_panel.tsx | 7 +- .../add_panel/add_panel_action.test.tsx | 6 +- .../add_panel/add_panel_action.ts | 7 +- .../add_panel/add_panel_flyout.test.tsx | 9 +-- .../add_panel/add_panel_flyout.tsx | 6 +- .../add_panel/open_add_panel_flyout.tsx | 6 +- .../customize_panel_action.test.ts | 3 +- .../inspect_panel_action.test.tsx | 6 +- .../remove_panel_action.test.tsx | 7 +- .../contact_card_embeddable_factory.tsx | 2 +- .../slow_contact_card_embeddable_factory.ts | 2 +- .../embeddables/filterable_container.tsx | 4 +- .../filterable_container_factory.ts | 6 +- .../filterable_embeddable_factory.ts | 2 +- .../embeddables/hello_world_container.tsx | 6 +- .../hello_world_container_component.tsx | 6 +- src/plugins/embeddable/public/lib/types.ts | 4 -- src/plugins/embeddable/public/mocks.ts | 7 +- .../tests/registry.test.ts => plugin.test.ts} | 16 +++-- src/plugins/embeddable/public/plugin.ts | 67 +++++++++++++----- .../public/tests/apply_filter_action.test.ts | 14 ++-- .../embeddable/public/tests/container.test.ts | 12 ++-- .../tests/customize_panel_modal.test.tsx | 8 +-- .../embeddable/public/tests/test_plugin.ts | 6 +- .../public/np_ready/public/app/app.tsx | 9 +-- .../app/dashboard_container_example.tsx | 16 +++-- .../hello_world_embeddable_factory.ts | 28 -------- .../public/np_ready/public/plugin.tsx | 30 ++------ .../dashboard_container.js | 14 ---- .../embeddable/embeddable_factory.ts | 42 ++++++----- .../editor_frame_service/service.test.tsx | 11 ++- .../public/editor_frame_service/service.tsx | 40 ++++++----- x-pack/legacy/plugins/lens/public/plugin.tsx | 6 +- .../embeddables/embedded_map_helpers.tsx | 11 +-- .../public/components/embeddables/types.ts | 6 -- x-pack/legacy/plugins/siem/public/plugin.tsx | 4 +- .../advanced_ui_actions/public/plugin.ts | 8 +-- .../test_helpers/time_range_container.ts | 4 +- .../time_range_embeddable_factory.ts | 2 +- .../public/embeddables/resolver/factory.ts | 2 +- x-pack/plugins/endpoint/public/plugin.ts | 4 +- .../plugins/resolver_test/public/plugin.ts | 8 ++- 92 files changed, 451 insertions(+), 647 deletions(-) delete mode 100644 src/plugins/embeddable/public/api/get_embeddable_factories.ts delete mode 100644 src/plugins/embeddable/public/api/get_embeddable_factory.ts delete mode 100644 src/plugins/embeddable/public/api/index.ts delete mode 100644 src/plugins/embeddable/public/api/register_embeddable_factory.ts delete mode 100644 src/plugins/embeddable/public/api/tests/helpers.ts delete mode 100644 src/plugins/embeddable/public/api/types.ts rename src/plugins/embeddable/public/{api/tests/registry.test.ts => plugin.test.ts} (70%) delete mode 100644 test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts diff --git a/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts b/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts index de5a3d9380def3..2995c99ac9e58a 100644 --- a/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts +++ b/examples/embeddable_examples/public/hello_world/hello_world_embeddable_factory.ts @@ -33,7 +33,7 @@ export class HelloWorldEmbeddableFactory extends EmbeddableFactory { * embeddables should check the UI Capabilities service to be sure of * the right permissions. */ - public isEditable() { + public async isEditable() { return true; } diff --git a/examples/embeddable_examples/public/list_container/list_container.tsx b/examples/embeddable_examples/public/list_container/list_container.tsx index 35a674a03573a6..bbbd0d6e323046 100644 --- a/examples/embeddable_examples/public/list_container/list_container.tsx +++ b/examples/embeddable_examples/public/list_container/list_container.tsx @@ -21,7 +21,7 @@ import ReactDOM from 'react-dom'; import { Container, ContainerInput, - GetEmbeddableFactory, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { ListContainerComponent } from './list_container_component'; @@ -31,7 +31,10 @@ export class ListContainer extends Container<{}, ContainerInput> { public readonly type = LIST_CONTAINER; private node?: HTMLElement; - constructor(input: ContainerInput, getEmbeddableFactory: GetEmbeddableFactory) { + constructor( + input: ContainerInput, + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'] + ) { super(input, { embeddableLoaded: {} }, getEmbeddableFactory); } diff --git a/examples/embeddable_examples/public/list_container/list_container_factory.ts b/examples/embeddable_examples/public/list_container/list_container_factory.ts index de6b7d5f5e5039..247cf48b41bde2 100644 --- a/examples/embeddable_examples/public/list_container/list_container_factory.ts +++ b/examples/embeddable_examples/public/list_container/list_container_factory.ts @@ -20,25 +20,30 @@ import { i18n } from '@kbn/i18n'; import { EmbeddableFactory, - GetEmbeddableFactory, ContainerInput, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { LIST_CONTAINER, ListContainer } from './list_container'; +interface StartServices { + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; +} + export class ListContainerFactory extends EmbeddableFactory { public readonly type = LIST_CONTAINER; public readonly isContainerType = true; - constructor(private getEmbeddableFactory: GetEmbeddableFactory) { + constructor(private getStartServices: () => Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } public async create(initialInput: ContainerInput) { - return new ListContainer(initialInput, this.getEmbeddableFactory); + const { getEmbeddableFactory } = await this.getStartServices(); + return new ListContainer(initialInput, getEmbeddableFactory); } public getDisplayName() { diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts index a54201b157a6ca..9afdeabaee7656 100644 --- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts +++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable_factory.ts @@ -32,7 +32,7 @@ export class MultiTaskTodoEmbeddableFactory extends EmbeddableFactory< > { public readonly type = MULTI_TASK_TODO_EMBEDDABLE; - public isEditable() { + public async isEditable() { return true; } diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index b7a4f5c078d546..3663af68ae2c79 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -17,20 +17,11 @@ * under the License. */ -import { - IEmbeddableSetup, - IEmbeddableStart, - EmbeddableFactory, -} from '../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from './hello_world'; import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoInput, TodoOutput } from './todo'; -import { - MULTI_TASK_TODO_EMBEDDABLE, - MultiTaskTodoEmbeddableFactory, - MultiTaskTodoOutput, - MultiTaskTodoInput, -} from './multi_task_todo'; +import { MULTI_TASK_TODO_EMBEDDABLE, MultiTaskTodoEmbeddableFactory } from './multi_task_todo'; import { SEARCHABLE_LIST_CONTAINER, SearchableListContainerFactory, @@ -38,46 +29,56 @@ import { import { LIST_CONTAINER, ListContainerFactory } from './list_container'; interface EmbeddableExamplesSetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; } interface EmbeddableExamplesStartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; } export class EmbeddableExamplesPlugin implements Plugin { - public setup(core: CoreSetup, deps: EmbeddableExamplesSetupDependencies) { + public setup( + core: CoreSetup, + deps: EmbeddableExamplesSetupDependencies + ) { deps.embeddable.registerEmbeddableFactory( HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory() ); - deps.embeddable.registerEmbeddableFactory< - EmbeddableFactory - >(MULTI_TASK_TODO_EMBEDDABLE, new MultiTaskTodoEmbeddableFactory()); - } + deps.embeddable.registerEmbeddableFactory( + MULTI_TASK_TODO_EMBEDDABLE, + new MultiTaskTodoEmbeddableFactory() + ); - public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) { // These are registered in the start method because `getEmbeddableFactory ` // is only available in start. We could reconsider this I think and make it // available in both. deps.embeddable.registerEmbeddableFactory( SEARCHABLE_LIST_CONTAINER, - new SearchableListContainerFactory(deps.embeddable.getEmbeddableFactory) + new SearchableListContainerFactory(async () => ({ + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })) ); deps.embeddable.registerEmbeddableFactory( LIST_CONTAINER, - new ListContainerFactory(deps.embeddable.getEmbeddableFactory) + new ListContainerFactory(async () => ({ + getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + })) ); - deps.embeddable.registerEmbeddableFactory>( + deps.embeddable.registerEmbeddableFactory( TODO_EMBEDDABLE, - new TodoEmbeddableFactory(core.overlays.openModal) + new TodoEmbeddableFactory(async () => ({ + openModal: (await core.getStartServices())[0].overlays.openModal, + })) ); } + public start(core: CoreStart, deps: EmbeddableExamplesStartDependencies) {} + public stop() {} } diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx index 3079abb867c387..06462937c768d7 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx @@ -21,7 +21,7 @@ import ReactDOM from 'react-dom'; import { Container, ContainerInput, - GetEmbeddableFactory, + EmbeddableStart, EmbeddableInput, } from '../../../../src/plugins/embeddable/public'; import { SearchableListContainerComponent } from './searchable_list_container_component'; @@ -40,7 +40,10 @@ export class SearchableListContainer extends Container Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } public async create(initialInput: SearchableContainerInput) { - return new SearchableListContainer(initialInput, this.getEmbeddableFactory); + const { getEmbeddableFactory } = await this.getStartServices(); + return new SearchableListContainer(initialInput, getEmbeddableFactory); } public getDisplayName() { diff --git a/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx b/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx index dd2168bb39eeeb..d7be436905382f 100644 --- a/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/todo/todo_embeddable_factory.tsx @@ -43,6 +43,10 @@ function TaskInput({ onSave }: { onSave: (task: string) => void }) { ); } +interface StartServices { + openModal: OverlayStart['openModal']; +} + export class TodoEmbeddableFactory extends EmbeddableFactory< TodoInput, TodoOutput, @@ -50,11 +54,11 @@ export class TodoEmbeddableFactory extends EmbeddableFactory< > { public readonly type = TODO_EMBEDDABLE; - constructor(private openModal: OverlayStart['openModal']) { + constructor(private getStartServices: () => Promise) { super(); } - public isEditable() { + public async isEditable() { return true; } @@ -69,9 +73,10 @@ export class TodoEmbeddableFactory extends EmbeddableFactory< * in this case, the task string. */ public async getExplicitInput() { + const { openModal } = await this.getStartServices(); return new Promise<{ task: string }>(resolve => { const onSave = (task: string) => resolve({ task }); - const overlay = this.openModal( + const overlay = openModal( toMountPoint( { diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx index da7e8cc188e31f..9c8568454855db 100644 --- a/examples/embeddable_explorer/public/app.tsx +++ b/examples/embeddable_explorer/public/app.tsx @@ -23,7 +23,7 @@ import { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from import { EuiPage, EuiPageSideBar, EuiSideNav } from '@elastic/eui'; -import { IEmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public'; import { @@ -74,7 +74,7 @@ const Nav = withRouter(({ history, navigateToApp, pages }: NavProps) => { interface Props { basename: string; navigateToApp: CoreStart['application']['navigateToApp']; - embeddableApi: IEmbeddableStart; + embeddableApi: EmbeddableStart; uiActionsApi: UiActionsStart; overlays: OverlayStart; notifications: CoreStart['notifications']; diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx index e6687d8563f59d..b26111bed7ff23 100644 --- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx +++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx @@ -31,9 +31,8 @@ import { import { EuiSpacer } from '@elastic/eui'; import { OverlayStart, CoreStart, SavedObjectsStart, IUiSettingsClient } from 'kibana/public'; import { - GetEmbeddableFactory, EmbeddablePanel, - IEmbeddableStart, + EmbeddableStart, IEmbeddable, } from '../../../src/plugins/embeddable/public'; import { @@ -47,8 +46,8 @@ import { Start as InspectorStartContract } from '../../../src/plugins/inspector/ import { getSavedObjectFinder } from '../../../src/plugins/saved_objects/public'; interface Props { - getAllEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; - getEmbeddableFactory: GetEmbeddableFactory; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; uiActionsApi: UiActionsStart; overlays: OverlayStart; notifications: CoreStart['notifications']; diff --git a/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx b/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx index 74a6766a1b5ee4..ea1c3d781ebfda 100644 --- a/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx +++ b/examples/embeddable_explorer/public/hello_world_embeddable_example.tsx @@ -29,14 +29,14 @@ import { EuiText, } from '@elastic/eui'; import { - GetEmbeddableFactory, + EmbeddableStart, EmbeddableFactoryRenderer, EmbeddableRoot, } from '../../../src/plugins/embeddable/public'; import { HelloWorldEmbeddable, HELLO_WORLD_EMBEDDABLE } from '../../embeddable_examples/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } export function HelloWorldEmbeddableExample({ getEmbeddableFactory }: Props) { diff --git a/examples/embeddable_explorer/public/list_container_example.tsx b/examples/embeddable_explorer/public/list_container_example.tsx index 2c7b12a27d963a..969fdb0ca46db8 100644 --- a/examples/embeddable_explorer/public/list_container_example.tsx +++ b/examples/embeddable_explorer/public/list_container_example.tsx @@ -29,10 +29,7 @@ import { EuiText, } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { - GetEmbeddableFactory, - EmbeddableFactoryRenderer, -} from '../../../src/plugins/embeddable/public'; +import { EmbeddableFactoryRenderer, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { HELLO_WORLD_EMBEDDABLE, TODO_EMBEDDABLE, @@ -42,7 +39,7 @@ import { } from '../../embeddable_examples/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } export function ListContainerExample({ getEmbeddableFactory }: Props) { diff --git a/examples/embeddable_explorer/public/plugin.tsx b/examples/embeddable_explorer/public/plugin.tsx index 1294e0c89c9e7d..7c75b108d99122 100644 --- a/examples/embeddable_explorer/public/plugin.tsx +++ b/examples/embeddable_explorer/public/plugin.tsx @@ -19,12 +19,12 @@ import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { UiActionsService } from '../../../src/plugins/ui_actions/public'; -import { IEmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; interface StartDeps { uiActions: UiActionsService; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStart; } diff --git a/examples/embeddable_explorer/public/todo_embeddable_example.tsx b/examples/embeddable_explorer/public/todo_embeddable_example.tsx index b1c93087faf83b..ce92301236c2b8 100644 --- a/examples/embeddable_explorer/public/todo_embeddable_example.tsx +++ b/examples/embeddable_explorer/public/todo_embeddable_example.tsx @@ -39,10 +39,10 @@ import { TODO_EMBEDDABLE, TodoEmbeddableFactory, } from '../../../examples/embeddable_examples/public/todo'; -import { GetEmbeddableFactory, EmbeddableRoot } from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableRoot } from '../../../src/plugins/embeddable/public'; interface Props { - getEmbeddableFactory: GetEmbeddableFactory; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; } interface State { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 9ca84735cac168..fe0e7a1d3e6d00 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -39,7 +39,7 @@ import { } from '../legacy_imports'; // @ts-ignore import { initDashboardApp } from './legacy_app'; -import { IEmbeddableStart } from '../../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; import { DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; @@ -67,7 +67,7 @@ export interface RenderDeps { chrome: ChromeStart; addBasePath: (path: string) => string; savedQueryService: DataPublicPluginStart['query']['savedQueries']; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; localStorage: Storage; share: SharePluginStart; config: KibanaLegacyStart['config']; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index d94612225782dc..a9ee77921ed4aa 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -35,7 +35,7 @@ import { DataPublicPluginSetup, esFilters, } from '../../../../../plugins/data/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { DashboardConstants } from './np_ready/dashboard_constants'; @@ -54,7 +54,7 @@ import { createKbnUrlTracker } from '../../../../../plugins/kibana_utils/public' export interface DashboardPluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; kibanaLegacy: KibanaLegacyStart; @@ -70,7 +70,7 @@ export class DashboardPlugin implements Plugin { private startDependencies: { data: DataPublicPluginStart; savedObjectsClient: SavedObjectsClientContract; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; dashboardConfig: KibanaLegacyStart['dashboardConfig']; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts index 90f1549c9f369e..6f3adc1f4fcce6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable_factory.ts @@ -32,6 +32,11 @@ import { SearchEmbeddable } from './search_embeddable'; import { SearchInput, SearchOutput } from './types'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; +interface StartServices { + executeTriggerActions: UiActionsStart['executeTriggerActions']; + isEditable: () => boolean; +} + export class SearchEmbeddableFactory extends EmbeddableFactory< SearchInput, SearchOutput, @@ -40,12 +45,10 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< public readonly type = SEARCH_EMBEDDABLE_TYPE; private $injector: auto.IInjectorService | null; private getInjector: () => Promise | null; - public isEditable: () => boolean; constructor( - private readonly executeTriggerActions: UiActionsStart['executeTriggerActions'], - getInjector: () => Promise, - isEditable: () => boolean + private getStartServices: () => Promise, + getInjector: () => Promise ) { super({ savedObjectMetaData: { @@ -58,13 +61,16 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< }); this.$injector = null; this.getInjector = getInjector; - this.isEditable = isEditable; } public canCreateNew() { return false; } + public async isEditable() { + return (await this.getStartServices()).isEditable(); + } + public getDisplayName() { return i18n.translate('kbn.embeddable.search.displayName', { defaultMessage: 'search', @@ -90,6 +96,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< try { const savedObject = await getServices().getSavedSearchById(savedObjectId); const indexPattern = savedObject.searchSource.getField('index'); + const { executeTriggerActions } = await this.getStartServices(); return new SearchEmbeddable( { savedSearch: savedObject, @@ -101,7 +108,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory< indexPatterns: indexPattern ? [indexPattern] : [], }, input, - this.executeTriggerActions, + executeTriggerActions, parent ); } catch (e) { diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 3ba0418d35f718..ba671a64592a57 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -30,7 +30,7 @@ import { } from '../../../../../plugins/data/public'; import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; -import { IEmbeddableStart, IEmbeddableSetup } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableSetup } from '../../../../../plugins/embeddable/public'; import { getInnerAngularModule, getInnerAngularModuleEmbeddable } from './get_inner_angular'; import { setAngularModule, setServices } from './kibana_services'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; @@ -63,7 +63,7 @@ export interface DiscoverSetup { export type DiscoverStart = void; export interface DiscoverSetupPlugins { uiActions: UiActionsSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; kibanaLegacy: KibanaLegacySetup; home: HomePublicPluginSetup; visualizations: VisualizationsSetup; @@ -71,7 +71,7 @@ export interface DiscoverSetupPlugins { } export interface DiscoverStartPlugins { uiActions: UiActionsStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; charts: ChartsPluginStart; data: DataPublicPluginStart; @@ -103,7 +103,7 @@ export class DiscoverPlugin implements Plugin { public initializeInnerAngular?: () => void; public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; - setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { + setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/discover', @@ -173,6 +173,7 @@ export class DiscoverPlugin implements Plugin { }); registerFeature(plugins.home); + this.registerEmbeddable(core, plugins); return { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }; @@ -203,8 +204,6 @@ export class DiscoverPlugin implements Plugin { return { core, plugins }; }; - - this.registerEmbeddable(core, plugins); } stop() { @@ -216,19 +215,25 @@ export class DiscoverPlugin implements Plugin { /** * register embeddable with a slimmer embeddable version of inner angular */ - private async registerEmbeddable(core: CoreStart, plugins: DiscoverStartPlugins) { + private async registerEmbeddable( + core: CoreSetup, + plugins: DiscoverSetupPlugins + ) { const { SearchEmbeddableFactory } = await import('./np_ready/embeddable'); - const isEditable = () => core.application.capabilities.discover.save as boolean; if (!this.getEmbeddableInjector) { throw Error('Discover plugin method getEmbeddableInjector is undefined'); } - const factory = new SearchEmbeddableFactory( - plugins.uiActions.executeTriggerActions, - this.getEmbeddableInjector, - isEditable - ); + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + return { + executeTriggerActions: deps.uiActions.executeTriggerActions, + isEditable: () => coreStart.application.capabilities.discover.save as boolean, + }; + }; + + const factory = new SearchEmbeddableFactory(getStartServices, this.getEmbeddableInjector); plugins.embeddable.registerEmbeddableFactory(factory.type, factory); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index cfd12b32834590..7e96d7bde6e13d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -29,7 +29,7 @@ import { import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { Storage } from '../../../../../plugins/kibana_utils/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; import { DataPublicPluginStart, IndexPatternsContract } from '../../../../../plugins/data/public'; import { VisualizationsStart } from '../../../visualizations/public'; @@ -44,7 +44,7 @@ export interface VisualizeKibanaServices { chrome: ChromeStart; core: CoreStart; data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; getBasePath: () => string; indexPatterns: IndexPatternsContract; localStorage: Storage; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts index ccb3b3ddbb1da1..01ce872aeb679e 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/types.d.ts @@ -24,7 +24,7 @@ import { DataPublicPluginStart, SavedQuery, } from 'src/plugins/data/public'; -import { IEmbeddableStart } from 'src/plugins/embeddable/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { PersistedState } from 'src/plugins/visualizations/public'; import { LegacyCoreStart } from 'kibana/public'; import { Vis } from 'src/legacy/core_plugins/visualizations/public'; @@ -61,7 +61,7 @@ export interface EditorRenderProps { appState: { save(): void }; core: LegacyCoreStart; data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; filters: Filter[]; uiState: PersistedState; timeRange: TimeRange; diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index b9e4487ae84fbc..9d88152c59aa74 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -36,7 +36,7 @@ import { DataPublicPluginSetup, esFilters, } from '../../../../../plugins/data/public'; -import { IEmbeddableStart } from '../../../../../plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { SharePluginStart } from '../../../../../plugins/share/public'; import { @@ -55,7 +55,7 @@ import { DefaultEditorController } from '../../../vis_default_editor/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; share: SharePluginStart; visualizations: VisualizationsStart; @@ -71,7 +71,7 @@ export interface VisualizePluginSetupDependencies { export class VisualizePlugin implements Plugin { private startDependencies: { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; navigation: NavigationStart; savedObjectsClient: SavedObjectsClientContract; share: SharePluginStart; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable_factory.tsx b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable_factory.tsx index 237eeff42d9b2d..1cd97115ee10e9 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable_factory.tsx @@ -83,7 +83,7 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< }); } - public isEditable() { + public async isEditable() { return getCapabilities().visualize.save as boolean; } @@ -114,13 +114,14 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< const indexPattern = await getIndexPattern(savedObject); const indexPatterns = indexPattern ? [indexPattern] : []; + const editable = await this.isEditable(); return new VisualizeEmbeddable( getTimeFilter(), { savedVisualization: savedObject, indexPatterns, editUrl, - editable: this.isEditable(), + editable, appState: input.appState, uiState: input.uiState, }, diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts index 2785247296ff4a..4ee727e46f4d6c 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -49,7 +49,7 @@ const createInstance = async () => { const setup = plugin.setup(coreMock.createSetup(), { data: dataPluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), - embeddable: embeddablePluginMock.createStartContract(), + embeddable: embeddablePluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), }); const doStart = () => diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts index 5a8a55d470540e..953caecefb9748 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -42,7 +42,7 @@ import { } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeEmbeddableFactory } from './embeddable'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../../plugins/expressions/public'; -import { IEmbeddableSetup } from '../../../../../../plugins/embeddable/public'; +import { EmbeddableSetup } from '../../../../../../plugins/embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; import { @@ -73,7 +73,7 @@ export interface VisualizationsStart extends TypesStart { export interface VisualizationsSetupDeps { expressions: ExpressionsSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; usageCollection: UsageCollectionSetup; data: DataPublicPluginSetup; } diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index ce4e1b05518817..07e17ad5622918 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -20,7 +20,7 @@ import { IScope } from 'angular'; import { UiActionsStart, UiActionsSetup } from 'src/plugins/ui_actions/public'; -import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; +import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public'; import { createBrowserHistory } from 'history'; import { LegacyCoreSetup, @@ -68,7 +68,7 @@ export interface PluginsSetup { bfetch: BfetchPublicSetup; charts: ChartsPluginSetup; data: ReturnType; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; expressions: ReturnType; home: HomePublicPluginSetup; inspector: InspectorSetup; @@ -88,7 +88,7 @@ export interface PluginsStart { bfetch: BfetchPublicStart; charts: ChartsPluginStart; data: ReturnType; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ReturnType; inspector: InspectorStart; uiActions: UiActionsStart; diff --git a/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx index f8c05170e8f672..22cf854a46623e 100644 --- a/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/actions/expand_panel_action.test.tsx @@ -28,7 +28,6 @@ import { ContactCardEmbeddableInput, ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; -import { DashboardOptions } from '../embeddable/dashboard_container_factory'; const embeddableFactories = new Map(); embeddableFactories.set( @@ -40,7 +39,7 @@ let container: DashboardContainer; let embeddable: ContactCardEmbeddable; beforeEach(async () => { - const options: DashboardOptions = { + const options = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, application: {} as any, diff --git a/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx index f15d538703e21c..3472d208f814c4 100644 --- a/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/actions/open_replace_panel_flyout.tsx @@ -24,7 +24,7 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput, - IEmbeddableStart, + EmbeddableStart, IContainer, } from '../embeddable_plugin'; @@ -34,7 +34,7 @@ export async function openReplacePanelFlyout(options: { savedObjectFinder: React.ComponentType; notifications: CoreStart['notifications']; panelToRemove: IEmbeddable; - getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories']; + getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; }) { const { embeddable, diff --git a/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx index 4438a6c9971261..69346dc8c118a6 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_action.test.tsx @@ -27,7 +27,6 @@ import { ContactCardEmbeddableInput, ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; -import { DashboardOptions } from '../embeddable/dashboard_container_factory'; import { coreMock } from '../../../../core/public/mocks'; import { CoreStart } from 'kibana/public'; @@ -43,7 +42,7 @@ let embeddable: ContactCardEmbeddable; let coreStart: CoreStart; beforeEach(async () => { coreStart = coreMock.createStart(); - const options: DashboardOptions = { + const options = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, application: {} as any, diff --git a/src/plugins/dashboard/public/actions/replace_panel_action.tsx b/src/plugins/dashboard/public/actions/replace_panel_action.tsx index 26d9c5c8ad4dd9..21ec961917d170 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_action.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_action.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from '../../../../core/public'; -import { IEmbeddable, ViewMode, IEmbeddableStart } from '../embeddable_plugin'; +import { IEmbeddable, ViewMode, EmbeddableStart } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; import { ActionByType, IncompatibleActionError } from '../ui_actions_plugin'; import { openReplacePanelFlyout } from './open_replace_panel_flyout'; @@ -43,7 +43,7 @@ export class ReplacePanelAction implements ActionByType, private notifications: CoreStart['notifications'], - private getEmbeddableFactories: IEmbeddableStart['getEmbeddableFactories'] + private getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories'] ) {} public getDisplayName({ embeddable }: ReplacePanelActionContext) { diff --git a/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx index 670105650f95aa..a1cd865f771d45 100644 --- a/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/actions/replace_panel_flyout.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; -import { GetEmbeddableFactories } from 'src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { DashboardPanelState } from '../embeddable'; import { NotificationsStart, Toast } from '../../../../core/public'; import { IContainer, IEmbeddable, EmbeddableInput, EmbeddableOutput } from '../embeddable_plugin'; @@ -31,7 +31,7 @@ interface Props { onClose: () => void; notifications: NotificationsStart; panelToRemove: IEmbeddable; - getEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; } export class ReplacePanelFlyout extends React.Component { diff --git a/src/plugins/dashboard/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/embeddable/dashboard_container.tsx index f9443ab97416da..86a6e374d3e255 100644 --- a/src/plugins/dashboard/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/embeddable/dashboard_container.tsx @@ -30,7 +30,7 @@ import { ViewMode, EmbeddableFactory, IEmbeddable, - IEmbeddableStart, + EmbeddableStart, } from '../embeddable_plugin'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; import { createPanelState } from './panel'; @@ -77,7 +77,7 @@ export interface DashboardContainerOptions { application: CoreStart['application']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; ExitFullScreenButton: React.ComponentType; diff --git a/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx b/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx index a358e41f7b5074..0fa62fc8756037 100644 --- a/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx +++ b/src/plugins/dashboard/public/embeddable/dashboard_container_factory.tsx @@ -18,24 +18,29 @@ */ import { i18n } from '@kbn/i18n'; -import { SavedObjectMetaData } from '../../../saved_objects/public'; -import { SavedObjectAttributes } from '../../../../core/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { CoreStart } from '../../../../core/public'; import { ContainerOutput, EmbeddableFactory, ErrorEmbeddable, Container, } from '../embeddable_plugin'; -import { - DashboardContainer, - DashboardContainerInput, - DashboardContainerOptions, -} from './dashboard_container'; -import { DashboardCapabilities } from '../types'; +import { DashboardContainer, DashboardContainerInput } from './dashboard_container'; import { DASHBOARD_CONTAINER_TYPE } from './dashboard_constants'; +import { Start as InspectorStartContract } from '../../../inspector/public'; -export interface DashboardOptions extends DashboardContainerOptions { - savedObjectMetaData?: SavedObjectMetaData; +interface StartServices { + capabilities: CoreStart['application']['capabilities']; + application: CoreStart['application']; + overlays: CoreStart['overlays']; + notifications: CoreStart['notifications']; + embeddable: EmbeddableStart; + inspector: InspectorStartContract; + SavedObjectFinder: React.ComponentType; + ExitFullScreenButton: React.ComponentType; + uiActions: UiActionsStart; } export class DashboardContainerFactory extends EmbeddableFactory< @@ -45,23 +50,13 @@ export class DashboardContainerFactory extends EmbeddableFactory< public readonly isContainerType = true; public readonly type = DASHBOARD_CONTAINER_TYPE; - private readonly allowEditing: boolean; - - constructor(private readonly options: DashboardOptions) { - super({ savedObjectMetaData: options.savedObjectMetaData }); - - const capabilities = (options.application.capabilities - .dashboard as unknown) as DashboardCapabilities; - - if (!capabilities || typeof capabilities !== 'object') { - throw new TypeError('Dashboard capabilities not found.'); - } - - this.allowEditing = !!capabilities.createNew && !!capabilities.showWriteControls; + constructor(private readonly getStartServices: () => Promise) { + super(); } - public isEditable() { - return this.allowEditing; + public async isEditable() { + const { capabilities } = await this.getStartServices(); + return !!capabilities.createNew && !!capabilities.showWriteControls; } public getDisplayName() { @@ -82,6 +77,7 @@ export class DashboardContainerFactory extends EmbeddableFactory< initialInput: DashboardContainerInput, parent?: Container ): Promise { - return new DashboardContainer(initialInput, this.options, parent); + const services = await this.getStartServices(); + return new DashboardContainer(initialInput, services, parent); } } diff --git a/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx index c1a3d88979f490..0f1b9c6dc93072 100644 --- a/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/embeddable/grid/dashboard_grid.test.tsx @@ -23,7 +23,7 @@ import sizeMe from 'react-sizeme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { skip } from 'rxjs/operators'; -import { EmbeddableFactory, GetEmbeddableFactory } from '../../embeddable_plugin'; +import { EmbeddableFactory } from '../../embeddable_plugin'; import { DashboardGrid, DashboardGridProps } from './dashboard_grid'; import { DashboardContainer, DashboardContainerOptions } from '../dashboard_container'; import { getSampleDashboardInput } from '../../test_helpers'; @@ -41,7 +41,7 @@ function prepare(props?: Partial) { CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory({} as any, (() => {}) as any, {} as any) ); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const initialInput = getSampleDashboardInput({ panels: { '1': { diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 6f78829af19f1c..8a6e747aac1705 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -23,7 +23,7 @@ import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { SharePluginSetup } from 'src/plugins/share/public'; import { UiActionsSetup, UiActionsStart } from '../../../plugins/ui_actions/public'; -import { CONTEXT_MENU_TRIGGER, IEmbeddableSetup, IEmbeddableStart } from './embeddable_plugin'; +import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from './embeddable_plugin'; import { ExpandPanelAction, ReplacePanelAction } from '.'; import { DashboardContainerFactory } from './embeddable/dashboard_container_factory'; import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; @@ -47,13 +47,13 @@ declare module '../../share/public' { } interface SetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; uiActions: UiActionsSetup; share?: SharePluginSetup; } interface StartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStartContract; uiActions: UiActionsStart; } @@ -72,7 +72,10 @@ export class DashboardEmbeddableContainerPublicPlugin implements Plugin { constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { share, uiActions }: SetupDependencies): Setup { + public setup( + core: CoreSetup, + { share, uiActions, embeddable }: SetupDependencies + ): Setup { const expandPanelAction = new ExpandPanelAction(); uiActions.registerAction(expandPanelAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, expandPanelAction); @@ -86,26 +89,44 @@ export class DashboardEmbeddableContainerPublicPlugin })) ); } + + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + + const useHideChrome = () => { + React.useEffect(() => { + coreStart.chrome.setIsVisible(false); + return () => coreStart.chrome.setIsVisible(true); + }, []); + }; + + const ExitFullScreenButton: React.FC = props => { + useHideChrome(); + return ; + }; + return { + capabilities: coreStart.application.capabilities, + application: coreStart.application, + notifications: coreStart.notifications, + overlays: coreStart.overlays, + embeddable: deps.embeddable, + inspector: deps.inspector, + SavedObjectFinder: getSavedObjectFinder(coreStart.savedObjects, coreStart.uiSettings), + ExitFullScreenButton, + uiActions: deps.uiActions, + }; + }; + + const factory = new DashboardContainerFactory(getStartServices); + embeddable.registerEmbeddableFactory(factory.type, factory); } public start(core: CoreStart, plugins: StartDependencies): Start { - const { application, notifications, overlays } = core; - const { embeddable, inspector, uiActions } = plugins; + const { notifications } = core; + const { uiActions } = plugins; const SavedObjectFinder = getSavedObjectFinder(core.savedObjects, core.uiSettings); - const useHideChrome = () => { - React.useEffect(() => { - core.chrome.setIsVisible(false); - return () => core.chrome.setIsVisible(true); - }, []); - }; - - const ExitFullScreenButton: React.FC = props => { - useHideChrome(); - return ; - }; - const changeViewAction = new ReplacePanelAction( core, SavedObjectFinder, @@ -114,19 +135,6 @@ export class DashboardEmbeddableContainerPublicPlugin ); uiActions.registerAction(changeViewAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, changeViewAction); - - const factory = new DashboardContainerFactory({ - application, - notifications, - overlays, - embeddable, - inspector, - SavedObjectFinder, - ExitFullScreenButton, - uiActions, - }); - - embeddable.registerEmbeddableFactory(factory.type, factory); } public stop() {} diff --git a/src/plugins/embeddable/public/api/get_embeddable_factories.ts b/src/plugins/embeddable/public/api/get_embeddable_factories.ts deleted file mode 100644 index c12d1283905f5a..00000000000000 --- a/src/plugins/embeddable/public/api/get_embeddable_factories.ts +++ /dev/null @@ -1,26 +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 { EmbeddableApiPure } from './types'; - -export const getEmbeddableFactories: EmbeddableApiPure['getEmbeddableFactories'] = ({ - embeddableFactories, -}) => () => { - return embeddableFactories.values(); -}; diff --git a/src/plugins/embeddable/public/api/get_embeddable_factory.ts b/src/plugins/embeddable/public/api/get_embeddable_factory.ts deleted file mode 100644 index 8e98da287c5ea6..00000000000000 --- a/src/plugins/embeddable/public/api/get_embeddable_factory.ts +++ /dev/null @@ -1,34 +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 { EmbeddableApiPure } from './types'; - -export const getEmbeddableFactory: EmbeddableApiPure['getEmbeddableFactory'] = ({ - embeddableFactories, -}) => embeddableFactoryId => { - const factory = embeddableFactories.get(embeddableFactoryId); - - if (!factory) { - throw new Error( - `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] does not exist.` - ); - } - - return factory; -}; diff --git a/src/plugins/embeddable/public/api/index.ts b/src/plugins/embeddable/public/api/index.ts deleted file mode 100644 index aec539330de9af..00000000000000 --- a/src/plugins/embeddable/public/api/index.ts +++ /dev/null @@ -1,47 +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 { - EmbeddableApiPure, - EmbeddableDependencies, - EmbeddableApi, - EmbeddableDependenciesInternal, -} from './types'; -import { getEmbeddableFactories } from './get_embeddable_factories'; -import { getEmbeddableFactory } from './get_embeddable_factory'; -import { registerEmbeddableFactory } from './register_embeddable_factory'; - -export * from './types'; - -export const pureApi: EmbeddableApiPure = { - getEmbeddableFactories, - getEmbeddableFactory, - registerEmbeddableFactory, -}; - -export const createApi = (deps: EmbeddableDependencies) => { - const partialApi: Partial = {}; - const depsInternal: EmbeddableDependenciesInternal = { ...deps, api: partialApi }; - for (const [key, fn] of Object.entries(pureApi)) { - (partialApi as any)[key] = fn(depsInternal); - } - Object.freeze(partialApi); - const api = partialApi as EmbeddableApi; - return { api, depsInternal }; -}; diff --git a/src/plugins/embeddable/public/api/register_embeddable_factory.ts b/src/plugins/embeddable/public/api/register_embeddable_factory.ts deleted file mode 100644 index 8b7bcdee5911fe..00000000000000 --- a/src/plugins/embeddable/public/api/register_embeddable_factory.ts +++ /dev/null @@ -1,32 +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 { EmbeddableApiPure } from './types'; - -export const registerEmbeddableFactory: EmbeddableApiPure['registerEmbeddableFactory'] = ({ - embeddableFactories, -}) => (embeddableFactoryId, factory) => { - if (embeddableFactories.has(embeddableFactoryId)) { - throw new Error( - `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.` - ); - } - - embeddableFactories.set(embeddableFactoryId, factory); -}; diff --git a/src/plugins/embeddable/public/api/tests/helpers.ts b/src/plugins/embeddable/public/api/tests/helpers.ts deleted file mode 100644 index be8e9a0dec3c27..00000000000000 --- a/src/plugins/embeddable/public/api/tests/helpers.ts +++ /dev/null @@ -1,27 +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 { EmbeddableDependencies } from '../types'; - -export const createDeps = (): EmbeddableDependencies => { - const deps: EmbeddableDependencies = { - embeddableFactories: new Map(), - }; - return deps; -}; diff --git a/src/plugins/embeddable/public/api/types.ts b/src/plugins/embeddable/public/api/types.ts deleted file mode 100644 index 179d96a4aff8cf..00000000000000 --- a/src/plugins/embeddable/public/api/types.ts +++ /dev/null @@ -1,43 +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 { EmbeddableFactoryRegistry } from '../types'; -import { EmbeddableFactory, GetEmbeddableFactories } from '../lib'; - -export interface EmbeddableApi { - getEmbeddableFactory: (embeddableFactoryId: string) => EmbeddableFactory; - getEmbeddableFactories: GetEmbeddableFactories; - // TODO: Make `registerEmbeddableFactory` receive only `factory` argument. - registerEmbeddableFactory: ( - id: string, - factory: TEmbeddableFactory - ) => void; -} - -export interface EmbeddableDependencies { - embeddableFactories: EmbeddableFactoryRegistry; -} - -export interface EmbeddableDependenciesInternal extends EmbeddableDependencies { - api: Readonly>; -} - -export type EmbeddableApiPure = { - [K in keyof EmbeddableApi]: (deps: EmbeddableDependenciesInternal) => EmbeddableApi[K]; -}; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 1474f9ed630525..eca74af4ec2530 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -48,8 +48,6 @@ export { EmbeddableRoot, EmbeddableVisTriggerContext, ErrorEmbeddable, - GetEmbeddableFactories, - GetEmbeddableFactory, IContainer, IEmbeddable, isErrorEmbeddable, @@ -68,4 +66,4 @@ export function plugin(initializerContext: PluginInitializerContext) { return new EmbeddablePublicPlugin(initializerContext); } -export { IEmbeddableSetup, IEmbeddableStart } from './plugin'; +export { EmbeddableSetup, EmbeddableStart } from './plugin'; diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 142a237a6a311e..9aeaf34f3311b8 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -19,11 +19,13 @@ import { EditPanelAction } from './edit_panel_action'; import { EmbeddableFactory, Embeddable, EmbeddableInput } from '../embeddables'; -import { GetEmbeddableFactory, ViewMode } from '../types'; +import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples'; +import { EmbeddableStart } from '../../plugin'; const embeddableFactories = new Map(); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = ((id: string) => + embeddableFactories.get(id)) as EmbeddableStart['getEmbeddableFactory']; class EditableEmbeddable extends Embeddable { public readonly type = 'EDITABLE_EMBEDDABLE'; @@ -82,7 +84,8 @@ test('is not compatible when edit url is not available', async () => { test('is not visible when edit url is available but in view mode', async () => { embeddableFactories.clear(); - const action = new EditPanelAction(type => embeddableFactories.get(type)); + const action = new EditPanelAction((type => + embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( @@ -98,7 +101,8 @@ test('is not visible when edit url is available but in view mode', async () => { test('is not compatible when edit url is available, in edit mode, but not editable', async () => { embeddableFactories.clear(); - const action = new EditPanelAction(type => embeddableFactories.get(type)); + const action = new EditPanelAction((type => + embeddableFactories.get(type)) as EmbeddableStart['getEmbeddableFactory']); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 82f8e33b7ae2f1..9125dc0813f986 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -19,9 +19,10 @@ import { i18n } from '@kbn/i18n'; import { Action } from 'src/plugins/ui_actions/public'; -import { GetEmbeddableFactory, ViewMode } from '../types'; +import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; import { IEmbeddable } from '../embeddables'; +import { EmbeddableStart } from '../../plugin'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -34,7 +35,7 @@ export class EditPanelAction implements Action { public readonly id = ACTION_EDIT_PANEL; public order = 15; - constructor(private readonly getEmbeddableFactory: GetEmbeddableFactory) {} + constructor(private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']) {} public getDisplayName({ embeddable }: ActionContext) { const factory = this.getEmbeddableFactory(embeddable.type); diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 71e7cca3552bbf..5ce79537ccaf3d 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -29,7 +29,7 @@ import { } from '../embeddables'; import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; -import { GetEmbeddableFactory } from '../types'; +import { EmbeddableStart } from '../../plugin'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -49,7 +49,7 @@ export abstract class Container< constructor( input: TContainerInput, output: TContainerOutput, - protected readonly getFactory: GetEmbeddableFactory, + protected readonly getFactory: EmbeddableStart['getEmbeddableFactory'], parent?: Container ) { super(input, output, parent); diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx index 3c9e6e31220b22..07915ce59e6ca2 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { nextTick } from 'test_utils/enzyme_helpers'; import { EmbeddableChildPanel } from './embeddable_child_panel'; -import { GetEmbeddableFactory } from '../types'; import { EmbeddableFactory } from '../embeddables'; import { CONTACT_CARD_EMBEDDABLE } from '../test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { SlowContactCardEmbeddableFactory } from '../test_samples/embeddables/contact_card/slow_contact_card_embeddable_factory'; @@ -42,7 +41,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async CONTACT_CARD_EMBEDDABLE, new SlowContactCardEmbeddableFactory({ execAction: (() => null) as any }) ); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const container = new HelloWorldContainer({ id: 'hello', panels: {} }, { getEmbeddableFactory, @@ -88,7 +87,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async test(`EmbeddableChildPanel renders an error message if the factory doesn't exist`, async () => { const inspector = inspectorPluginMock.createStartContract(); - const getEmbeddableFactory: GetEmbeddableFactory = () => undefined; + const getEmbeddableFactory = () => undefined; const container = new HelloWorldContainer( { id: 'hello', diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx index e15f1faaa397c2..4c08a80a356bff 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx @@ -29,15 +29,15 @@ import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { ErrorEmbeddable, IEmbeddable } from '../embeddables'; import { EmbeddablePanel } from '../panel'; import { IContainer } from './i_container'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../types'; +import { EmbeddableStart } from '../../plugin'; export interface EmbeddableChildPanelProps { embeddableId: string; className?: string; container: IContainer; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 162da75c228aa5..81f7f35c900c98 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -74,13 +74,11 @@ export abstract class EmbeddableFactory< this.savedObjectMetaData = savedObjectMetaData; } - // TODO: Can this be a property? If this "...should be based of capabilities service...", - // TODO: maybe then it should be *async*? /** * Returns whether the current user should be allowed to edit this type of - * embeddable. Most of the time this should be based off the capabilities service. + * embeddable. Most of the time this should be based off the capabilities service, hence it's async. */ - public abstract isEditable(): boolean; + public abstract async isEditable(): Promise; /** * Returns a display name for this type of embeddable. Used in "Create new... " options diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx index 7c3a1c6ca45c41..51b83ea0ecaa34 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory_renderer.test.tsx @@ -22,21 +22,21 @@ import { HelloWorldEmbeddableFactory, } from '../../../../../../examples/embeddable_examples/public'; import { EmbeddableFactory } from './embeddable_factory'; -import { GetEmbeddableFactory } from '../types'; import { EmbeddableFactoryRenderer } from './embeddable_factory_renderer'; import { mount } from 'enzyme'; import { nextTick } from 'test_utils/enzyme_helpers'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddableStart } from '../../plugin'; test('EmbeddableFactoryRenderer renders an embeddable', async () => { const embeddableFactories = new Map(); embeddableFactories.set(HELLO_WORLD_EMBEDDABLE, new HelloWorldEmbeddableFactory()); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const component = mount( @@ -54,7 +54,7 @@ test('EmbeddableFactoryRenderer renders an embeddable', async () => { }); test('EmbeddableRoot renders an error if the type does not exist', async () => { - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => undefined; + const getEmbeddableFactory = (id: string) => undefined; const component = mount( >(); const triggerRegistry = new Map(); const embeddableFactories = new Map(); -const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); const editModeAction = createEditModeAction(); const trigger: Trigger = { diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 28474544f40b57..b95060a73252f0 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -27,7 +27,7 @@ import { toMountPoint } from '../../../../kibana_react/public'; import { Start as InspectorStartContract } from '../inspector'; import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, EmbeddableContext } from '../triggers'; import { IEmbeddable } from '../embeddables/i_embeddable'; -import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../types'; +import { ViewMode } from '../types'; import { RemovePanelAction } from './panel_header/panel_actions'; import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel_action'; @@ -36,12 +36,13 @@ import { PanelHeader } from './panel_header/panel_header'; import { InspectPanelAction } from './panel_header/panel_actions/inspect_panel_action'; import { EditPanelAction } from '../actions'; import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal'; +import { EmbeddableStart } from '../../plugin'; interface Props { embeddable: IEmbeddable; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx index 028d6a530236a6..8ee8c8dad9df36 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.test.tsx @@ -27,15 +27,15 @@ import { } from '../../../../test_samples/embeddables/filterable_embeddable'; import { FilterableEmbeddableFactory } from '../../../../test_samples/embeddables/filterable_embeddable_factory'; import { FilterableContainer } from '../../../../test_samples/embeddables/filterable_container'; -import { GetEmbeddableFactory } from '../../../../types'; // eslint-disable-next-line import { coreMock } from '../../../../../../../../core/public/mocks'; import { ContactCardEmbeddable } from '../../../../test_samples'; import { esFilters, Filter } from '../../../../../../../../plugins/data/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = (id: string) => embeddableFactories.get(id); let container: FilterableContainer; let embeddable: FilterableEmbeddable; @@ -58,7 +58,7 @@ beforeEach(async () => { }; container = new FilterableContainer( { id: 'hello', panels: {}, filters: [derivedFilter] }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const filterableEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index 36bb742040ccc1..f3a483bb4bda4b 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -19,7 +19,8 @@ import { i18n } from '@kbn/i18n'; import { Action } from 'src/plugins/ui_actions/public'; import { NotificationsStart, OverlayStart } from 'src/core/public'; -import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; +import { ViewMode } from '../../../../types'; import { openAddPanelFlyout } from './open_add_panel_flyout'; import { IContainer } from '../../../../containers'; @@ -34,8 +35,8 @@ export class AddPanelAction implements Action { public readonly id = ACTION_ADD_PANEL; constructor( - private readonly getFactory: GetEmbeddableFactory, - private readonly getAllFactories: GetEmbeddableFactories, + private readonly getFactory: EmbeddableStart['getEmbeddableFactory'], + private readonly getAllFactories: EmbeddableStart['getEmbeddableFactories'], private readonly overlays: OverlayStart, private readonly notifications: NotificationsStart, private readonly SavedObjectFinder: React.ComponentType diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx index 5f06e4ec447873..2fa21e40ca0f07 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.test.tsx @@ -19,7 +19,6 @@ import * as React from 'react'; import { AddPanelFlyout } from './add_panel_flyout'; -import { GetEmbeddableFactory } from '../../../../types'; import { ContactCardEmbeddableFactory, CONTACT_CARD_EMBEDDABLE, @@ -32,6 +31,7 @@ import { ReactWrapper } from 'enzyme'; import { coreMock } from '../../../../../../../../core/public/mocks'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; function DummySavedObjectFinder(props: { children: React.ReactNode }) { return ( @@ -55,7 +55,7 @@ test('createNewEmbeddable() add embeddable to container', async () => { firstName: 'foo', lastName: 'bar', } as any); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; + const getEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; const input: ContainerInput<{ firstName: string; lastName: string }> = { id: '1', panels: {}, @@ -66,7 +66,7 @@ test('createNewEmbeddable() add embeddable to container', async () => { new Set([contactCardEmbeddableFactory]).values()} notifications={core.notifications} SavedObjectFinder={() => null} @@ -100,7 +100,8 @@ test('selecting embeddable in "Create new ..." list calls createNewEmbeddable()' firstName: 'foo', lastName: 'bar', } as any); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => contactCardEmbeddableFactory; + const getEmbeddableFactory = ((id: string) => + contactCardEmbeddableFactory) as EmbeddableStart['getEmbeddableFactory']; const input: ContainerInput<{ firstName: string; lastName: string }> = { id: '1', panels: {}, diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index 815394ebd97e05..95eeb63710c32e 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -29,16 +29,16 @@ import { EuiTitle, } from '@elastic/eui'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; import { IContainer } from '../../../../containers'; import { EmbeddableFactoryNotFoundError } from '../../../../errors'; -import { GetEmbeddableFactories, GetEmbeddableFactory } from '../../../../types'; import { SavedObjectFinderCreateNew } from './saved_object_finder_create_new'; interface Props { onClose: () => void; container: IContainer; - getFactory: GetEmbeddableFactory; - getAllFactories: GetEmbeddableFactories; + getFactory: EmbeddableStart['getEmbeddableFactory']; + getAllFactories: EmbeddableStart['getEmbeddableFactories']; notifications: CoreSetup['notifications']; SavedObjectFinder: React.ComponentType; } diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index 481693501066c9..a452e07b515771 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -18,15 +18,15 @@ */ import React from 'react'; import { NotificationsStart, OverlayStart } from 'src/core/public'; +import { EmbeddableStart } from '../../../../../plugin'; import { toMountPoint } from '../../../../../../../kibana_react/public'; import { IContainer } from '../../../../containers'; import { AddPanelFlyout } from './add_panel_flyout'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types'; export async function openAddPanelFlyout(options: { embeddable: IContainer; - getFactory: GetEmbeddableFactory; - getAllFactories: GetEmbeddableFactories; + getFactory: EmbeddableStart['getEmbeddableFactory']; + getAllFactories: EmbeddableStart['getEmbeddableFactories']; overlays: OverlayStart; notifications: NotificationsStart; SavedObjectFinder: React.ComponentType; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts index 4ba63bb025a87a..3f7c917cd16173 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.test.ts @@ -32,7 +32,6 @@ import { ContactCardEmbeddableFactory, } from '../../../../test_samples/embeddables/contact_card/contact_card_embeddable_factory'; import { HelloWorldContainer } from '../../../../test_samples/embeddables/hello_world_container'; -import { GetEmbeddableFactory } from '../../../../types'; import { EmbeddableFactory } from '../../../../embeddables'; let container: Container; @@ -40,7 +39,7 @@ let embeddable: ContactCardEmbeddable; function createHelloWorldContainer(input = { id: '123', panels: {} }) { const embeddableFactories = new Map(); - const getEmbeddableFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getEmbeddableFactory = (id: string) => embeddableFactories.get(id); embeddableFactories.set( CONTACT_CARD_EMBEDDABLE, new ContactCardEmbeddableFactory({}, (() => {}) as any, {} as any) diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx index 8d9beec940acc0..e19acda8419da7 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx @@ -34,14 +34,14 @@ import { isErrorEmbeddable, ErrorEmbeddable, } from '../../../embeddables'; -import { GetEmbeddableFactory } from '../../../types'; import { of } from '../../../../tests/helpers'; import { esFilters } from '../../../../../../../plugins/data/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public/plugin'; const setup = async () => { const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); - const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); + const getFactory = (id: string) => embeddableFactories.get(id); const container = new FilterableContainer( { id: 'hello', @@ -54,7 +54,7 @@ const setup = async () => { }, ], }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const embeddable: FilterableEmbeddable | ErrorEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx index be096a4cc60cef..f4d5aa148373bd 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx @@ -20,6 +20,7 @@ import { EmbeddableOutput, isErrorEmbeddable } from '../../../'; import { RemovePanelAction } from './remove_panel_action'; import { EmbeddableFactory } from '../../../embeddables'; +import { EmbeddableStart } from '../../../../plugin'; import { FILTERABLE_EMBEDDABLE, FilterableEmbeddable, @@ -27,13 +28,13 @@ import { } from '../../../test_samples/embeddables/filterable_embeddable'; import { FilterableEmbeddableFactory } from '../../../test_samples/embeddables/filterable_embeddable_factory'; import { FilterableContainer } from '../../../test_samples/embeddables/filterable_container'; -import { GetEmbeddableFactory, ViewMode } from '../../../types'; +import { ViewMode } from '../../../types'; import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable'; import { esFilters, Filter } from '../../../../../../../plugins/data/public'; const embeddableFactories = new Map(); embeddableFactories.set(FILTERABLE_EMBEDDABLE, new FilterableEmbeddableFactory()); -const getFactory: GetEmbeddableFactory = (id: string) => embeddableFactories.get(id); +const getFactory = (id: string) => embeddableFactories.get(id); let container: FilterableContainer; let embeddable: FilterableEmbeddable; @@ -46,7 +47,7 @@ beforeEach(async () => { }; container = new FilterableContainer( { id: 'hello', panels: {}, filters: [derivedFilter], viewMode: ViewMode.EDIT }, - getFactory + getFactory as EmbeddableStart['getEmbeddableFactory'] ); const filterableEmbeddable = await container.addNewEmbeddable< diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx index 7a9ba4fbbf6d62..20a5a8112f4d36 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/contact_card/contact_card_embeddable_factory.tsx @@ -42,7 +42,7 @@ export class ContactCardEmbeddableFactory extends EmbeddableFactory { public readonly type = FILTERABLE_CONTAINER; constructor( - private readonly getFactory: GetEmbeddableFactory, + private readonly getFactory: EmbeddableStart['getEmbeddableFactory'], options: EmbeddableFactoryOptions = {} ) { super(options); @@ -43,7 +43,7 @@ export class FilterableContainerFactory extends EmbeddableFactory { public readonly type = FILTERABLE_EMBEDDABLE; - public isEditable() { + public async isEditable() { return true; } diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index c5ba054bebb7ac..a88c3ba0863259 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -24,7 +24,7 @@ import { UiActionsService } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { Container, ViewMode, ContainerInput } from '../..'; import { HelloWorldContainerComponent } from './hello_world_container_component'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types'; +import { EmbeddableStart } from '../../../plugin'; export const HELLO_WORLD_CONTAINER = 'HELLO_WORLD_CONTAINER'; @@ -46,8 +46,8 @@ interface HelloWorldContainerInput extends ContainerInput { interface HelloWorldContainerOptions { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx index e9acfd45397683..e8c1464edab384 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container_component.tsx @@ -24,13 +24,13 @@ import { CoreStart } from 'src/core/public'; import { UiActionsService } from 'src/plugins/ui_actions/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { IContainer, PanelState, EmbeddableChildPanel } from '../..'; -import { GetEmbeddableFactory, GetEmbeddableFactories } from '../../types'; +import { EmbeddableStart } from '../../../plugin'; interface Props { container: IContainer; getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/src/plugins/embeddable/public/lib/types.ts b/src/plugins/embeddable/public/lib/types.ts index 68ea5bc17f7c97..1cfff7baca1862 100644 --- a/src/plugins/embeddable/public/lib/types.ts +++ b/src/plugins/embeddable/public/lib/types.ts @@ -18,7 +18,6 @@ */ import { Adapters } from './inspector'; -import { EmbeddableFactory } from './embeddables/embeddable_factory'; export interface Trigger { id: string; @@ -40,6 +39,3 @@ export enum ViewMode { } export { Adapters }; - -export type GetEmbeddableFactory = (id: string) => EmbeddableFactory | undefined; -export type GetEmbeddableFactories = () => IterableIterator; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index fd299bc626fb9e..ba2f78e42e10eb 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -17,15 +17,15 @@ * under the License. */ -import { IEmbeddableStart, IEmbeddableSetup } from '.'; +import { EmbeddableStart, EmbeddableSetup } from '.'; import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; // eslint-disable-next-line import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; -export type Setup = jest.Mocked; -export type Start = jest.Mocked; +export type Setup = jest.Mocked; +export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { @@ -36,7 +36,6 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { - registerEmbeddableFactory: jest.fn(), getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), }; diff --git a/src/plugins/embeddable/public/api/tests/registry.test.ts b/src/plugins/embeddable/public/plugin.test.ts similarity index 70% rename from src/plugins/embeddable/public/api/tests/registry.test.ts rename to src/plugins/embeddable/public/plugin.test.ts index 30a8a71d243f95..c334411004e2c1 100644 --- a/src/plugins/embeddable/public/api/tests/registry.test.ts +++ b/src/plugins/embeddable/public/plugin.test.ts @@ -16,18 +16,20 @@ * specific language governing permissions and limitations * under the License. */ - -import { createApi } from '..'; -import { createDeps } from './helpers'; +import { coreMock } from '../../../core/public/mocks'; +import { testPlugin } from './tests/test_plugin'; test('cannot register embeddable factory with the same ID', async () => { - const deps = createDeps(); - const { api } = createApi(deps); + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const { setup } = testPlugin(coreSetup, coreStart); const embeddableFactoryId = 'ID'; const embeddableFactory = {} as any; - api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory); - expect(() => api.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory)).toThrowError( + setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory); + expect(() => + setup.registerEmbeddableFactory(embeddableFactoryId, embeddableFactory) + ).toThrowError( 'Embeddable factory [embeddableFactoryId = ID] already registered in Embeddables API.' ); }); diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.ts index c84fb888412e13..381665c359ffd2 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.ts @@ -16,45 +16,78 @@ * specific language governing permissions and limitations * under the License. */ - import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { EmbeddableFactoryRegistry } from './types'; -import { createApi, EmbeddableApi } from './api'; import { bootstrap } from './bootstrap'; +import { EmbeddableFactory, EmbeddableInput, EmbeddableOutput } from './lib'; -export interface IEmbeddableSetupDependencies { +export interface EmbeddableSetupDependencies { uiActions: UiActionsSetup; } -export interface IEmbeddableSetup { - registerEmbeddableFactory: EmbeddableApi['registerEmbeddableFactory']; +export interface EmbeddableSetup { + registerEmbeddableFactory: ( + id: string, + factory: EmbeddableFactory + ) => void; +} +export interface EmbeddableStart { + getEmbeddableFactory: < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput + >( + embeddableFactoryId: string + ) => EmbeddableFactory | undefined; + getEmbeddableFactories: () => IterableIterator; } -export type IEmbeddableStart = EmbeddableApi; - -export class EmbeddablePublicPlugin implements Plugin { +export class EmbeddablePublicPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); - private api!: EmbeddableApi; constructor(initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, { uiActions }: IEmbeddableSetupDependencies) { - ({ api: this.api } = createApi({ - embeddableFactories: this.embeddableFactories, - })); + public setup(core: CoreSetup, { uiActions }: EmbeddableSetupDependencies) { bootstrap(uiActions); - const { registerEmbeddableFactory } = this.api; - return { - registerEmbeddableFactory, + registerEmbeddableFactory: this.registerEmbeddableFactory, }; } public start(core: CoreStart) { - return this.api; + return { + getEmbeddableFactory: this.getEmbeddableFactory, + getEmbeddableFactories: () => this.embeddableFactories.values(), + }; } public stop() {} + + private registerEmbeddableFactory = (embeddableFactoryId: string, factory: EmbeddableFactory) => { + if (this.embeddableFactories.has(embeddableFactoryId)) { + throw new Error( + `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] already registered in Embeddables API.` + ); + } + + this.embeddableFactories.set(embeddableFactoryId, factory); + }; + + private getEmbeddableFactory = < + I extends EmbeddableInput = EmbeddableInput, + O extends EmbeddableOutput = EmbeddableOutput + >( + embeddableFactoryId: string + ) => { + const factory = this.embeddableFactories.get(embeddableFactoryId); + + if (!factory) { + throw new Error( + `Embeddable factory [embeddableFactoryId = ${embeddableFactoryId}] does not exist.` + ); + } + + return factory as EmbeddableFactory; + }; } diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index 0721acb1a1fba9..6beef35bbe1368 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -35,14 +35,14 @@ import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; import { esFilters } from '../../../../plugins/data/public'; test('ApplyFilterAction applies the filter to the root of the container tree', async () => { - const { doStart } = testPlugin(); + const { doStart, setup } = testPlugin(); const api = doStart(); const factory1 = new FilterableContainerFactory(api.getEmbeddableFactory); const factory2 = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory1.type, factory1); - api.registerEmbeddableFactory(factory2.type, factory2); + setup.registerEmbeddableFactory(factory1.type, factory1); + setup.registerEmbeddableFactory(factory2.type, factory2); const applyFilterAction = createFilterAction(); @@ -93,7 +93,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a }); test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { - const { doStart, coreStart } = testPlugin(); + const { doStart, coreStart, setup } = testPlugin(); const api = doStart(); const inspector = inspectorPluginMock.createStartContract(); @@ -112,7 +112,7 @@ test('ApplyFilterAction is incompatible if the root container does not accept a ); const factory = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const embeddable = await parent.addNewEmbeddable< FilterableContainerInput, @@ -129,12 +129,12 @@ test('ApplyFilterAction is incompatible if the root container does not accept a }); test('trying to execute on incompatible context throws an error ', async () => { - const { doStart, coreStart } = testPlugin(); + const { doStart, coreStart, setup } = testPlugin(); const api = doStart(); const inspector = inspectorPluginMock.createStartContract(); const factory = new FilterableEmbeddableFactory(); - api.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const applyFilterAction = createFilterAction(); const parent = new HelloWorldContainer( diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index be19ac206999d3..1ee52f47491352 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -562,7 +562,7 @@ test('Panel added to input state', async () => { test('Container changes made directly after adding a new embeddable are propagated', async done => { const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); - const { doStart, uiActions } = testPlugin(coreSetup, coreStart); + const { setup, doStart, uiActions } = testPlugin(coreSetup, coreStart); const start = doStart(); const container = new HelloWorldContainer( @@ -586,7 +586,7 @@ test('Container changes made directly after adding a new embeddable are propagat loadTickCount: 3, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const subscription = Rx.merge(container.getOutput$(), container.getInput$()) .pipe(skip(2)) @@ -755,7 +755,7 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy }); test('untilEmbeddableLoaded resolves with undefined if child is subsequently removed', async done => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, setup, coreStart, uiActions } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -764,7 +764,7 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem loadTickCount: 3, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const container = new HelloWorldContainer( { id: 'hello', @@ -795,7 +795,7 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem }); test('adding a panel then subsequently removing it before its loaded removes the panel', async done => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, coreStart, uiActions, setup } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -804,7 +804,7 @@ test('adding a panel then subsequently removing it before its loaded removes the loadTickCount: 1, execAction: uiActions.executeTriggerActions, }); - start.registerEmbeddableFactory(factory.type, factory); + setup.registerEmbeddableFactory(factory.type, factory); const container = new HelloWorldContainer( { id: 'hello', diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx index 70d7c99d3fb9d0..99d5a7c747d15b 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -34,16 +34,16 @@ import { HelloWorldContainer } from '../lib/test_samples/embeddables/hello_world // eslint-disable-next-line import { coreMock } from '../../../../core/public/mocks'; import { testPlugin } from './test_plugin'; -import { EmbeddableApi } from '../api'; import { CustomizePanelModal } from '../lib/panel/panel_header/panel_actions/customize_title/customize_panel_modal'; import { mount } from 'enzyme'; +import { EmbeddableStart } from '../plugin'; -let api: EmbeddableApi; +let api: EmbeddableStart; let container: Container; let embeddable: ContactCardEmbeddable; beforeEach(async () => { - const { doStart, coreStart, uiActions } = testPlugin( + const { doStart, coreStart, uiActions, setup } = testPlugin( coreMock.createSetup(), coreMock.createStart() ); @@ -54,7 +54,7 @@ beforeEach(async () => { uiActions.executeTriggerActions, {} as any ); - api.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); + setup.registerEmbeddableFactory(contactCardFactory.type, contactCardFactory); container = new HelloWorldContainer( { id: '123', panels: {} }, diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index 1edc3327803365..e199ef193aa1cc 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -22,14 +22,14 @@ import { CoreSetup, CoreStart } from 'src/core/public'; import { uiActionsPluginMock } from 'src/plugins/ui_actions/public/mocks'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; import { coreMock } from '../../../../core/public/mocks'; -import { EmbeddablePublicPlugin, IEmbeddableSetup, IEmbeddableStart } from '../plugin'; +import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin'; export interface TestPluginReturn { plugin: EmbeddablePublicPlugin; coreSetup: CoreSetup; coreStart: CoreStart; - setup: IEmbeddableSetup; - doStart: (anotherCoreStart?: CoreStart) => IEmbeddableStart; + setup: EmbeddableSetup; + doStart: (anotherCoreStart?: CoreStart) => EmbeddableStart; uiActions: UiActionsStart; } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx index 144954800c91fd..54d13efe4d7909 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx @@ -19,18 +19,15 @@ import { EuiTab } from '@elastic/eui'; import React, { Component } from 'react'; import { CoreStart } from 'src/core/public'; -import { - GetEmbeddableFactory, - GetEmbeddableFactories, -} from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions/public'; import { DashboardContainerExample } from './dashboard_container_example'; import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public'; export interface AppProps { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx index 7cc9c1df1c9482..f8625e4490e511 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx @@ -18,18 +18,19 @@ */ import React from 'react'; import { EuiButton, EuiLoadingChart } from '@elastic/eui'; +import { ContainerOutput } from 'src/plugins/embeddable/public'; import { ErrorEmbeddable, ViewMode, isErrorEmbeddable, EmbeddablePanel, - GetEmbeddableFactory, - GetEmbeddableFactories, + EmbeddableStart, } from '../embeddable_api'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, DashboardContainerFactory, + DashboardContainerInput, } from '../../../../../../../../src/plugins/dashboard/public'; import { CoreStart } from '../../../../../../../../src/core/public'; @@ -39,8 +40,8 @@ import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions interface Props { getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: GetEmbeddableFactory; - getAllEmbeddableFactories: GetEmbeddableFactories; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; @@ -67,9 +68,10 @@ export class DashboardContainerExample extends React.Component { public async componentDidMount() { this.mounted = true; - const dashboardFactory = this.props.getEmbeddableFactory( - DASHBOARD_CONTAINER_TYPE - ) as DashboardContainerFactory; + const dashboardFactory = this.props.getEmbeddableFactory< + DashboardContainerInput, + ContainerOutput + >(DASHBOARD_CONTAINER_TYPE) as DashboardContainerFactory; if (dashboardFactory) { this.container = await dashboardFactory.create(dashboardInput); if (this.mounted) { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts deleted file mode 100644 index 0c90cb3b85867a..00000000000000 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/embeddables/hello_world_embeddable_factory.ts +++ /dev/null @@ -1,28 +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. - */ - -// eslint-disable-next-line -import { npSetup } from '../../../../../../../../src/legacy/ui/public/new_platform'; -// eslint-disable-next-line -import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE } from '../../../../../../../../examples/embeddable_examples/public'; - -npSetup.plugins.embeddable.registerEmbeddableFactory( - HELLO_WORLD_EMBEDDABLE, - new HelloWorldEmbeddableFactory() -); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index 25666dc0359d95..18ceec652392d1 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -31,21 +31,16 @@ import { CONTEXT_MENU_TRIGGER } from './embeddable_api'; const REACT_ROOT_ID = 'embeddableExplorerRoot'; -import { - SayHelloAction, - createSendMessageAction, - ContactCardEmbeddableFactory, -} from './embeddable_api'; +import { SayHelloAction, createSendMessageAction } from './embeddable_api'; import { App } from './app'; import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public'; -import { HelloWorldEmbeddableFactory } from '../../../../../../../examples/embeddable_examples/public'; import { - IEmbeddableStart, - IEmbeddableSetup, + EmbeddableStart, + EmbeddableSetup, } from '.../../../../../../../src/plugins/embeddable/public'; export interface SetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; inspector: InspectorSetupContract; __LEGACY: { ExitFullScreenButton: React.ComponentType; @@ -53,7 +48,7 @@ export interface SetupDependencies { } interface StartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; uiActions: UiActionsStart; inspector: InspectorStartContract; __LEGACY: { @@ -74,12 +69,6 @@ export class EmbeddableExplorerPublicPlugin const helloWorldAction = createHelloWorldAction(core.overlays); const sayHelloAction = new SayHelloAction(alert); const sendMessageAction = createSendMessageAction(core.overlays); - const helloWorldEmbeddableFactory = new HelloWorldEmbeddableFactory(); - const contactCardEmbeddableFactory = new ContactCardEmbeddableFactory( - {}, - plugins.uiActions.executeTriggerActions, - core.overlays - ); plugins.uiActions.registerAction(helloWorldAction); plugins.uiActions.registerAction(sayHelloAction); @@ -87,15 +76,6 @@ export class EmbeddableExplorerPublicPlugin plugins.uiActions.attachAction(CONTEXT_MENU_TRIGGER, helloWorldAction); - plugins.embeddable.registerEmbeddableFactory( - helloWorldEmbeddableFactory.type, - helloWorldEmbeddableFactory - ); - plugins.embeddable.registerEmbeddableFactory( - contactCardEmbeddableFactory.type, - contactCardEmbeddableFactory - ); - plugins.__LEGACY.onRenderComplete(() => { const root = document.getElementById(REACT_ROOT_ID); ReactDOM.render( diff --git a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js index 203378e547c8ab..4a1bcecc0d5a1e 100644 --- a/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js +++ b/test/plugin_functional/test_suites/embeddable_explorer/dashboard_container.js @@ -17,11 +17,8 @@ * under the License. */ -import expect from '@kbn/expect'; - export default function({ getService }) { const testSubjects = getService('testSubjects'); - const retry = getService('retry'); const pieChart = getService('pieChart'); const dashboardExpect = getService('dashboardExpect'); @@ -30,17 +27,6 @@ export default function({ getService }) { await testSubjects.click('embedExplorerTab-dashboardContainer'); }); - it('hello world embeddable renders', async () => { - await retry.try(async () => { - const text = await testSubjects.getVisibleText('helloWorldEmbeddable'); - expect(text).to.be('HELLO WORLD!'); - }); - }); - - it('contact card embeddable renders', async () => { - await testSubjects.existOrFail('embeddablePanelHeading-HelloSue'); - }); - it('pie charts', async () => { await pieChart.expectPieSliceCount(5); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index d30ad62b385c22..2bde698e23562d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -27,17 +27,19 @@ import { Embeddable } from './embeddable'; import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; import { getEditPath } from '../../../../../../plugins/lens/common'; +interface StartServices { + timefilter: TimefilterContract; + coreHttp: HttpSetup; + capabilities: RecursiveReadonly; + savedObjectsClient: SavedObjectsClientContract; + expressionRenderer: ReactExpressionRendererType; + indexPatternService: IndexPatternsContract; +} + export class EmbeddableFactory extends AbstractEmbeddableFactory { type = DOC_TYPE; - constructor( - private timefilter: TimefilterContract, - private coreHttp: HttpSetup, - private capabilities: RecursiveReadonly, - private savedObjectsClient: SavedObjectsClientContract, - private expressionRenderer: ReactExpressionRendererType, - private indexPatternService: IndexPatternsContract - ) { + constructor(private getStartServices: () => Promise) { super({ savedObjectMetaData: { name: i18n.translate('xpack.lens.lensSavedObjectLabel', { @@ -49,8 +51,9 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { }); } - public isEditable() { - return this.capabilities.visualize.save as boolean; + public async isEditable() { + const { capabilities } = await this.getStartServices(); + return capabilities.visualize.save as boolean; } canCreateNew() { @@ -68,13 +71,20 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { input: Partial & { id: string }, parent?: IContainer ) { - const store = new SavedObjectIndexStore(this.savedObjectsClient); + const { + savedObjectsClient, + coreHttp, + indexPatternService, + timefilter, + expressionRenderer, + } = await this.getStartServices(); + const store = new SavedObjectIndexStore(savedObjectsClient); const savedVis = await store.load(savedObjectId); const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( async ({ id }) => { try { - return await this.indexPatternService.get(id); + return await indexPatternService.get(id); } catch (error) { // Unable to load index pattern, ignore error as the index patterns are only used to // configure the filter and query bar - there is still a good chance to get the visualization @@ -90,12 +100,12 @@ export class EmbeddableFactory extends AbstractEmbeddableFactory { ); return new Embeddable( - this.timefilter, - this.expressionRenderer, + timefilter, + expressionRenderer, { savedVis, - editUrl: this.coreHttp.basePath.prepend(getEditPath(savedObjectId)), - editable: this.isEditable(), + editUrl: coreHttp.basePath.prepend(getEditPath(savedObjectId)), + editable: await this.isEditable(), indexPatterns, }, input, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx index 2e1645c816140c..6b9dc88e7ed12b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.test.tsx @@ -12,6 +12,7 @@ import { createMockSetupDependencies, createMockStartDependencies, } from './mocks'; +import { CoreSetup } from 'kibana/public'; jest.mock('ui/new_platform'); @@ -41,7 +42,10 @@ describe('editor_frame service', () => { it('should create an editor frame instance which mounts and unmounts', async () => { await expect( (async () => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + pluginInstance.setup( + coreMock.createSetup() as CoreSetup, + pluginSetupDependencies + ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); const instance = await publicAPI.createInstance({}); instance.mount(mountpoint, { @@ -57,7 +61,10 @@ describe('editor_frame service', () => { }); it('should not have child nodes after unmount', async () => { - pluginInstance.setup(coreMock.createSetup(), pluginSetupDependencies); + pluginInstance.setup( + coreMock.createSetup() as CoreSetup, + pluginSetupDependencies + ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); const instance = await publicAPI.createInstance({}); instance.mount(mountpoint, { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx index 5347be47e145ed..1375c60060ca82 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/service.tsx @@ -12,10 +12,7 @@ import { ExpressionsSetup, ExpressionsStart, } from '../../../../../../src/plugins/expressions/public'; -import { - IEmbeddableSetup, - IEmbeddableStart, -} from '../../../../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -35,13 +32,13 @@ import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; expressions: ExpressionsSetup; } export interface EditorFrameStartPlugins { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ExpressionsStart; } @@ -63,10 +60,27 @@ export class EditorFrameService { private readonly datasources: Array> = []; private readonly visualizations: Array> = []; - public setup(core: CoreSetup, plugins: EditorFrameSetupPlugins): EditorFrameSetup { + public setup( + core: CoreSetup, + plugins: EditorFrameSetupPlugins + ): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); plugins.expressions.registerFunction(() => formatColumn); + const getStartServices = async () => { + const [coreStart, deps] = await core.getStartServices(); + return { + capabilities: coreStart.application.capabilities, + savedObjectsClient: coreStart.savedObjects.client, + coreHttp: coreStart.http, + timefilter: deps.data.query.timefilter.timefilter, + expressionRenderer: deps.expressions.ReactExpressionRenderer, + indexPatternService: deps.data.indexPatterns, + }; + }; + + plugins.embeddable.registerEmbeddableFactory('lens', new EmbeddableFactory(getStartServices)); + return { registerDatasource: datasource => { this.datasources.push(datasource as Datasource); @@ -78,18 +92,6 @@ export class EditorFrameService { } public start(core: CoreStart, plugins: EditorFrameStartPlugins): EditorFrameStart { - plugins.embeddable.registerEmbeddableFactory( - 'lens', - new EmbeddableFactory( - plugins.data.query.timefilter.timefilter, - core.http, - core.application.capabilities, - core.savedObjects.client, - plugins.expressions.ReactExpressionRenderer, - plugins.data.indexPatterns - ) - ); - const createInstance = async (): Promise => { let domElement: Element; const [resolvedDatasources, resolvedVisualizations] = await Promise.all([ diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index 7afe6d7abedc0e..cc029fee49d1d8 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -36,7 +36,7 @@ import { getLensUrlFromDashboardAbsoluteUrl, } from '../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; import { FormatFactory } from './legacy_imports'; -import { IEmbeddableSetup, IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { VisualizationsSetup } from './legacy_imports'; @@ -45,7 +45,7 @@ export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; expressions: ExpressionsSetup; data: DataPublicPluginSetup; - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; __LEGACY: { formatFactory: FormatFactory; visualizations: VisualizationsSetup; @@ -54,7 +54,7 @@ export interface LensPluginSetupDependencies { export interface LensPluginStartDependencies { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; expressions: ExpressionsStart; } diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 0c93cd51abd79e..888df8447a728a 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -9,18 +9,13 @@ import React from 'react'; import { OutPortal, PortalNode } from 'react-reverse-portal'; import minimatch from 'minimatch'; import { ViewMode } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { - IndexPatternMapping, - MapEmbeddable, - RenderTooltipContentParams, - SetQuery, - EmbeddableApi, -} from './types'; +import { IndexPatternMapping, MapEmbeddable, RenderTooltipContentParams, SetQuery } from './types'; import { getLayerList } from './map_config'; // @ts-ignore Missing type defs as maps moves to Typescript import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; import * as i18n from './translations'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; import { IndexPatternSavedObject } from '../../hooks/types'; /** @@ -45,7 +40,7 @@ export const createEmbeddable = async ( endDate: number, setQuery: SetQuery, portalNode: PortalNode, - embeddableApi: EmbeddableApi + embeddableApi: EmbeddableStart ): Promise => { const factory = embeddableApi.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts index 812d327ce4488b..cc253beb08eaed 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts @@ -9,7 +9,6 @@ import { EmbeddableInput, EmbeddableOutput, IEmbeddable, - EmbeddableFactory, } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { inputsModel } from '../../store/inputs'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; @@ -85,8 +84,3 @@ export interface RenderTooltipContentParams { } export type MapToolTipProps = Partial; - -export interface EmbeddableApi { - getEmbeddableFactory: (embeddableFactoryId: string) => EmbeddableFactory; - registerEmbeddableFactory: (id: string, factory: EmbeddableFactory) => void; -} diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index f22add59a95d4a..71fa3a54df7689 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -13,7 +13,7 @@ import { } from '../../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { IEmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; import { Start as NewsfeedStart } from '../../../../../src/plugins/newsfeed/public'; import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; @@ -37,7 +37,7 @@ export interface SetupPlugins { } export interface StartPlugins { data: DataPublicPluginStart; - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; inspector: InspectorStart; newsfeed?: NewsfeedStart; uiActions: UiActionsStart; diff --git a/x-pack/plugins/advanced_ui_actions/public/plugin.ts b/x-pack/plugins/advanced_ui_actions/public/plugin.ts index 2f6935cdf1961f..b9f0ce43d3cdcb 100644 --- a/x-pack/plugins/advanced_ui_actions/public/plugin.ts +++ b/x-pack/plugins/advanced_ui_actions/public/plugin.ts @@ -15,8 +15,8 @@ import { UiActionsStart, UiActionsSetup } from '../../../../src/plugins/ui_actio import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, - IEmbeddableSetup, - IEmbeddableStart, + EmbeddableSetup, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; import { CustomTimeRangeAction, @@ -32,12 +32,12 @@ import { import { CommonlyUsedRange } from './types'; interface SetupDependencies { - embeddable: IEmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. + embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. uiActions: UiActionsSetup; } interface StartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; uiActions: UiActionsStart; } diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts index 789a4181c2aff5..3d143b0cacd063 100644 --- a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts +++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_container.ts @@ -8,7 +8,7 @@ import { ContainerInput, Container, ContainerOutput, - GetEmbeddableFactory, + EmbeddableStart, } from '../../../../../src/plugins/embeddable/public'; import { TimeRange } from '../../../../../src/plugins/data/public'; @@ -37,7 +37,7 @@ export class TimeRangeContainer extends Container< public readonly type = TIME_RANGE_CONTAINER; constructor( initialInput: ContainerTimeRangeInput, - getFactory: GetEmbeddableFactory, + getFactory: EmbeddableStart['getEmbeddableFactory'], parent?: Container ) { super(initialInput, { embeddableLoaded: {} }, getFactory, parent); diff --git a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts index efbf7a3bd2dc65..311d3357476b91 100644 --- a/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts +++ b/x-pack/plugins/advanced_ui_actions/public/test_helpers/time_range_embeddable_factory.ts @@ -19,7 +19,7 @@ interface EmbeddableTimeRangeInput extends EmbeddableInput { export class TimeRangeEmbeddableFactory extends EmbeddableFactory { public readonly type = TIME_RANGE_EMBEDDABLE; - public isEditable() { + public async isEditable() { return true; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts index f5d1aad93ed574..c8e038869efcd6 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -15,7 +15,7 @@ import { ResolverEmbeddable } from './embeddable'; export class ResolverEmbeddableFactory extends EmbeddableFactory { public readonly type = 'resolver'; - public isEditable() { + public async isEditable() { return true; } diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 155d709042fe7b..2759db26bb6c87 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -5,7 +5,7 @@ */ import { Plugin, CoreSetup, AppMountParameters, CoreStart } from 'kibana/public'; -import { IEmbeddableSetup } from 'src/plugins/embeddable/public'; +import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { i18n } from '@kbn/i18n'; import { ResolverEmbeddableFactory } from './embeddables/resolver'; @@ -13,7 +13,7 @@ import { ResolverEmbeddableFactory } from './embeddables/resolver'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; export interface EndpointPluginSetupDependencies { - embeddable: IEmbeddableSetup; + embeddable: EmbeddableSetup; data: DataPublicPluginStart; } export interface EndpointPluginStartDependencies { diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts index 045cc56238ac9b..502164554c711b 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -6,13 +6,13 @@ import { Plugin, CoreSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { IEmbeddable, IEmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; +import { IEmbeddable, EmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; export type ResolverTestPluginSetup = void; export type ResolverTestPluginStart = void; export interface ResolverTestPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface export interface ResolverTestPluginStartDependencies { - embeddable: IEmbeddableStart; + embeddable: EmbeddableStart; } export class ResolverTestPlugin @@ -41,7 +41,9 @@ export class ResolverTestPlugin (async () => { const [, { embeddable }] = await core.getStartServices(); const factory = embeddable.getEmbeddableFactory('resolver'); - resolveEmbeddable!(factory.create({ id: 'test basic render' })); + if (factory) { + resolveEmbeddable!(factory.create({ id: 'test basic render' })); + } })(); const { renderApp } = await import('./applications/resolver_test'); From 77a859d43db820b468e45cd1628de63fbc9d3585 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 16 Mar 2020 15:46:17 -0400 Subject: [PATCH 09/17] [Remote clusters] Add support for proxy mode (#59221) --- .../remote_clusters_add.test.js | 50 + .../remote_clusters_list.test.js | 53 +- .../remote_clusters/common/constants.ts | 3 + .../common/lib/cluster_serialization.test.ts | 45 + .../common/lib/cluster_serialization.ts | 110 +- .../remote_clusters/common/lib/index.ts | 2 +- .../fixtures/remote_cluster.js | 10 + .../remote_cluster_form.test.js.snap | 1534 ++++++++++++++++- .../remote_cluster_form.js | 275 ++- .../remote_cluster_form.test.js | 10 + .../__snapshots__/validate_proxy.test.js.snap | 25 + .../remote_cluster_form/validators/index.js | 3 +- .../validators/validate_proxy.js | 44 + .../validators/validate_proxy.test.js | 25 + .../validators/validate_seed.js | 8 +- .../remote_cluster_edit.js | 40 +- .../connection_status/connection_status.js | 13 +- .../detail_panel/detail_panel.js | 425 +++-- .../remote_cluster_table.js | 84 +- .../application/services/documentation.ts | 2 + .../public/application/services/index.js | 2 +- ...idate_seed_node.js => validate_address.js} | 4 +- ..._node.test.js => validate_address.test.js} | 34 +- .../server/routes/api/add_route.test.ts | 91 +- .../server/routes/api/add_route.ts | 15 +- .../server/routes/api/delete_route.test.ts | 24 +- .../server/routes/api/get_route.test.ts | 3 + .../server/routes/api/get_route.ts | 17 +- .../server/routes/api/update_route.test.ts | 28 +- .../server/routes/api/update_route.ts | 17 +- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - .../remote_clusters.helpers.js | 3 +- .../remote_clusters/remote_clusters.js | 7 + 34 files changed, 2765 insertions(+), 251 deletions(-) create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/__snapshots__/validate_proxy.test.js.snap create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.js create mode 100644 x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.test.js rename x-pack/plugins/remote_clusters/public/application/services/{validate_seed_node.js => validate_address.js} (91%) rename x-pack/plugins/remote_clusters/public/application/services/{validate_seed_node.test.js => validate_address.test.js} (55%) diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js index 8f34e7d84a08be..78482198b1a5d4 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_add.test.js @@ -53,6 +53,17 @@ describe('Create Remote cluster', () => { expect(find('remoteClusterFormSkipUnavailableFormToggle').props()['aria-checked']).toBe(true); }); + test('should have a toggle to enable "proxy" mode for a remote cluster', () => { + expect(exists('remoteClusterFormConnectionModeToggle')).toBe(true); + + // By default it should be set to "false" + expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(false); + + form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); + + expect(find('remoteClusterFormConnectionModeToggle').props()['aria-checked']).toBe(true); + }); + test('should display errors and disable the save button when clicking "save" without filling the form', () => { expect(exists('remoteClusterFormGlobalError')).toBe(false); expect(find('remoteClusterFormSaveButton').props().disabled).toBe(false); @@ -144,5 +155,44 @@ describe('Create Remote cluster', () => { expect(form.getErrorsMessages()).toContain('A port is required.'); }); }); + + describe('proxy address', () => { + let actions; + let form; + + beforeEach(async () => { + ({ form, actions } = setup()); + + // Enable "proxy" mode + form.toggleEuiSwitch('remoteClusterFormConnectionModeToggle'); + }); + + test('should only allow alpha-numeric characters and "-" (dash) in the proxy address "host" part', () => { + actions.clickSaveForm(); // display form errors + + const notInArray = array => value => array.indexOf(value) < 0; + + const expectInvalidChar = char => { + form.setInputValue('remoteClusterFormProxyAddressInput', `192.16${char}:3000`); + expect(form.getErrorsMessages()).toContain( + 'Address must use host:port format. Example: 127.0.0.1:9400, localhost:9400. Hosts can only consist of letters, numbers, and dashes.' + ); + }; + + [...NON_ALPHA_NUMERIC_CHARS, ...ACCENTED_CHARS] + .filter(notInArray(['-', '_', ':'])) + .forEach(expectInvalidChar); + }); + + test('should require a numeric "port" to be set', () => { + actions.clickSaveForm(); + + form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1'); + expect(form.getErrorsMessages()).toContain('A port is required.'); + + form.setInputValue('remoteClusterFormProxyAddressInput', '192.168.1.1:abc'); + expect(form.getErrorsMessages()).toContain('A port is required.'); + }); + }); }); }); diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js index 1b7c600218cee2..954deb8b98d3e1 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js @@ -15,6 +15,8 @@ import { import { getRouter } from '../../public/application/services'; import { getRemoteClusterMock } from '../../fixtures/remote_cluster'; +import { PROXY_MODE } from '../../common/constants'; + jest.mock('ui/new_platform'); const { setup } = pageHelpers.remoteClustersList; @@ -84,12 +86,26 @@ describe('', () => { const remoteCluster2 = getRemoteClusterMock({ name: `b${getRandomString()}`, isConnected: false, - connectedNodesCount: 0, - seeds: ['localhost:9500'], + connectedSocketsCount: 0, + proxyAddress: 'localhost:9500', isConfiguredByNode: true, + mode: PROXY_MODE, + seeds: null, + connectedNodesCount: null, + }); + const remoteCluster3 = getRemoteClusterMock({ + name: `c${getRandomString()}`, + isConnected: false, + connectedSocketsCount: 0, + proxyAddress: 'localhost:9500', + isConfiguredByNode: false, + mode: PROXY_MODE, + hasDeprecatedProxySetting: true, + seeds: null, + connectedNodesCount: null, }); - const remoteClusters = [remoteCluster1, remoteCluster2]; + const remoteClusters = [remoteCluster1, remoteCluster2, remoteCluster3]; beforeEach(async () => { httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); @@ -118,17 +134,28 @@ describe('', () => { [ '', // Empty because the first column is the checkbox to select the row remoteCluster1.name, - remoteCluster1.seeds.join(', '), 'Connected', + 'default', + remoteCluster1.seeds.join(', '), remoteCluster1.connectedNodesCount.toString(), '', // Empty because the last column is for the "actions" on the resource ], [ '', remoteCluster2.name, - remoteCluster2.seeds.join(', '), 'Not connected', - remoteCluster2.connectedNodesCount.toString(), + PROXY_MODE, + remoteCluster2.proxyAddress, + remoteCluster2.connectedSocketsCount.toString(), + '', + ], + [ + '', + remoteCluster3.name, + 'Not connected', + PROXY_MODE, + remoteCluster2.proxyAddress, + remoteCluster2.connectedSocketsCount.toString(), '', ], ]); @@ -141,6 +168,14 @@ describe('', () => { ).toBe(1); }); + test('should have a tooltip to indicate that the cluster has a deprecated setting', () => { + const secondRow = rows[2].reactWrapper; // The third cluster has been defined with deprecated setting + expect( + findTestSubject(secondRow, 'remoteClustersTableListClusterWithDeprecatedSettingTooltip') + .length + ).toBe(1); + }); + describe('bulk delete button', () => { test('should be visible when a remote cluster is selected', () => { expect(exists('remoteClusterBulkDeleteButton')).toBe(false); @@ -199,8 +234,8 @@ describe('', () => { errors: [], }); - // Make sure that we have our 2 remote clusters in the table - expect(rows.length).toBe(2); + // Make sure that we have our 3 remote clusters in the table + expect(rows.length).toBe(3); actions.selectRemoteClusterAt(0); actions.clickBulkDeleteButton(); @@ -211,7 +246,7 @@ describe('', () => { ({ rows } = table.getMetaData('remoteClusterListTable')); - expect(rows.length).toBe(1); + expect(rows.length).toBe(2); expect(rows[0].columns[1].value).toEqual(remoteCluster2.name); }); }); diff --git a/x-pack/plugins/remote_clusters/common/constants.ts b/x-pack/plugins/remote_clusters/common/constants.ts index 353160de8bf4a8..20ad6da227c558 100644 --- a/x-pack/plugins/remote_clusters/common/constants.ts +++ b/x-pack/plugins/remote_clusters/common/constants.ts @@ -20,3 +20,6 @@ export const PLUGIN = { }; export const API_BASE_PATH = '/api/remote_clusters'; + +export const SNIFF_MODE = 'sniff'; +export const PROXY_MODE = 'proxy'; diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts index 476fbee7fb6a06..5be6ed8828e6fc 100644 --- a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.test.ts @@ -9,6 +9,7 @@ import { deserializeCluster, serializeCluster } from './cluster_serialization'; describe('cluster_serialization', () => { describe('deserializeCluster()', () => { it('should throw an error for invalid arguments', () => { + // @ts-ignore expect(() => deserializeCluster('foo', 'bar')).toThrowError(); }); @@ -60,6 +61,39 @@ describe('cluster_serialization', () => { }); }); + it('should deserialize a cluster that contains a deprecated proxy address', () => { + expect( + deserializeCluster( + 'test_cluster', + { + seeds: ['localhost:9300'], + connected: true, + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + transport: { + ping_schedule: '-1', + compress: false, + }, + }, + 'localhost:9300' + ) + ).toEqual({ + name: 'test_cluster', + proxyAddress: 'localhost:9300', + mode: 'proxy', + hasDeprecatedProxySetting: true, + isConnected: true, + connectedNodesCount: 1, + maxConnectionsPerCluster: 3, + initialConnectTimeout: '30s', + skipUnavailable: false, + transportPingSchedule: '-1', + transportCompress: false, + }); + }); + it('should deserialize a cluster object with arbitrary missing properties', () => { expect( deserializeCluster('test_cluster', { @@ -84,6 +118,7 @@ describe('cluster_serialization', () => { describe('serializeCluster()', () => { it('should throw an error for invalid arguments', () => { + // @ts-ignore expect(() => serializeCluster('foo')).toThrowError(); }); @@ -105,8 +140,13 @@ describe('cluster_serialization', () => { cluster: { remote: { test_cluster: { + mode: null, + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, seeds: ['localhost:9300'], skip_unavailable: false, + server_name: null, }, }, }, @@ -125,8 +165,13 @@ describe('cluster_serialization', () => { cluster: { remote: { test_cluster: { + mode: null, + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, seeds: ['localhost:9300'], skip_unavailable: null, + server_name: null, }, }, }, diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts index 07ea79d42b8006..53dc72eb1695a5 100644 --- a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts @@ -4,29 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ -export function deserializeCluster(name: string, esClusterObject: any): any { +import { PROXY_MODE } from '../constants'; + +export interface ClusterEs { + seeds?: string[]; + mode?: 'proxy' | 'sniff'; + connected?: boolean; + num_nodes_connected?: number; + max_connections_per_cluster?: number; + initial_connect_timeout?: string; + skip_unavailable?: boolean; + transport?: { + ping_schedule?: string; + compress?: boolean; + }; + address?: string; + max_socket_connections?: number; + num_sockets_connected?: number; +} + +export interface Cluster { + name: string; + seeds?: string[]; + skipUnavailable?: boolean; + nodeConnections?: number; + proxyAddress?: string; + proxySocketConnections?: number; + serverName?: string; + mode?: 'proxy' | 'sniff'; + isConnected?: boolean; + transportPingSchedule?: string; + transportCompress?: boolean; + connectedNodesCount?: number; + maxConnectionsPerCluster?: number; + initialConnectTimeout?: string; + connectedSocketsCount?: number; + hasDeprecatedProxySetting?: boolean; +} +export interface ClusterPayload { + persistent: { + cluster: { + remote: { + [key: string]: { + skip_unavailable?: boolean | null; + mode?: 'sniff' | 'proxy' | null; + proxy_address?: string | null; + proxy_socket_connections?: number | null; + server_name?: string | null; + seeds?: string[] | null; + node_connections?: number | null; + }; + }; + }; + }; +} + +export function deserializeCluster( + name: string, + esClusterObject: ClusterEs, + deprecatedProxyAddress?: string | undefined +): Cluster { if (!name || !esClusterObject || typeof esClusterObject !== 'object') { throw new Error('Unable to deserialize cluster'); } const { seeds, + mode, connected: isConnected, num_nodes_connected: connectedNodesCount, max_connections_per_cluster: maxConnectionsPerCluster, initial_connect_timeout: initialConnectTimeout, skip_unavailable: skipUnavailable, transport, + address: proxyAddress, + max_socket_connections: proxySocketConnections, + num_sockets_connected: connectedSocketsCount, } = esClusterObject; - let deserializedClusterObject: any = { + let deserializedClusterObject: Cluster = { name, - seeds, + mode, isConnected, connectedNodesCount, maxConnectionsPerCluster, initialConnectTimeout, skipUnavailable, + seeds, + proxyAddress, + proxySocketConnections, + connectedSocketsCount, }; if (transport) { @@ -39,30 +106,57 @@ export function deserializeCluster(name: string, esClusterObject: any): any { }; } + // If a user has a remote cluster with the deprecated proxy setting, + // we transform the data to support the new implementation and also flag the deprecation + if (deprecatedProxyAddress) { + deserializedClusterObject = { + ...deserializedClusterObject, + proxyAddress: deprecatedProxyAddress, + seeds: undefined, + hasDeprecatedProxySetting: true, + mode: PROXY_MODE, + }; + } + // It's unnecessary to send undefined values back to the client, so we can remove them. Object.keys(deserializedClusterObject).forEach(key => { - if (deserializedClusterObject[key] === undefined) { - delete deserializedClusterObject[key]; + if (deserializedClusterObject[key as keyof Cluster] === undefined) { + delete deserializedClusterObject[key as keyof Cluster]; } }); return deserializedClusterObject; } -export function serializeCluster(deserializedClusterObject: any): any { +export function serializeCluster(deserializedClusterObject: Cluster): ClusterPayload { if (!deserializedClusterObject || typeof deserializedClusterObject !== 'object') { throw new Error('Unable to serialize cluster'); } - const { name, seeds, skipUnavailable } = deserializedClusterObject; + const { + name, + seeds, + skipUnavailable, + mode, + nodeConnections, + proxyAddress, + proxySocketConnections, + serverName, + } = deserializedClusterObject; return { + // Background on why we only save as persistent settings detailed here: https://github.com/elastic/kibana/pull/26067#issuecomment-441848124 persistent: { cluster: { remote: { [name]: { - seeds: seeds ? seeds : null, skip_unavailable: skipUnavailable !== undefined ? skipUnavailable : null, + mode: mode ?? null, + proxy_address: proxyAddress ?? null, + proxy_socket_connections: proxySocketConnections ?? null, + server_name: serverName ?? null, + seeds: seeds ?? null, + node_connections: nodeConnections ?? null, }, }, }, diff --git a/x-pack/plugins/remote_clusters/common/lib/index.ts b/x-pack/plugins/remote_clusters/common/lib/index.ts index bc67bf21af0384..52a0536bfd55b9 100644 --- a/x-pack/plugins/remote_clusters/common/lib/index.ts +++ b/x-pack/plugins/remote_clusters/common/lib/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { deserializeCluster, serializeCluster } from './cluster_serialization'; +export { deserializeCluster, serializeCluster, Cluster, ClusterEs } from './cluster_serialization'; diff --git a/x-pack/plugins/remote_clusters/fixtures/remote_cluster.js b/x-pack/plugins/remote_clusters/fixtures/remote_cluster.js index e3e087548cf001..6a3bcba21d772d 100644 --- a/x-pack/plugins/remote_clusters/fixtures/remote_cluster.js +++ b/x-pack/plugins/remote_clusters/fixtures/remote_cluster.js @@ -5,12 +5,18 @@ */ import { getRandomString } from '../../../test_utils'; +import { SNIFF_MODE } from '../common/constants'; + export const getRemoteClusterMock = ({ name = getRandomString(), isConnected = true, connectedNodesCount = 1, + connectedSocketsCount, seeds = ['localhost:9400'], isConfiguredByNode = false, + mode = SNIFF_MODE, + proxyAddress, + hasDeprecatedProxySetting = false, } = {}) => ({ name, seeds, @@ -20,4 +26,8 @@ export const getRemoteClusterMock = ({ maxConnectionsPerCluster: 3, initialConnectTimeout: '30s', skipUnavailable: false, + mode, + connectedSocketsCount, + proxyAddress, + hasDeprecatedProxySetting, }); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap index 8d6c5b040ce846..88b869b1d1d8fc 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/__snapshots__/remote_cluster_form.test.js.snap @@ -1,5 +1,1429 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`RemoteClusterForm proxy mode renders correct connection settings when user enables proxy mode 1`] = ` + + +
+ + } + fullWidth={true} + title={ + +

+ +

+
+ } + > + +
+ + + Name + + + +
+ +
+ + + + +
+ +
+ + A unique name for the remote cluster. + +
+
+
+
+
+
+ +
+ + } + fullWidth={true} + hasChildLabel={true} + hasEmptyLabelSpace={false} + helpText={ + + } + isInvalid={false} + label={ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + Name can only contain letters, numbers, underscores, and dashes. + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + , + } + } + /> + } + labelType="label" + > + + } + onChange={[Function]} + /> + + + } + fullWidth={true} + title={ + +

+ +

+
+ } + > + +
+ + + Connection mode + + + +
+ +
+ + + + +
+ +
+ + Remote cluster connections work by configuring a remote cluster and connecting only to a limited number of nodes in that remote cluster. + + + + , + } + } + /> + } + labelType="label" + > +
+
+ + } + onBlur={[Function]} + onChange={[Function]} + onFocus={[Function]} + > +
+ + + + Use proxy mode + + +
+
+ +
+ + + , + } + } + > + Configure a remote cluster with a single proxy address. + + + + +
+
+
+
+
+
+
+
+
+
+
+ +
+ + } + fullWidth={true} + hasChildLabel={true} + hasEmptyLabelSpace={false} + helpText={ + + } + isInvalid={false} + label={ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + The address used for all remote connections. + +
+
+
+
+
+ + } + label={ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + The number of socket connections to open per remote cluster. + +
+
+
+
+
+ + } + label={ + + } + labelType="label" + > +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + An optional hostname string which will be sent in the server_name field of the TLS Server Name Indication extension if TLS is enabled. + +
+
+
+
+
+
+
+
+
+
+
+
+ +

+ + + , + "optionName": + + , + } + } + /> +

+ + } + fullWidth={true} + title={ + +

+ +

+
+ } + > + +
+ + + Make remote cluster optional + + + +
+ +
+ + + + +
+ +
+

+ + + , + "optionName": + + , + } + } + > + By default, a request fails if any of the queried remote clusters are unavailable. To continue sending a request to other remote clusters if this cluster is unavailable, enable + + + Skip if unavailable + + + . + + + + +

+
+
+
+
+
+
+ +
+ +
+
+ +
+ + + Skip if unavailable + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+ +
+ +
+ +
+ + + +
+
+
+
+
+
+ +
+ + + +
+
+
+
+ +`; + exports[`RemoteClusterForm renders untouched state 1`] = ` Array [
@@ -191,7 +1676,48 @@ Array [ > transport port - of the remote cluster. + of the remote cluster. Specify multiple seed nodes so discovery doesn't fail if a node is unavailable. +
+ + +
+
+ +
+
+
+
+ +
+
+
+ The number of gateway nodes to connect to.
@@ -490,7 +2016,7 @@ Array [ > transport port - of the remote cluster. + of the remote cluster. Specify multiple seed nodes so discovery doesn't fail if a node is unavailable. , diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js index 08cd01496a8b9e..358ffc03da783b 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.js @@ -15,6 +15,7 @@ import { EuiCallOut, EuiComboBox, EuiDescribedFormGroup, + EuiFieldNumber, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -33,16 +34,27 @@ import { htmlIdGenerator, } from '@elastic/eui'; -import { skippingDisconnectedClustersUrl, transportPortUrl } from '../../../services/documentation'; +import { + skippingDisconnectedClustersUrl, + transportPortUrl, + proxyModeUrl, +} from '../../../services/documentation'; import { RequestFlyout } from './request_flyout'; -import { validateName, validateSeeds, validateSeed } from './validators'; +import { validateName, validateSeeds, validateProxy, validateSeed } from './validators'; + +import { SNIFF_MODE, PROXY_MODE } from '../../../../../common/constants'; const defaultFields = { name: '', seeds: [], skipUnavailable: false, + mode: SNIFF_MODE, + nodeConnections: 3, + proxyAddress: '', + proxySocketConnections: 18, + serverName: '', }; const ERROR_TITLE_ID = 'removeClustersErrorTitle'; @@ -88,10 +100,12 @@ export class RemoteClusterForm extends Component { }; getFieldsErrors(fields, seedInput = '') { - const { name, seeds } = fields; + const { name, seeds, mode, proxyAddress } = fields; + return { name: validateName(name), - seeds: validateSeeds(seeds, seedInput), + seeds: mode === SNIFF_MODE ? validateSeeds(seeds, seedInput) : null, + proxyAddress: mode === PROXY_MODE ? validateProxy(proxyAddress) : null, }; } @@ -110,13 +124,38 @@ export class RemoteClusterForm extends Component { getAllFields() { const { - fields: { name, seeds, skipUnavailable }, + fields: { + name, + mode, + seeds, + nodeConnections, + proxyAddress, + proxySocketConnections, + serverName, + skipUnavailable, + }, } = this.state; + let modeSettings; + + if (mode === PROXY_MODE) { + modeSettings = { + proxyAddress, + proxySocketConnections, + serverName, + }; + } else { + modeSettings = { + seeds, + nodeConnections, + }; + } + return { name, - seeds, skipUnavailable, + mode, + ...modeSettings, }; } @@ -215,10 +254,10 @@ export class RemoteClusterForm extends Component { return hasErrors; }; - renderSeeds() { + renderSniffModeSettings() { const { areErrorsVisible, - fields: { seeds }, + fields: { seeds, nodeConnections }, fieldsErrors: { seeds: errorsSeeds }, localSeedErrors, } = this.state; @@ -231,26 +270,7 @@ export class RemoteClusterForm extends Component { const formattedSeeds = seeds.map(seed => ({ label: seed })); return ( - -

- -

- - } - description={ - - } - fullWidth - > + <> @@ -296,6 +316,187 @@ export class RemoteClusterForm extends Component { data-test-subj="remoteClusterFormSeedsInput" /> + + + } + helpText={ + + } + fullWidth + > + this.onFieldsChange({ nodeConnections: Number(e.target.value) || null })} + fullWidth + /> + + + ); + } + + renderProxyModeSettings() { + const { + areErrorsVisible, + fields: { proxyAddress, proxySocketConnections, serverName }, + fieldsErrors: { proxyAddress: errorProxyAddress }, + } = this.state; + + return ( + <> + + } + helpText={ + + } + isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} + error={errorProxyAddress} + fullWidth + > + this.onFieldsChange({ proxyAddress: e.target.value })} + isInvalid={Boolean(areErrorsVisible && errorProxyAddress)} + data-test-subj="remoteClusterFormProxyAddressInput" + fullWidth + /> + + + + } + helpText={ + + } + fullWidth + > + + this.onFieldsChange({ proxySocketConnections: Number(e.target.value) || null }) + } + fullWidth + /> + + + } + helpText={ + + } + fullWidth + > + this.onFieldsChange({ serverName: e.target.value })} + fullWidth + /> + + + ); + } + + renderMode() { + const { + fields: { mode }, + } = this.state; + + return ( + +

+ +

+ + } + description={ + <> + + + + + ), + }} + /> + } + > + + } + checked={mode === PROXY_MODE} + data-test-subj="remoteClusterFormConnectionModeToggle" + onChange={e => + this.onFieldsChange({ mode: e.target.checked ? PROXY_MODE : SNIFF_MODE }) + } + /> + + + } + fullWidth + > + {mode === PROXY_MODE ? this.renderProxyModeSettings() : this.renderSniffModeSettings()}
); } @@ -522,7 +723,7 @@ export class RemoteClusterForm extends Component { renderErrors = () => { const { areErrorsVisible, - fieldsErrors: { name: errorClusterName, seeds: errorsSeeds }, + fieldsErrors: { name: errorClusterName, seeds: errorsSeeds, proxyAddress: errorProxyAddress }, localSeedErrors, } = this.state; @@ -564,6 +765,16 @@ export class RemoteClusterForm extends Component { }); } + if (errorProxyAddress) { + errorExplanations.push({ + key: 'seedsExplanation', + field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputProxyErrorMessage', { + defaultMessage: 'The "Proxy address" field is invalid.', + }), + error: errorProxyAddress, + }); + } + const messagesToBeRendered = errorExplanations.length && (
@@ -662,7 +873,7 @@ export class RemoteClusterForm extends Component { - {this.renderSeeds()} + {this.renderMode()} {this.renderSkipUnavailable()} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js index 799bf1f4fd0519..907fd2183265f9 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/remote_cluster_form.test.js @@ -21,6 +21,16 @@ describe('RemoteClusterForm', () => { expect(component).toMatchSnapshot(); }); + describe('proxy mode', () => { + test('renders correct connection settings when user enables proxy mode', () => { + const component = mountWithIntl( {}} />); + + findTestSubject(component, 'remoteClusterFormConnectionModeToggle').simulate('click'); + + expect(component).toMatchSnapshot(); + }); + }); + describe('validation', () => { test('renders invalid state and a global form error when the user tries to submit an invalid form', () => { const component = mountWithIntl( {}} />); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/__snapshots__/validate_proxy.test.js.snap b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/__snapshots__/validate_proxy.test.js.snap new file mode 100644 index 00000000000000..646b0b509f4e46 --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/__snapshots__/validate_proxy.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validateProxy rejects proxy address when the address is invalid 1`] = ` + +`; + +exports[`validateProxy rejects proxy address when the port is invalid 1`] = ` + +`; + +exports[`validateProxy rejects proxy address when there's no input 1`] = ` + +`; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.js index 66a1016c7fcc84..ec5f0b1166ce5d 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/index.js @@ -5,5 +5,6 @@ */ export { validateName } from './validate_name'; -export { validateSeed } from './validate_seed'; +export { validateProxy } from './validate_proxy'; export { validateSeeds } from './validate_seeds'; +export { validateSeed } from './validate_seed'; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.js new file mode 100644 index 00000000000000..9648bd36c1a4ec --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.js @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { isAddressValid, isPortValid } from '../../../../services'; + +export function validateProxy(proxy) { + if (!proxy) { + return ( + + ); + } + + const isValid = isAddressValid(proxy); + + if (!isValid) { + return ( + + ); + } + + if (!isPortValid(proxy)) { + return ( + + ); + } + + return null; +} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.test.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.test.js new file mode 100644 index 00000000000000..e6e69849e13aaa --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_proxy.test.js @@ -0,0 +1,25 @@ +/* + * 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 { validateProxy } from './validate_proxy'; + +describe('validateProxy', () => { + test(`rejects proxy address when there's no input`, () => { + expect(validateProxy(undefined)).toMatchSnapshot(); + }); + + test(`rejects proxy address when the address is invalid`, () => { + expect(validateProxy('___')).toMatchSnapshot(); + }); + + test(`rejects proxy address when the port is invalid`, () => { + expect(validateProxy('noport')).toMatchSnapshot(); + }); + + test(`accepts valid proxy address`, () => { + expect(validateProxy('localhost:3000')).toBe(null); + }); +}); diff --git a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.js b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.js index e2260504cc033f..e312e77972fbb7 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/components/remote_cluster_form/validators/validate_seed.js @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { isSeedNodeValid, isSeedNodePortValid } from '../../../../services'; +import { isAddressValid, isPortValid } from '../../../../services'; export function validateSeed(seed) { const errors = []; @@ -15,7 +15,7 @@ export function validateSeed(seed) { return errors; } - const isValid = isSeedNodeValid(seed); + const isValid = isAddressValid(seed); if (!isValid) { errors.push( @@ -30,9 +30,7 @@ export function validateSeed(seed) { ); } - const isPortValid = isSeedNodePortValid(seed); - - if (!isPortValid) { + if (!isPortValid(seed)) { errors.push( i18n.translate('xpack.remoteClusters.remoteClusterForm.localSeedError.invalidPortMessage', { defaultMessage: 'A port is required.', diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js index f48d854da7255d..2c0936b319d09a 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_edit/remote_cluster_edit.js @@ -158,7 +158,7 @@ export class RemoteClusterEdit extends Component { ); } - const { isConfiguredByNode } = cluster; + const { isConfiguredByNode, hasDeprecatedProxySetting } = cluster; if (isConfiguredByNode) { return ( @@ -178,14 +178,36 @@ export class RemoteClusterEdit extends Component { } return ( - + <> + {hasDeprecatedProxySetting ? ( + <> + + } + color="warning" + iconType="help" + > + + + + + ) : null} + + ); } diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js index d6d3272c2abe4d..f032636af0bc35 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/connection_status/connection_status.js @@ -10,7 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiText } from '@elastic/eui'; -export function ConnectionStatus({ isConnected }) { +import { SNIFF_MODE, PROXY_MODE } from '../../../../../../common/constants'; + +export function ConnectionStatus({ isConnected, mode }) { let icon; let message; @@ -47,13 +49,16 @@ export function ConnectionStatus({ isConnected }) { - - - + {!isConnected && mode === SNIFF_MODE && ( + + + + )} ); } ConnectionStatus.propTypes = { isConnected: PropTypes.bool, + mode: PropTypes.oneOf([SNIFF_MODE, PROXY_MODE]), }; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js index 1c8ba372aa745a..89a48927f68338 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/detail_panel/detail_panel.js @@ -7,10 +7,13 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { + EuiBadge, EuiButton, EuiButtonEmpty, + EuiCallOut, EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, @@ -21,6 +24,7 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiIcon, + EuiLink, EuiSpacer, EuiText, EuiTextColor, @@ -28,9 +32,11 @@ import { } from '@elastic/eui'; import { CRUD_APP_BASE_PATH } from '../../../constants'; +import { PROXY_MODE } from '../../../../../common/constants'; import { getRouterLinkProps } from '../../../services'; import { ConfiguredByNodeWarning } from '../../components'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; +import { proxyModeUrl } from '../../../services/documentation'; export class DetailPanel extends Component { static propTypes = { @@ -106,139 +112,312 @@ export class DetailPanel extends Component { ); } - renderCluster({ + renderClusterWithDeprecatedSettingWarning( + { hasDeprecatedProxySetting, isConfiguredByNode }, + clusterName + ) { + if (!hasDeprecatedProxySetting) { + return null; + } + return ( + <> + + } + color="warning" + iconType="help" + > + + + + ) : ( + + + + ), + }} + /> + + + + ); + } + + renderSniffModeDescriptionList({ isConnected, connectedNodesCount, skipUnavailable, seeds, maxConnectionsPerCluster, initialConnectTimeout, + mode, }) { return ( -
- -

- -

-
+ + + + + + + + + + + + + + + + + + + + + + + {connectedNodesCount} + + + - - - - - - - - + + + + + + + + + + {seeds.map(seed => ( + {seed} + ))} + + - - - - + + + + + + + + + {this.renderSkipUnavailableValue(skipUnavailable)} + + + - - - - - - + - - {connectedNodesCount} - - - + + + + + + + + + + {maxConnectionsPerCluster} + + - + + + + + + + + + {initialConnectTimeout} + + + + + ); + } - - - - - - - - - - {seeds.map(seed => ( - {seed} - ))} - - + renderProxyModeDescriptionList({ + isConnected, + skipUnavailable, + initialConnectTimeout, + proxyAddress, + proxySocketConnections, + connectedSocketsCount, + mode, + }) { + return ( + + + + + + + + + + + + + - - - - - - + + + + + + + + + {connectedSocketsCount ? connectedSocketsCount : '-'} + + + - - {this.renderSkipUnavailableValue(skipUnavailable)} - - - + - + + + + + + + + + + {proxyAddress} + + - - - - - - - + + + + + + + + + {this.renderSkipUnavailableValue(skipUnavailable)} + + + - - {maxConnectionsPerCluster} - - + - - - - - - + + + + + + + + + + {proxySocketConnections ? proxySocketConnections : '-'} + + - - {initialConnectTimeout} - - - - + + + + + + + + + {initialConnectTimeout} + + + + + ); + } + + renderCluster(cluster) { + return ( +
+ +

+ +

+
+ + + + {cluster.mode === PROXY_MODE + ? this.renderProxyModeDescriptionList(cluster) + : this.renderSniffModeDescriptionList(cluster)}
); } renderFlyoutBody() { - const { cluster } = this.props; + const { cluster, clusterName } = this.props; return ( @@ -246,6 +425,7 @@ export class DetailPanel extends Component { {cluster && ( {this.renderClusterConfiguredByNodeWarning(cluster)} + {this.renderClusterWithDeprecatedSettingWarning(cluster, clusterName)} {this.renderCluster(cluster)} )} @@ -315,7 +495,7 @@ export class DetailPanel extends Component { } render() { - const { isOpen, closeDetailPanel, clusterName } = this.props; + const { isOpen, closeDetailPanel, clusterName, cluster } = this.props; if (!isOpen) { return null; @@ -327,16 +507,33 @@ export class DetailPanel extends Component { onClose={closeDetailPanel} aria-labelledby="remoteClusterDetailsFlyoutTitle" size="m" - maxWidth={400} + maxWidth={550} > - -

{clusterName}

-
+ + + +

{clusterName}

+
+
+ {cluster && cluster.mode === PROXY_MODE ? ( + + {' '} + + {cluster.mode} + + + ) : null} +
{this.renderFlyoutBody()} diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js index 62c417b19904ac..ec20805ccd9192 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import { CRUD_APP_BASE_PATH, UIM_SHOW_DETAILS_CLICK } from '../../../constants'; +import { PROXY_MODE } from '../../../../../common/constants'; import { getRouterLinkProps, trackUiMetric, METRIC_TYPE } from '../../../services'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; @@ -83,7 +84,7 @@ export class RemoteClusterTable extends Component { }), sortable: true, truncateText: false, - render: (name, { isConfiguredByNode }) => { + render: (name, { isConfiguredByNode, hasDeprecatedProxySetting }) => { const link = ( + + {link} + + + + + } + /> + + + ); + } + return link; }, }, - { - field: 'seeds', - name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.seedsColumnTitle', { - defaultMessage: 'Seeds', - }), - truncateText: true, - render: seeds => seeds.join(', '), - }, { field: 'isConnected', name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.connectedColumnTitle', { - defaultMessage: 'Connection', + defaultMessage: 'Status', }), sortable: true, - render: isConnected => , + render: (isConnected, { mode }) => ( + + ), width: '240px', }, { - field: 'connectedNodesCount', + field: 'mode', + name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.modeColumnTitle', { + defaultMessage: 'Mode', + }), + sortable: true, + render: mode => + mode === PROXY_MODE + ? mode + : i18n.translate('xpack.remoteClusters.remoteClusterList.table.sniffModeDescription', { + defaultMessage: 'default', + }), + }, + { + field: 'mode', + name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.addressesColumnTitle', { + defaultMessage: 'Addresses', + }), + truncateText: true, + render: (mode, { seeds, proxyAddress }) => { + if (mode === PROXY_MODE) { + return proxyAddress; + } + return seeds.join(', '); + }, + }, + { + field: 'mode', name: i18n.translate( - 'xpack.remoteClusters.remoteClusterList.table.connectedNodesColumnTitle', + 'xpack.remoteClusters.remoteClusterList.table.connectionsColumnTitle', { - defaultMessage: 'Connected nodes', + defaultMessage: 'Connections', } ), sortable: true, width: '160px', + render: (mode, { connectedNodesCount, connectedSocketsCount }) => { + if (mode === PROXY_MODE) { + return connectedSocketsCount; + } + return connectedNodesCount; + }, }, { name: i18n.translate('xpack.remoteClusters.remoteClusterList.table.actionsColumnTitle', { diff --git a/x-pack/plugins/remote_clusters/public/application/services/documentation.ts b/x-pack/plugins/remote_clusters/public/application/services/documentation.ts index 38cf2223a313bc..f6f5dc987c2eb7 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/documentation.ts +++ b/x-pack/plugins/remote_clusters/public/application/services/documentation.ts @@ -9,6 +9,7 @@ import { DocLinksStart } from 'kibana/public'; export let skippingDisconnectedClustersUrl: string; export let remoteClustersUrl: string; export let transportPortUrl: string; +export let proxyModeUrl: string; export function init(docLinks: DocLinksStart): void { const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; @@ -17,4 +18,5 @@ export function init(docLinks: DocLinksStart): void { skippingDisconnectedClustersUrl = `${esDocBasePath}/modules-cross-cluster-search.html#_skipping_disconnected_clusters`; remoteClustersUrl = `${esDocBasePath}/modules-remote-clusters.html`; transportPortUrl = `${esDocBasePath}/modules-transport.html`; + proxyModeUrl = `${esDocBasePath}/modules-remote-clusters.html#proxy-mode`; } diff --git a/x-pack/plugins/remote_clusters/public/application/services/index.js b/x-pack/plugins/remote_clusters/public/application/services/index.js index 031770d9500ed2..387a04b6e5d8c3 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/index.js +++ b/x-pack/plugins/remote_clusters/public/application/services/index.js @@ -10,7 +10,7 @@ export { showApiError, showApiWarning } from './api_errors'; export { initRedirect, redirect } from './redirect'; -export { isSeedNodeValid, isSeedNodePortValid } from './validate_seed_node'; +export { isAddressValid, isPortValid } from './validate_address'; export { extractQueryParams } from './query_params'; diff --git a/x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.js b/x-pack/plugins/remote_clusters/public/application/services/validate_address.js similarity index 91% rename from x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.js rename to x-pack/plugins/remote_clusters/public/application/services/validate_address.js index 714b5cf44de23f..7e12b9c06595d2 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.js +++ b/x-pack/plugins/remote_clusters/public/application/services/validate_address.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export function isSeedNodeValid(seedNode) { +export function isAddressValid(seedNode) { if (!seedNode) { return false; } @@ -23,7 +23,7 @@ export function isSeedNodeValid(seedNode) { return !containsInvalidCharacters; } -export function isSeedNodePortValid(seedNode) { +export function isPortValid(seedNode) { if (!seedNode) { return false; } diff --git a/x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.test.js b/x-pack/plugins/remote_clusters/public/application/services/validate_address.test.js similarity index 55% rename from x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.test.js rename to x-pack/plugins/remote_clusters/public/application/services/validate_address.test.js index 36e989a41b066c..2551f4fac7908b 100644 --- a/x-pack/plugins/remote_clusters/public/application/services/validate_seed_node.test.js +++ b/x-pack/plugins/remote_clusters/public/application/services/validate_address.test.js @@ -4,75 +4,75 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isSeedNodeValid, isSeedNodePortValid } from './validate_seed_node'; +import { isAddressValid, isPortValid } from './validate_address'; -describe('Validate seed node', () => { +describe('Validate address', () => { describe('isSeedNodeValid', () => { describe('rejects', () => { it('adjacent periods', () => { - expect(isSeedNodeValid('a..b')).toBe(false); + expect(isAddressValid('a..b')).toBe(false); }); it('underscores', () => { - expect(isSeedNodeValid('____')).toBe(false); + expect(isAddressValid('____')).toBe(false); }); ['/', '\\', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '=', '+', '?'].forEach(char => { it(char, () => { - expect(isSeedNodeValid(char)).toBe(false); + expect(isAddressValid(char)).toBe(false); }); }); }); describe('accepts', () => { it('uppercase letters', () => { - expect(isSeedNodeValid('A.B.C.D')).toBe(true); + expect(isAddressValid('A.B.C.D')).toBe(true); }); it('lowercase letters', () => { - expect(isSeedNodeValid('a')).toBe(true); + expect(isAddressValid('a')).toBe(true); }); it('numbers', () => { - expect(isSeedNodeValid('56546354')).toBe(true); + expect(isAddressValid('56546354')).toBe(true); }); it('dashes', () => { - expect(isSeedNodeValid('----')).toBe(true); + expect(isAddressValid('----')).toBe(true); }); it('many parts', () => { - expect(isSeedNodeValid('abcd.efgh.ijkl.mnop.qrst.uvwx.yz')).toBe(true); + expect(isAddressValid('abcd.efgh.ijkl.mnop.qrst.uvwx.yz')).toBe(true); }); }); }); - describe('isSeedNodePortValid', () => { + describe('isPortValid', () => { describe('rejects', () => { it('missing port', () => { - expect(isSeedNodePortValid('abcd')).toBe(false); + expect(isPortValid('abcd')).toBe(false); }); it('empty port', () => { - expect(isSeedNodePortValid('abcd:')).toBe(false); + expect(isPortValid('abcd:')).toBe(false); }); it('letters', () => { - expect(isSeedNodePortValid('ab:cd')).toBe(false); + expect(isPortValid('ab:cd')).toBe(false); }); it('non-numbers', () => { - expect(isSeedNodePortValid('ab:5 0')).toBe(false); + expect(isPortValid('ab:5 0')).toBe(false); }); it('multiple ports', () => { - expect(isSeedNodePortValid('ab:cd:9000')).toBe(false); + expect(isPortValid('ab:cd:9000')).toBe(false); }); }); describe('accepts', () => { it('a single numeric port, even beyond the standard port range', () => { - expect(isSeedNodePortValid('abcd:100000000')).toBe(true); + expect(isPortValid('abcd:100000000')).toBe(true); }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index a6edd15995d728..34d741aa4b7da9 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -80,7 +80,7 @@ describe('ADD remote clusters', () => { }; describe('success', () => { - addRemoteClustersTest('adds remote cluster', { + addRemoteClustersTest(`adds remote cluster with "sniff" mode`, { apiResponses: [ async () => ({}), async () => ({ @@ -106,6 +106,7 @@ describe('ADD remote clusters', () => { payload: { name: 'test', seeds: ['127.0.0.1:9300'], + mode: 'sniff', skipUnavailable: false, }, asserts: { @@ -117,7 +118,79 @@ describe('ADD remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, + }, + }, + }, + }, + ], + ], + statusCode: 200, + result: { + acknowledged: true, + }, + }, + }); + addRemoteClustersTest(`adds remote cluster with "proxy" mode`, { + apiResponses: [ + async () => ({}), + async () => ({ + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + mode: 'proxy', + seeds: ['127.0.0.1:9300'], + num_sockets_connected: 1, + max_socket_connections: 18, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }, + }, + }, + transient: {}, + }), + ], + payload: { + name: 'test', + proxyAddress: '127.0.0.1:9300', + mode: 'proxy', + skipUnavailable: false, + serverName: 'foobar', + }, + asserts: { + apiArguments: [ + ['cluster.remoteInfo'], + [ + 'cluster.putSettings', + { + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: null, + skip_unavailable: false, + mode: 'proxy', + node_connections: null, + proxy_address: '127.0.0.1:9300', + proxy_socket_connections: null, + server_name: 'foobar', + }, + }, }, }, }, @@ -151,6 +224,7 @@ describe('ADD remote clusters', () => { name: 'test', seeds: ['127.0.0.1:9300'], skipUnavailable: false, + mode: 'sniff', }, asserts: { apiArguments: [['cluster.remoteInfo']], @@ -167,6 +241,7 @@ describe('ADD remote clusters', () => { name: 'test', seeds: ['127.0.0.1:9300'], skipUnavailable: false, + mode: 'sniff', }, asserts: { apiArguments: [ @@ -177,7 +252,17 @@ describe('ADD remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, }, }, }, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index e4ede01ca23ea4..5e0fce82376e0a 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -9,17 +9,22 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; -import { serializeCluster } from '../../../common/lib'; +import { serializeCluster, Cluster } from '../../../common/lib'; import { doesClusterExist } from '../../lib/does_cluster_exist'; -import { API_BASE_PATH } from '../../../common/constants'; +import { API_BASE_PATH, PROXY_MODE, SNIFF_MODE } from '../../../common/constants'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; import { RouteDependencies } from '../../types'; const bodyValidation = schema.object({ name: schema.string(), - seeds: schema.arrayOf(schema.string()), skipUnavailable: schema.boolean(), + mode: schema.oneOf([schema.literal(PROXY_MODE), schema.literal(SNIFF_MODE)]), + seeds: schema.nullable(schema.arrayOf(schema.string())), + nodeConnections: schema.nullable(schema.number()), + proxyAddress: schema.nullable(schema.string()), + proxySocketConnections: schema.nullable(schema.number()), + serverName: schema.nullable(schema.string()), }); type RouteBody = TypeOf; @@ -33,7 +38,7 @@ export const register = (deps: RouteDependencies): void => { try { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; - const { name, seeds, skipUnavailable } = request.body; + const { name } = request.body; // Check if cluster already exists. const existingCluster = await doesClusterExist(callAsCurrentUser, name); @@ -50,7 +55,7 @@ export const register = (deps: RouteDependencies): void => { }); } - const addClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); + const addClusterPayload = serializeCluster(request.body as Cluster); const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { body: addClusterPayload, }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index 04deb62d2c2d26..cf14f8a67054ec 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -113,7 +113,17 @@ describe('DELETE remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: null, skip_unavailable: null } }, + remote: { + test: { + seeds: null, + skip_unavailable: null, + mode: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + node_connections: null, + }, + }, }, }, }, @@ -211,7 +221,17 @@ describe('DELETE remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: null, skip_unavailable: null } }, + remote: { + test: { + seeds: null, + skip_unavailable: null, + mode: null, + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, }, }, }, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index 90955be85859d4..d81b50f1148de4 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -89,6 +89,7 @@ describe('GET remote clusters', () => { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false, + mode: 'sniff', }, }, }, @@ -120,6 +121,7 @@ describe('GET remote clusters', () => { initialConnectTimeout: '30s', skipUnavailable: false, isConfiguredByNode: false, + mode: 'sniff', }, ], }, @@ -170,6 +172,7 @@ describe('GET remote clusters', () => { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false, + mode: 'sniff', }, }, }, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index 44b6284109ac54..abd44977d8e46f 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -33,13 +33,28 @@ export const register = (deps: RouteDependencies): void => { const cluster = clustersByName[clusterName]; const isTransient = transientClusterNames.includes(clusterName); const isPersistent = persistentClusterNames.includes(clusterName); + // If the cluster hasn't been stored in the cluster state, then it's defined by the // node's config file. const isConfiguredByNode = !isTransient && !isPersistent; + // Pre-7.6, ES supported an undocumented "proxy" field + // ES does not handle migrating this to the new implementation, so we need to surface it in the UI + // This value is not available via the GET /_remote/info API, so we get it from the cluster settings + const deprecatedProxyAddress = isPersistent + ? get(clusterSettings, `persistent.cluster.remote[${clusterName}].proxy`, undefined) + : undefined; + + // server_name is not available via the GET /_remote/info API, so we get it from the cluster settings + // Per https://github.com/elastic/kibana/pull/26067#issuecomment-441848124, we only look at persistent settings + const serverName = isPersistent + ? get(clusterSettings, `persistent.cluster.remote[${clusterName}].server_name`, undefined) + : undefined; + return { - ...deserializeCluster(clusterName, cluster), + ...deserializeCluster(clusterName, cluster, deprecatedProxyAddress), isConfiguredByNode, + serverName, }; }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 9ba239c3ff6616..84ba9587ddfa62 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -129,6 +129,7 @@ describe('UPDATE remote clusters', () => { payload: { seeds: ['127.0.0.1:9300'], skipUnavailable: true, + mode: 'sniff', }, asserts: { apiArguments: [ @@ -139,7 +140,17 @@ describe('UPDATE remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: true } }, + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: true, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, }, }, }, @@ -156,6 +167,7 @@ describe('UPDATE remote clusters', () => { name: 'test', seeds: ['127.0.0.1:9300'], skipUnavailable: true, + mode: 'sniff', }, }, }); @@ -167,6 +179,7 @@ describe('UPDATE remote clusters', () => { payload: { seeds: ['127.0.0.1:9300'], skipUnavailable: false, + mode: 'sniff', }, params: { name: 'test', @@ -198,6 +211,7 @@ describe('UPDATE remote clusters', () => { payload: { seeds: ['127.0.0.1:9300'], skipUnavailable: false, + mode: 'sniff', }, params: { name: 'test', @@ -211,7 +225,17 @@ describe('UPDATE remote clusters', () => { body: { persistent: { cluster: { - remote: { test: { seeds: ['127.0.0.1:9300'], skip_unavailable: false } }, + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + }, + }, }, }, }, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index ed584307d84c11..14b161b6f26b5a 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -9,16 +9,21 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { RequestHandler } from 'src/core/server'; -import { API_BASE_PATH } from '../../../common/constants'; -import { serializeCluster, deserializeCluster } from '../../../common/lib'; +import { API_BASE_PATH, SNIFF_MODE, PROXY_MODE } from '../../../common/constants'; +import { serializeCluster, deserializeCluster, Cluster, ClusterEs } from '../../../common/lib'; import { doesClusterExist } from '../../lib/does_cluster_exist'; import { RouteDependencies } from '../../types'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; import { isEsError } from '../../lib/is_es_error'; const bodyValidation = schema.object({ - seeds: schema.arrayOf(schema.string()), skipUnavailable: schema.boolean(), + mode: schema.oneOf([schema.literal(PROXY_MODE), schema.literal(SNIFF_MODE)]), + seeds: schema.nullable(schema.arrayOf(schema.string())), + nodeConnections: schema.nullable(schema.number()), + proxyAddress: schema.nullable(schema.string()), + proxySocketConnections: schema.nullable(schema.number()), + serverName: schema.nullable(schema.string()), }); const paramsValidation = schema.object({ @@ -39,7 +44,6 @@ export const register = (deps: RouteDependencies): void => { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; const { name } = request.params; - const { seeds, skipUnavailable } = request.body; // Check if cluster does exist. const existingCluster = await doesClusterExist(callAsCurrentUser, name); @@ -57,13 +61,14 @@ export const register = (deps: RouteDependencies): void => { } // Update cluster as new settings - const updateClusterPayload = serializeCluster({ name, seeds, skipUnavailable }); + const updateClusterPayload = serializeCluster({ ...request.body, name } as Cluster); + const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { body: updateClusterPayload, }); const acknowledged = get(updateClusterResponse, 'acknowledged'); - const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); + const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`) as ClusterEs; if (acknowledged && cluster) { const body = { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ad49f0242e8e6e..9b842f736b8bb5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10081,10 +10081,7 @@ "xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "保存", "xpack.remoteClusters.remoteClusterForm.sectionNameDescription": "リモートクラスターの固有の名前です。", "xpack.remoteClusters.remoteClusterForm.sectionNameTitle": "名前", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsDescription1": "クラスターステータスのクエリを実行するリモートクラスターノードのリストです。1 つのノードが利用できない場合にディスカバリが失敗しないよう、複数シードノードを指定してください。", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText": "リモートクラスターの {transportPort} の前にくる IP アドレスまたはホスト名です。", "xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.transportPortLinkText": "トランスポートポート", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsTitle": "クラスターディスカバリのシードノード", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription": "デフォルトで、リクエストのリモートクラスターのどれかが利用できないと、リクエストは失敗となります。このクラスターが利用できない場合にリクエストを他のリモートクラスターに送信し続けるには、{optionName} を有効にします。{learnMoreLink}", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel": "詳細", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel": "利用不可の場合スキップ", @@ -10106,11 +10103,9 @@ "xpack.remoteClusters.remoteClusterList.table.actionEditDescription": "リモートクラスターを編集します", "xpack.remoteClusters.remoteClusterList.table.actionsColumnTitle": "アクション", "xpack.remoteClusters.remoteClusterList.table.connectedColumnTitle": "接続", - "xpack.remoteClusters.remoteClusterList.table.connectedNodesColumnTitle": "接続済みのノード", "xpack.remoteClusters.remoteClusterList.table.isConfiguredByNodeMessage": "elasticsearch.yml で定義されています", "xpack.remoteClusters.remoteClusterList.table.nameColumnTitle": "名前", "xpack.remoteClusters.remoteClusterList.table.removeButtonLabel": "{count, plural, one {リモートクラスター} other {{count}リモートクラスター}}を削除", - "xpack.remoteClusters.remoteClusterList.table.seedsColumnTitle": "シード", "xpack.remoteClusters.remoteClusterListTitle": "リモートクラスター", "xpack.remoteClusters.removeAction.errorMultipleNotificationTitle": "「{count}」リモートクラスターの削除中にエラーが発生", "xpack.remoteClusters.removeAction.errorSingleNotificationTitle": "リモートクラスター「{name}」の削除中にエラーが発生", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 76ecf333eb10a4..97c30f180dfe1f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10081,10 +10081,7 @@ "xpack.remoteClusters.remoteClusterForm.saveButtonLabel": "保存", "xpack.remoteClusters.remoteClusterForm.sectionNameDescription": "远程集群的唯一名称。", "xpack.remoteClusters.remoteClusterForm.sectionNameTitle": "名称", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsDescription1": "要查询集群状态的远程集群节点的列表。指定多个种子节点,以便在节点不可用时发现不会失败。", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText": "IP 地址或主机名,后跟远程集群的 {transportPort}。", "xpack.remoteClusters.remoteClusterForm.sectionSeedsHelpText.transportPortLinkText": "传输端口", - "xpack.remoteClusters.remoteClusterForm.sectionSeedsTitle": "用于集群发现的种子节点", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription": "默认情况下,如果任何查询的远程集群不可用,请求将失败。要在此集群不可用时继续向其他远程集群发送请求,请启用 {optionName}。{learnMoreLink}", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.learnMoreLinkLabel": "了解详情。", "xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableDescription.optionNameLabel": "如果不可用,则跳过", @@ -10106,11 +10103,9 @@ "xpack.remoteClusters.remoteClusterList.table.actionEditDescription": "编辑远程集群", "xpack.remoteClusters.remoteClusterList.table.actionsColumnTitle": "操作", "xpack.remoteClusters.remoteClusterList.table.connectedColumnTitle": "连接", - "xpack.remoteClusters.remoteClusterList.table.connectedNodesColumnTitle": "已连接节点", "xpack.remoteClusters.remoteClusterList.table.isConfiguredByNodeMessage": "在 elasticsearch.yml 中定义", "xpack.remoteClusters.remoteClusterList.table.nameColumnTitle": "名称", "xpack.remoteClusters.remoteClusterList.table.removeButtonLabel": "删除 {count, plural, one { 个远程集群} other {{count} 个远程集群}}", - "xpack.remoteClusters.remoteClusterList.table.seedsColumnTitle": "种子", "xpack.remoteClusters.remoteClusterListTitle": "远程集群", "xpack.remoteClusters.removeAction.errorMultipleNotificationTitle": "删除 “{count}” 个远程集群时出错", "xpack.remoteClusters.removeAction.errorSingleNotificationTitle": "删除远程集群 “{name}” 时出错", diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/remote_clusters.helpers.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/remote_clusters.helpers.js index d8cee1db9a2bcf..4462fcf75d5d85 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/remote_clusters.helpers.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/remote_clusters.helpers.js @@ -25,7 +25,8 @@ export const registerHelpers = supertest => { .post(`${REMOTE_CLUSTERS_API_BASE_PATH}`) .set('kbn-xsrf', 'xxx') .send({ - name: name, + name, + mode: 'sniff', seeds: [`localhost:${esTransportPort}`], skipUnavailable: true, }); diff --git a/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js b/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js index 677d22ff749847..7921186000e193 100644 --- a/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js +++ b/x-pack/test/api_integration/apis/management/remote_clusters/remote_clusters.js @@ -40,6 +40,7 @@ export default function({ getService }) { name: 'test_cluster', seeds: [NODE_SEED], skipUnavailable: true, + mode: 'sniff', }) .expect(200); @@ -58,6 +59,7 @@ export default function({ getService }) { name: 'test_cluster', seeds: [NODE_SEED], skipUnavailable: false, + mode: 'sniff', }) .expect(409); @@ -79,6 +81,7 @@ export default function({ getService }) { .send({ skipUnavailable: false, seeds: [NODE_SEED], + mode: 'sniff', }) .expect(200); @@ -87,6 +90,7 @@ export default function({ getService }) { skipUnavailable: 'false', // ES issue #35671 seeds: [NODE_SEED], isConfiguredByNode: false, + mode: 'sniff', }); }); }); @@ -109,6 +113,7 @@ export default function({ getService }) { initialConnectTimeout: '30s', skipUnavailable: false, isConfiguredByNode: false, + mode: 'sniff', }, ]); }); @@ -139,6 +144,7 @@ export default function({ getService }) { name: 'test_cluster1', seeds: [NODE_SEED], skipUnavailable: true, + mode: 'sniff', }) .expect(200); @@ -149,6 +155,7 @@ export default function({ getService }) { name: 'test_cluster2', seeds: [NODE_SEED], skipUnavailable: true, + mode: 'sniff', }) .expect(200); From 93914b6cb5afbc382699818c1beab6568a5dabac Mon Sep 17 00:00:00 2001 From: marshallmain <55718608+marshallmain@users.noreply.github.com> Date: Mon, 16 Mar 2020 15:53:49 -0400 Subject: [PATCH 10/17] [Endpoint] Sample data generator CLI script (#59952) * start on cli * make it work * cleanup * remove failed attempt code * update package and tsconfig * remove empty file * generate resolver events from multiple endpoints * re-add child randomization * align index names with real plugin * remove duplication * better naming * add temporary mapping to sample data generator * error handling, move tsconfig * add readme * Update README.md * move mapping from common to scripts * make delete index option * remove unnecessary map call * fix import style Co-authored-by: Elastic Machine --- .../endpoint/common/generate_data.test.ts | 2 +- .../plugins/endpoint/common/generate_data.ts | 111 +- x-pack/plugins/endpoint/package.json | 7 +- .../endpoint/store/managing/index.test.ts | 2 +- .../store/managing/middleware.test.ts | 2 +- x-pack/plugins/endpoint/scripts/README.md | 46 + .../endpoint/scripts/cli_tsconfig.json | 8 + x-pack/plugins/endpoint/scripts/mapping.json | 2367 +++++++++++++++++ .../endpoint/scripts/resolver_generator.ts | 161 ++ 9 files changed, 2634 insertions(+), 72 deletions(-) create mode 100644 x-pack/plugins/endpoint/scripts/README.md create mode 100644 x-pack/plugins/endpoint/scripts/cli_tsconfig.json create mode 100644 x-pack/plugins/endpoint/scripts/mapping.json create mode 100644 x-pack/plugins/endpoint/scripts/resolver_generator.ts diff --git a/x-pack/plugins/endpoint/common/generate_data.test.ts b/x-pack/plugins/endpoint/common/generate_data.test.ts index ebe3c25eef829c..e14f506c825f2c 100644 --- a/x-pack/plugins/endpoint/common/generate_data.test.ts +++ b/x-pack/plugins/endpoint/common/generate_data.test.ts @@ -151,7 +151,7 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const root = generator.generateEvent({ timestamp }); const generations = 2; - const events = generator.generateDescendantsTree(root, generations); + const events = [root, ...generator.generateDescendantsTree(root, generations)]; const rootNode = buildResolverTree(events); const visitedEvents = countResolverEvents(rootNode, generations); expect(visitedEvents).toEqual(events.length); diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index a91cf0ffca7836..b539e309d76f72 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -6,7 +6,7 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; -import { AlertEvent, EndpointEvent, EndpointMetadata, OSFields } from './types'; +import { AlertEvent, EndpointEvent, EndpointMetadata, OSFields, HostFields } from './types'; export type Event = AlertEvent | EndpointEvent; @@ -67,31 +67,51 @@ const FILE_OPERATIONS: string[] = ['creation', 'open', 'rename', 'execution', 'd // These are from the v1 schemas and aren't all valid ECS event categories, still in flux const OTHER_EVENT_CATEGORIES: string[] = ['driver', 'file', 'library', 'network', 'registry']; +interface HostInfo { + agent: { + version: string; + id: string; + }; + host: HostFields; + endpoint: { + policy: { + id: string; + }; + }; +} + export class EndpointDocGenerator { - agentId: string; - hostId: string; - hostname: string; - macAddress: string[]; - ip: string[]; - agentVersion: string; - os: OSFields; - policy: { name: string; id: string }; + commonInfo: HostInfo; random: seedrandom.prng; constructor(seed = Math.random().toString()) { this.random = seedrandom(seed); - this.hostId = this.seededUUIDv4(); - this.agentId = this.seededUUIDv4(); - this.hostname = this.randomHostname(); - this.ip = this.randomArray(3, () => this.randomIP()); - this.macAddress = this.randomArray(3, () => this.randomMac()); - this.agentVersion = this.randomVersion(); - this.os = this.randomChoice(OS); - this.policy = this.randomChoice(POLICIES); + this.commonInfo = this.createHostData(); } - public randomizeIPs() { - this.ip = this.randomArray(3, () => this.randomIP()); + // This function will create new values for all the host fields, so documents from a different endpoint can be created + // This provides a convenient way to make documents from multiple endpoints that are all tied to a single seed value + public randomizeHostData() { + this.commonInfo = this.createHostData(); + } + + private createHostData(): HostInfo { + return { + agent: { + version: this.randomVersion(), + id: this.seededUUIDv4(), + }, + host: { + id: this.seededUUIDv4(), + hostname: this.randomHostname(), + ip: this.randomArray(3, () => this.randomIP()), + mac: this.randomArray(3, () => this.randomMac()), + os: this.randomChoice(OS), + }, + endpoint: { + policy: this.randomChoice(POLICIES), + }, + }; } public generateEndpointMetadata(ts = new Date().getTime()): EndpointMetadata { @@ -100,22 +120,7 @@ export class EndpointDocGenerator { event: { created: ts, }, - endpoint: { - policy: { - id: this.policy.id, - }, - }, - agent: { - version: this.agentVersion, - id: this.agentId, - }, - host: { - id: this.hostId, - hostname: this.hostname, - ip: this.ip, - mac: this.macAddress, - os: this.os, - }, + ...this.commonInfo, }; } @@ -125,11 +130,8 @@ export class EndpointDocGenerator { parentEntityID?: string ): AlertEvent { return { + ...this.commonInfo, '@timestamp': ts, - agent: { - id: this.agentId, - version: this.agentVersion, - }, event: { action: this.randomChoice(FILE_OPERATIONS), kind: 'alert', @@ -139,11 +141,6 @@ export class EndpointDocGenerator { module: 'endpoint', type: 'creation', }, - endpoint: { - policy: { - id: this.policy.id, - }, - }, file: { owner: 'SYSTEM', name: 'fake_malware.exe', @@ -169,13 +166,6 @@ export class EndpointDocGenerator { }, temp_file_path: 'C:/temp/fake_malware.exe', }, - host: { - id: this.hostId, - hostname: this.hostname, - ip: this.ip, - mac: this.macAddress, - os: this.os, - }, process: { pid: 2, name: 'malware writer', @@ -243,11 +233,7 @@ export class EndpointDocGenerator { public generateEvent(options: EventOptions = {}): EndpointEvent { return { '@timestamp': options.timestamp ? options.timestamp : new Date().getTime(), - agent: { - id: this.agentId, - version: this.agentVersion, - type: 'endpoint', - }, + agent: { ...this.commonInfo.agent, type: 'endgame' }, ecs: { version: '1.4.0', }, @@ -257,13 +243,7 @@ export class EndpointDocGenerator { type: options.eventType ? options.eventType : 'creation', id: this.seededUUIDv4(), }, - host: { - id: this.hostId, - hostname: this.hostname, - ip: this.ip, - mac: this.macAddress, - os: this.os, - }, + host: this.commonInfo.host, process: { entity_id: options.entityID ? options.entityID : this.randomString(10), parent: options.parentEntityID ? { entity_id: options.parentEntityID } : undefined, @@ -323,14 +303,13 @@ export class EndpointDocGenerator { percentNodesWithRelated = 100, percentChildrenTerminated = 100 ): Event[] { - let events: Event[] = [root]; + let events: Event[] = []; let parents = [root]; let timestamp = root['@timestamp']; for (let i = 0; i < generations; i++) { const newParents: EndpointEvent[] = []; parents.forEach(element => { - // const numChildren = randomN(maxChildrenPerNode); - const numChildren = maxChildrenPerNode; + const numChildren = this.randomN(maxChildrenPerNode); for (let j = 0; j < numChildren; j++) { timestamp = timestamp + 1000; const child = this.generateEvent({ diff --git a/x-pack/plugins/endpoint/package.json b/x-pack/plugins/endpoint/package.json index c7ba8b3fb41960..fc4f4bd586bef7 100644 --- a/x-pack/plugins/endpoint/package.json +++ b/x-pack/plugins/endpoint/package.json @@ -4,10 +4,11 @@ "version": "0.0.0", "private": true, "license": "Elastic-License", - "scripts": {}, + "scripts": { + "test:generate": "ts-node --project scripts/cli_tsconfig.json scripts/resolver_generator.ts" + }, "dependencies": { - "react-redux": "^7.1.0", - "seedrandom": "^3.0.5" + "react-redux": "^7.1.0" }, "devDependencies": { "@types/seedrandom": ">=2.0.0 <4.0.0", diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts index fba1dacb0d3bdc..e435fded13f4ce 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/index.test.ts @@ -20,7 +20,7 @@ describe('endpoint_list store concerns', () => { dispatch = store.dispatch; }; const generateEndpoint = (): EndpointMetadata => { - return generator.generateEndpointMetadata(new Date().getTime()); + return generator.generateEndpointMetadata(); }; const loadDataToStore = () => { dispatch({ diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts index d98dc82624149e..459a1789a58da7 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/managing/middleware.test.ts @@ -27,7 +27,7 @@ describe('endpoint list saga', () => { const generator = new EndpointDocGenerator(); // https://github.com/elastic/endpoint-app-team/issues/131 const generateEndpoint = (): EndpointMetadata => { - return generator.generateEndpointMetadata(new Date().getTime()); + return generator.generateEndpointMetadata(); }; let history: History; diff --git a/x-pack/plugins/endpoint/scripts/README.md b/x-pack/plugins/endpoint/scripts/README.md new file mode 100644 index 00000000000000..f0c8c5a9b0b660 --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/README.md @@ -0,0 +1,46 @@ +This script makes it easy to create the endpoint metadata, alert, and event documents needed to test Resolver in Kibana. +The default behavior is to create 1 endpoint with 1 alert and a moderate number of events (random, typically on the order of 20). +A seed value can be provided as a string for the random number generator for repeatable behavior, useful for demos etc. +Use the `-d` option if you want to delete and remake the indices, otherwise it will add documents to existing indices. + +The sample data generator script depends on ts-node, install with npm: + +```npm install -g ts-node``` + +Example command sequence to get ES and kibana running with sample data after installing ts-node: + +```yarn es snapshot``` -> starts ES + +```npx yarn start --xpack.endpoint.enabled=true --no-base-path``` -> starts kibana + +```cd ~/path/to/kibana/x-pack/plugins/endpoint``` + +```yarn test:generate --auth elastic:changeme``` -> run the resolver_generator.ts script + +Resolver generator CLI options: +```--help Show help [boolean] + --seed, -s random seed to use for document generator [string] + --node, -n elasticsearch node url + [string] [default: "http://localhost:9200"] + --eventIndex, --ei index to store events in + [string] [default: "events-endpoint-1"] + --metadataIndex, --mi index to store endpoint metadata in + [string] [default: "endpoint-agent-1"] + --auth elasticsearch username and password, separated by + a colon [string] + --ancestors, --anc number of ancestors of origin to create + [number] [default: 3] + --generations, --gen number of child generations to create + [number] [default: 3] + --children, --ch maximum number of children per node + [number] [default: 3] + --relatedEvents, --related number of related events to create for each + process event [number] [default: 5] + --percentWithRelated, --pr percent of process events to add related events to + [number] [default: 30] + --percentTerminated, --pt percent of process events to add termination event + for [number] [default: 30] + --numEndpoints, --ne number of different endpoints to generate alerts + for [number] [default: 1] + --alertsPerEndpoint, --ape number of resolver trees to make for each endpoint + [number] [default: 1]``` diff --git a/x-pack/plugins/endpoint/scripts/cli_tsconfig.json b/x-pack/plugins/endpoint/scripts/cli_tsconfig.json new file mode 100644 index 00000000000000..25afe109a42ead --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/cli_tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "target": "es2019", + "resolveJsonModule": true + } + } + \ No newline at end of file diff --git a/x-pack/plugins/endpoint/scripts/mapping.json b/x-pack/plugins/endpoint/scripts/mapping.json new file mode 100644 index 00000000000000..34c039d6435171 --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/mapping.json @@ -0,0 +1,2367 @@ +{ + "mappings": { + "_meta": { + "version": "1.5.0-dev" + }, + "date_detection": false, + "dynamic": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "endpoint": { + "properties": { + "artifact": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "policy": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "entry_modified": { + "type": "double" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "macro": { + "properties": { + "code_page": { + "type": "long" + }, + "collection": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "object" + }, + "errors": { + "properties": { + "count": { + "type": "long" + }, + "error_type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "file_extension": { + "type": "long" + }, + "project_file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "object" + }, + "stream": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code_size": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "temp_file_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_percent": { + "type": "double" + }, + "cwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "env_variables": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "handles": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "memory_percent": { + "type": "double" + }, + "memory_region": { + "properties": { + "allocation_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocation_protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram": { + "properties": { + "histogram_array": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_flavor": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_resolution": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "length": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "permission": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_tag": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "unbacked_on_disk": { + "type": "boolean" + } + }, + "type": "nested" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "num_threads": { + "type": "long" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "phys_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "session_id": { + "type": "long" + }, + "short_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tty_device": { + "properties": { + "major_number": { + "type": "integer" + }, + "minor_number": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "target": { + "properties": { + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_percent": { + "type": "double" + }, + "cwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "env_variables": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "handles": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classifier": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "memory_percent": { + "type": "double" + }, + "memory_region": { + "properties": { + "allocation_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocation_protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram": { + "properties": { + "histogram_array": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_flavor": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_resolution": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "length": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "permission": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_tag": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "unbacked_on_disk": { + "type": "boolean" + } + }, + "type": "nested" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "num_threads": { + "type": "long" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "phys_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "session_id": { + "type": "long" + }, + "short_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tty_device": { + "properties": { + "major_number": { + "type": "integer" + }, + "minor_number": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": 10000 + } + }, + "refresh_interval": "5s" + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts new file mode 100644 index 00000000000000..a3e56497f0790c --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as yargs from 'yargs'; +import { Client, ClientOptions } from '@elastic/elasticsearch'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { EndpointDocGenerator } from '../common/generate_data'; +import { default as mapping } from './mapping.json'; + +main(); + +async function main() { + const argv = yargs.help().options({ + seed: { + alias: 's', + describe: 'random seed to use for document generator', + type: 'string', + }, + node: { + alias: 'n', + describe: 'elasticsearch node url', + default: 'http://localhost:9200', + type: 'string', + }, + eventIndex: { + alias: 'ei', + describe: 'index to store events in', + default: 'events-endpoint-1', + type: 'string', + }, + metadataIndex: { + alias: 'mi', + describe: 'index to store endpoint metadata in', + default: 'endpoint-agent-1', + type: 'string', + }, + auth: { + describe: 'elasticsearch username and password, separated by a colon', + type: 'string', + }, + ancestors: { + alias: 'anc', + describe: 'number of ancestors of origin to create', + type: 'number', + default: 3, + }, + generations: { + alias: 'gen', + describe: 'number of child generations to create', + type: 'number', + default: 3, + }, + children: { + alias: 'ch', + describe: 'maximum number of children per node', + type: 'number', + default: 3, + }, + relatedEvents: { + alias: 'related', + describe: 'number of related events to create for each process event', + type: 'number', + default: 5, + }, + percentWithRelated: { + alias: 'pr', + describe: 'percent of process events to add related events to', + type: 'number', + default: 30, + }, + percentTerminated: { + alias: 'pt', + describe: 'percent of process events to add termination event for', + type: 'number', + default: 30, + }, + numEndpoints: { + alias: 'ne', + describe: 'number of different endpoints to generate alerts for', + type: 'number', + default: 1, + }, + alertsPerEndpoint: { + alias: 'ape', + describe: 'number of resolver trees to make for each endpoint', + type: 'number', + default: 1, + }, + delete: { + alias: 'd', + describe: 'delete indices and remake them', + type: 'boolean', + default: false, + }, + }).argv; + const clientOptions: ClientOptions = { + node: argv.node, + }; + if (argv.auth) { + const [username, password]: string[] = argv.auth.split(':', 2); + clientOptions.auth = { username, password }; + } + const client = new Client(clientOptions); + if (argv.delete) { + try { + await client.indices.delete({ + index: [argv.eventIndex, argv.metadataIndex], + }); + } catch (err) { + if (err instanceof ResponseError && err.statusCode !== 404) { + // eslint-disable-next-line no-console + console.log(err); + process.exit(1); + } + } + } + try { + await client.indices.create({ + index: argv.eventIndex, + body: mapping, + }); + } catch (err) { + if ( + err instanceof ResponseError && + err.body.error.type !== 'resource_already_exists_exception' + ) { + // eslint-disable-next-line no-console + console.log(err.body); + process.exit(1); + } + } + + const generator = new EndpointDocGenerator(argv.seed); + for (let i = 0; i < argv.numEndpoints; i++) { + await client.index({ + index: argv.metadataIndex, + body: generator.generateEndpointMetadata(), + }); + for (let j = 0; j < argv.alertsPerEndpoint; j++) { + const resolverDocs = generator.generateFullResolverTree( + argv.ancestors, + argv.generations, + argv.children, + argv.relatedEvents, + argv.percentWithRelated, + argv.percentTerminated + ); + const body = resolverDocs.reduce( + (array: Array>, doc) => ( + array.push({ index: { _index: argv.eventIndex } }, doc), array + ), + [] + ); + + await client.bulk({ body }); + } + generator.randomizeHostData(); + } +} From 92aec698f5d20630ed194f55b5eea4fd00c5f231 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 16 Mar 2020 21:28:34 +0100 Subject: [PATCH 11/17] Embeddable triggers (#58440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add Embeddable.supportedTriggers() method * feat: 🎸 report supported triggers on visualization embeddable * feat: 🎸 hard-code supportedTriggers() in visualize_embed * feat: 🎸 use VIS_EVENT_TO_TRIGGER when executing trigger * perf: ⚡️ improve type * fix: 🐛 revert back trigger check to how it is done on master * chore: 🤖 remove unused import * feat: 🎸 hard-code triggers for each visualization * fix: 🐛 fix import * feat: 🎸 reshuffle vis_types in supportedTriggers() method Co-authored-by: Elastic Machine --- .../np_ready/public/embeddable/events.ts | 33 +++++++++++++++++ .../public/embeddable/visualize_embeddable.ts | 36 +++++++++++++++---- .../public/lib/embeddables/embeddable.tsx | 5 +++ .../public/lib/embeddables/i_embeddable.ts | 6 ++++ 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts new file mode 100644 index 00000000000000..53d04bf6eb04ac --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/events.ts @@ -0,0 +1,33 @@ +/* + * 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 { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../../../plugins/ui_actions/public'; + +export interface VisEventToTrigger { + ['brush']: typeof SELECT_RANGE_TRIGGER; + ['filter']: typeof VALUE_CLICK_TRIGGER; +} + +export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = { + brush: SELECT_RANGE_TRIGGER, + filter: VALUE_CLICK_TRIGGER, +}; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts index 474912ed508f83..c45e6832dc8362 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/embeddable/visualize_embeddable.ts @@ -36,10 +36,6 @@ import { Container, EmbeddableVisTriggerContext, } from '../../../../../../../plugins/embeddable/public'; -import { - selectRangeTrigger, - valueClickTrigger, -} from '../../../../../../../plugins/ui_actions/public'; import { dispatchRenderComplete } from '../../../../../../../plugins/kibana_utils/public'; import { IExpressionLoaderParams, @@ -50,6 +46,7 @@ import { buildPipeline } from '../legacy/build_pipeline'; import { Vis } from '../vis'; import { getExpressions, getUiActions } from '../services'; import { VisSavedObject } from '../types'; +import { VIS_EVENT_TO_TRIGGER } from './events'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -295,8 +292,8 @@ export class VisualizeEmbeddable extends Embeddable { + return []; + } } diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 62121cb0f23dd5..7fef80edde85f9 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -21,6 +21,7 @@ import { Observable } from 'rxjs'; import { Adapters } from '../types'; import { IContainer } from '../containers/i_container'; import { ViewMode } from '../types'; +import { TriggerContextMapping } from '../../../../ui_actions/public'; export interface EmbeddableInput { viewMode?: ViewMode; @@ -161,4 +162,9 @@ export interface IEmbeddable< * Cleans up subscriptions, destroy nodes mounted from calls to render. */ destroy(): void; + + /** + * List of triggers that this embeddable will execute. + */ + supportedTriggers(): Array; } From c898e799a5fd605177e823e112a4ad2f430fcabc Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Mon, 16 Mar 2020 14:33:56 -0600 Subject: [PATCH 12/17] Migrate dual validated range (#59689) * Move validated range files to new NP location * Update refs in code * Clean up old validated range files * Change relative paths to 'kibana-react'. Some clean up * Change to relative paths * Fix i18n errors * i18n clean up. Export module explicitly * Change files over to TS to prevent build issue where validated range was missing * Clean up TS conversion * More clean up. Extend EuiRangeProps * Remove unneeded ts-ignore * Review feedback and test fixes * Change double to single quotes * min and max aren't always passed, make optional * Type updates * Review feedback. Set state to empty on init and add ignore comment * Review feedback * Add back in last 2 ts-ignores. Build fails without focusable attribute on EuiDualRange & No good alternatives for spread syntax in TS components * Rollback change to state init. Initializing state to null actually triggers a react browser warning and complicates using 'prevState' in getDerivedStateFromProps Co-authored-by: Elastic Machine --- .../public/components/vis/range_control.tsx | 3 +- .../public/legacy_imports.ts | 2 - .../public/components/tag_cloud_options.tsx | 3 +- .../public/legacy_imports.ts | 1 - .../ui/public/validated_range/index.d.ts | 25 --------- src/plugins/kibana_react/public/index.ts | 1 + .../public/validated_range/index.ts} | 0 .../validated_range/is_range_valid.test.ts} | 0 .../public/validated_range/is_range_valid.ts} | 24 ++++++--- .../validated_range/validated_dual_range.tsx} | 54 ++++++++++++------- .../layer_settings/layer_settings.js | 2 +- .../components/size/size_range_selector.js | 2 +- .../translations/translations/ja-JP.json | 3 -- .../translations/translations/zh-CN.json | 3 -- 14 files changed, 58 insertions(+), 65 deletions(-) delete mode 100644 src/legacy/ui/public/validated_range/index.d.ts rename src/{legacy/ui/public/validated_range/index.js => plugins/kibana_react/public/validated_range/index.ts} (100%) rename src/{legacy/ui/public/validated_range/is_range_valid.test.js => plugins/kibana_react/public/validated_range/is_range_valid.test.ts} (100%) rename src/{legacy/ui/public/validated_range/is_range_valid.js => plugins/kibana_react/public/validated_range/is_range_valid.ts} (74%) rename src/{legacy/ui/public/validated_range/validated_dual_range.js => plugins/kibana_react/public/validated_range/validated_dual_range.tsx} (67%) diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx index cd3982afd9afd5..0cd2a2b3319801 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx +++ b/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx @@ -19,8 +19,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; - -import { ValidatedDualRange } from '../../legacy_imports'; +import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; import { FormRow } from './form_row'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts index b6c4eb28e974f2..8c58ac2386da43 100644 --- a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts +++ b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts @@ -22,7 +22,5 @@ import { SearchSource as SearchSourceClass, ISearchSource } from '../../../../pl export { SearchSourceFields } from '../../../../plugins/data/public'; -export { ValidatedDualRange } from 'ui/validated_range'; - export type SearchSource = Class; export const SearchSource = SearchSourceClass; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index ab7c2cd980c424..a9e816f70cf531 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,11 +20,10 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - +import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; import { VisOptionsProps } from '../../../vis_default_editor/public'; import { SelectOption, SwitchOption } from '../../../vis_type_vislib/public'; import { TagCloudVisParams } from '../types'; -import { ValidatedDualRange } from '../legacy_imports'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { const handleFontSizeChange = ([minFontSize, maxFontSize]: [string | number, string | number]) => { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts index d5b442bc5b3468..0d76bc5d8b68b0 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy_imports.ts @@ -18,5 +18,4 @@ */ export { Schemas } from 'ui/agg_types'; -export { ValidatedDualRange } from 'ui/validated_range'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/ui/public/validated_range/index.d.ts b/src/legacy/ui/public/validated_range/index.d.ts deleted file mode 100644 index 50cacbc517be8b..00000000000000 --- a/src/legacy/ui/public/validated_range/index.d.ts +++ /dev/null @@ -1,25 +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 React from 'react'; -import { EuiRangeProps } from '@elastic/eui'; - -export class ValidatedDualRange extends React.Component { - allowEmptyRange?: boolean; -} diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index f04c6f1f19c338..e88ca7178cde39 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -25,6 +25,7 @@ export * from './ui_settings'; export * from './field_icon'; export * from './table_list_view'; export * from './split_panel'; +export { ValidatedDualRange } from './validated_range'; export { Markdown, MarkdownSimple } from './markdown'; export { reactToUiComponent, uiToReactComponent } from './adapters'; export { useUrlTracker } from './use_url_tracker'; diff --git a/src/legacy/ui/public/validated_range/index.js b/src/plugins/kibana_react/public/validated_range/index.ts similarity index 100% rename from src/legacy/ui/public/validated_range/index.js rename to src/plugins/kibana_react/public/validated_range/index.ts diff --git a/src/legacy/ui/public/validated_range/is_range_valid.test.js b/src/plugins/kibana_react/public/validated_range/is_range_valid.test.ts similarity index 100% rename from src/legacy/ui/public/validated_range/is_range_valid.test.js rename to src/plugins/kibana_react/public/validated_range/is_range_valid.test.ts diff --git a/src/legacy/ui/public/validated_range/is_range_valid.js b/src/plugins/kibana_react/public/validated_range/is_range_valid.ts similarity index 74% rename from src/legacy/ui/public/validated_range/is_range_valid.js rename to src/plugins/kibana_react/public/validated_range/is_range_valid.ts index 9b733815a66ba8..1f822c0cb94b96 100644 --- a/src/legacy/ui/public/validated_range/is_range_valid.js +++ b/src/plugins/kibana_react/public/validated_range/is_range_valid.ts @@ -18,14 +18,24 @@ */ import { i18n } from '@kbn/i18n'; +import { ValueMember, Value } from './validated_dual_range'; const LOWER_VALUE_INDEX = 0; const UPPER_VALUE_INDEX = 1; -export function isRangeValid(value, min, max, allowEmptyRange) { - allowEmptyRange = typeof allowEmptyRange === 'boolean' ? allowEmptyRange : true; //cannot use default props since that uses falsy check - let lowerValue = isNaN(value[LOWER_VALUE_INDEX]) ? '' : value[LOWER_VALUE_INDEX]; - let upperValue = isNaN(value[UPPER_VALUE_INDEX]) ? '' : value[UPPER_VALUE_INDEX]; +export function isRangeValid( + value: Value = [0, 0], + min: ValueMember = 0, + max: ValueMember = 0, + allowEmptyRange?: boolean +) { + allowEmptyRange = typeof allowEmptyRange === 'boolean' ? allowEmptyRange : true; // cannot use default props since that uses falsy check + let lowerValue: ValueMember = isNaN(value[LOWER_VALUE_INDEX] as number) + ? '' + : `${value[LOWER_VALUE_INDEX]}`; + let upperValue: ValueMember = isNaN(value[UPPER_VALUE_INDEX] as number) + ? '' + : `${value[UPPER_VALUE_INDEX]}`; const isLowerValueValid = lowerValue.toString() !== ''; const isUpperValueValid = upperValue.toString() !== ''; @@ -39,7 +49,7 @@ export function isRangeValid(value, min, max, allowEmptyRange) { let errorMessage = ''; const bothMustBeSetErrorMessage = i18n.translate( - 'common.ui.dualRangeControl.mustSetBothErrorMessage', + 'kibana-react.dualRangeControl.mustSetBothErrorMessage', { defaultMessage: 'Both lower and upper values must be set', } @@ -55,13 +65,13 @@ export function isRangeValid(value, min, max, allowEmptyRange) { errorMessage = bothMustBeSetErrorMessage; } else if ((isLowerValueValid && lowerValue < min) || (isUpperValueValid && upperValue > max)) { isValid = false; - errorMessage = i18n.translate('common.ui.dualRangeControl.outsideOfRangeErrorMessage', { + errorMessage = i18n.translate('kibana-react.dualRangeControl.outsideOfRangeErrorMessage', { defaultMessage: 'Values must be on or between {min} and {max}', values: { min, max }, }); } else if (isLowerValueValid && isUpperValueValid && upperValue < lowerValue) { isValid = false; - errorMessage = i18n.translate('common.ui.dualRangeControl.upperValidErrorMessage', { + errorMessage = i18n.translate('kibana-react.dualRangeControl.upperValidErrorMessage', { defaultMessage: 'Upper value must be greater or equal to lower value', }); } diff --git a/src/legacy/ui/public/validated_range/validated_dual_range.js b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx similarity index 67% rename from src/legacy/ui/public/validated_range/validated_dual_range.js rename to src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx index 3b0efba11afccc..e7392eeba3830f 100644 --- a/src/legacy/ui/public/validated_range/validated_dual_range.js +++ b/src/plugins/kibana_react/public/validated_range/validated_dual_range.tsx @@ -18,17 +18,38 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { isRangeValid } from './is_range_valid'; - import { EuiFormRow, EuiDualRange } from '@elastic/eui'; +import { EuiFormRowDisplayKeys } from '@elastic/eui/src/components/form/form_row/form_row'; +import { EuiDualRangeProps } from '@elastic/eui/src/components/form/range/dual_range'; +import { isRangeValid } from './is_range_valid'; // Wrapper around EuiDualRange that ensures onChange callback is only called when range value // is valid and within min/max -export class ValidatedDualRange extends Component { - state = {}; - static getDerivedStateFromProps(nextProps, prevState) { +export type Value = EuiDualRangeProps['value']; +export type ValueMember = EuiDualRangeProps['value'][0]; + +interface Props extends Omit { + value?: Value; + allowEmptyRange?: boolean; + label?: string; + formRowDisplay?: EuiFormRowDisplayKeys; + onChange?: (val: [string, string]) => void; + min?: ValueMember; + max?: ValueMember; +} + +interface State { + isValid?: boolean; + errorMessage?: string; + value: [ValueMember, ValueMember]; + prevValue?: Value; +} + +export class ValidatedDualRange extends Component { + static defaultProps: { fullWidth: boolean; allowEmptyRange: boolean; compressed: boolean }; + + static getDerivedStateFromProps(nextProps: Props, prevState: State) { if (nextProps.value !== prevState.prevValue) { const { isValid, errorMessage } = isRangeValid( nextProps.value, @@ -47,7 +68,10 @@ export class ValidatedDualRange extends Component { return null; } - _onChange = value => { + // @ts-ignore state populated by getDerivedStateFromProps + state: State = {}; + + _onChange = (value: Value) => { const { isValid, errorMessage } = isRangeValid( value, this.props.min, @@ -61,8 +85,8 @@ export class ValidatedDualRange extends Component { errorMessage, }); - if (isValid) { - this.props.onChange(value); + if (this.props.onChange && isValid) { + this.props.onChange([value[0] as string, value[1] as string]); } }; @@ -75,7 +99,8 @@ export class ValidatedDualRange extends Component { value, // eslint-disable-line no-unused-vars onChange, // eslint-disable-line no-unused-vars allowEmptyRange, // eslint-disable-line no-unused-vars - ...rest + // @ts-ignore + ...rest // TODO: Consider alternatives for spread operator in component } = this.props; return ( @@ -92,6 +117,7 @@ export class ValidatedDualRange extends Component { fullWidth={fullWidth} value={this.state.value} onChange={this._onChange} + // @ts-ignore focusable={false} // remove when #59039 is fixed {...rest} /> @@ -100,14 +126,6 @@ export class ValidatedDualRange extends Component { } } -ValidatedDualRange.propTypes = { - allowEmptyRange: PropTypes.bool, - fullWidth: PropTypes.bool, - compressed: PropTypes.bool, - label: PropTypes.node, - formRowDisplay: PropTypes.string, -}; - ValidatedDualRange.defaultProps = { allowEmptyRange: true, fullWidth: false, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js index ac17915b5f2776..eb23607aa2150c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js @@ -11,7 +11,7 @@ import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elasti import { ValidatedRange } from '../../../components/validated_range'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ValidatedDualRange } from 'ui/validated_range'; +import { ValidatedDualRange } from '../../../../../../../../src/plugins/kibana_react/public'; import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; export function LayerSettings(props) { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js index 1d5815a84920cb..5de7b462136e16 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js @@ -6,7 +6,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ValidatedDualRange } from 'ui/validated_range'; +import { ValidatedDualRange } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { MIN_SIZE, MAX_SIZE } from '../../vector_style_defaults'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9b842f736b8bb5..3fdcf9b815931a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -78,9 +78,6 @@ "messages": { "common.ui.aggResponse.allDocsTitle": "すべてのドキュメント", "common.ui.directives.paginate.size.allDropDownOptionLabel": "すべて", - "common.ui.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", - "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "値は {min} と {max} の間でなければなりません", - "common.ui.dualRangeControl.upperValidErrorMessage": "上の値は下の値以上でなければなりません", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "エラー", "common.ui.errorAutoCreateIndex.errorDescription": "Elasticsearch クラスターの {autoCreateIndexActionConfig} 設定が原因で、Kibana が保存されたオブジェクトを格納するインデックスを自動的に作成できないようです。Kibana は、保存されたオブジェクトインデックスが適切なマッピング/スキーマを使用し Kibana から Elasticsearch へのポーリングの回数を減らすための最適な手段であるため、この Elasticsearch の機能を使用します。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "申し訳ございませんが、この問題が解決されるまで Kibana で何も保存することができません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 97c30f180dfe1f..1bcbcca055c32a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -78,9 +78,6 @@ "messages": { "common.ui.aggResponse.allDocsTitle": "所有文档", "common.ui.directives.paginate.size.allDropDownOptionLabel": "全部", - "common.ui.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", - "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", - "common.ui.dualRangeControl.upperValidErrorMessage": "上限值必须大于或等于下限值", "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "错误", "common.ui.errorAutoCreateIndex.errorDescription": "似乎 Elasticsearch 集群的 {autoCreateIndexActionConfig} 设置使 Kibana 无法自动创建用于存储已保存对象的索引。Kibana 将使用此 Elasticsearch 功能,因为这是确保已保存对象索引使用正确映射/架构的最好方式,而且其允许 Kibana 较少地轮询 Elasticsearch。", "common.ui.errorAutoCreateIndex.errorDisclaimer": "但是,只有解决了此问题后,您才能在 Kibana 保存内容。", From 69ec60d744893b86449a913fa65836f2f2dd6295 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Mon, 16 Mar 2020 17:18:49 -0400 Subject: [PATCH 13/17] EMT-248: implement ack resource to accept event payload to acknowledge agent actions (#60218) [Ingest]EMT-248: implement ack resource to accept event payload to acknowledge agent actions --- .../common/types/rest_spec/agent.ts | 7 +- .../server/routes/agent/acks_handlers.test.ts | 94 ++++++++++++ .../server/routes/agent/acks_handlers.ts | 69 +++++++++ .../server/routes/agent/handlers.ts | 36 +---- .../server/routes/agent/index.ts | 11 +- .../server/services/agents/acks.test.ts | 118 +++++++++++++++ .../server/services/agents/acks.ts | 99 +++++++++++-- .../server/services/agents/crud.ts | 2 +- .../server/types/models/agent.ts | 5 + .../server/types/rest_spec/agent.ts | 4 +- .../api_integration/apis/fleet/agents/acks.ts | 138 +++++++++++++++++- .../es_archives/fleet/agents/data.json | 12 ++ 12 files changed, 539 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index af919d973b7d9c..7bbaf42422bb25 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -69,13 +69,18 @@ export interface PostAgentEnrollResponse { export interface PostAgentAcksRequest { body: { - action_ids: string[]; + events: AgentEvent[]; }; params: { agentId: string; }; } +export interface PostAgentAcksResponse { + action: string; + success: boolean; +} + export interface PostAgentUnenrollRequest { body: { kuery: string } | { ids: string[] }; } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts new file mode 100644 index 00000000000000..84923d5c336642 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { postAgentAcksHandlerBuilder } from './acks_handlers'; +import { + KibanaResponseFactory, + RequestHandlerContext, + SavedObjectsClientContract, +} from 'kibana/server'; +import { httpServerMock, savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; +import { AckEventSchema } from '../../types/models'; +import { AcksService } from '../../services/agents'; + +describe('test acks schema', () => { + it('validate that ack event schema expect action id', async () => { + expect(() => + AckEventSchema.validate({ + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + agent_id: 'agent', + message: 'hello', + payload: 'payload', + }) + ).toThrow(Error); + + expect( + AckEventSchema.validate({ + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + agent_id: 'agent', + action_id: 'actionId', + message: 'hello', + payload: 'payload', + }) + ).toBeTruthy(); + }); +}); + +describe('test acks handlers', () => { + let mockResponse: jest.Mocked; + let mockSavedObjectsClient: jest.Mocked; + + beforeEach(() => { + mockSavedObjectsClient = savedObjectsClientMock.create(); + mockResponse = httpServerMock.createResponseFactory(); + }); + + it('should succeed on valid agent event', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + headers: { + authorization: 'ApiKey TmVqTDBIQUJsRkw1em52R1ZIUF86NS1NaTItdHFUTHFHbThmQW1Fb0ljUQ==', + }, + body: { + events: [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action1', + agent_id: 'agent', + message: 'message', + }, + ], + }, + }); + + const ackService: AcksService = { + acknowledgeAgentActions: jest.fn().mockReturnValueOnce([ + { + type: 'CONFIG_CHANGE', + id: 'action1', + }, + ]), + getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({ + id: 'agent', + }), + getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient), + saveAgentEvents: jest.fn(), + } as jest.Mocked; + + const postAgentAcksHandler = postAgentAcksHandlerBuilder(ackService); + await postAgentAcksHandler(({} as unknown) as RequestHandlerContext, mockRequest, mockResponse); + expect(mockResponse.ok.mock.calls[0][0]?.body as PostAgentAcksResponse).toEqual({ + action: 'acks', + success: true, + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts new file mode 100644 index 00000000000000..53b677bb1389ea --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// handlers that handle events from agents in response to actions received + +import { RequestHandler } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { PostAgentAcksRequestSchema } from '../../types/rest_spec'; +import * as APIKeyService from '../../services/api_keys'; +import { AcksService } from '../../services/agents'; +import { AgentEvent } from '../../../common/types/models'; +import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; + +export const postAgentAcksHandlerBuilder = function( + ackService: AcksService +): RequestHandler< + TypeOf, + undefined, + TypeOf +> { + return async (context, request, response) => { + try { + const soClient = ackService.getSavedObjectsClientContract(request); + const res = APIKeyService.parseApiKey(request.headers); + const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string); + const agentEvents = request.body.events as AgentEvent[]; + + // validate that all events are for the authorized agent obtained from the api key + const notAuthorizedAgentEvent = agentEvents.filter( + agentEvent => agentEvent.agent_id !== agent.id + ); + + if (notAuthorizedAgentEvent && notAuthorizedAgentEvent.length > 0) { + return response.badRequest({ + body: + 'agent events contains events with different agent id from currently authorized agent', + }); + } + + const agentActions = await ackService.acknowledgeAgentActions(soClient, agent, agentEvents); + + if (agentActions.length > 0) { + await ackService.saveAgentEvents(soClient, agentEvents); + } + + const body: PostAgentAcksResponse = { + action: 'acks', + success: true, + }; + + return response.ok({ body }); + } catch (e) { + if (e.isBoom) { + return response.customError({ + statusCode: e.output.statusCode, + body: { message: e.message }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } + }; +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index cb4e4d557d74fd..cf1fd2476f310d 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -23,7 +23,6 @@ import { GetOneAgentEventsRequestSchema, PostAgentCheckinRequestSchema, PostAgentEnrollRequestSchema, - PostAgentAcksRequestSchema, PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, } from '../../types'; @@ -31,7 +30,7 @@ import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; import { appContextService } from '../../services/app_context'; -function getInternalUserSOClient(request: KibanaRequest) { +export function getInternalUserSOClient(request: KibanaRequest) { // soClient as kibana internal users, be carefull on how you use it, security is not enabled return appContextService.getSavedObjects().getScopedClient(request, { excludedWrappers: ['security'], @@ -210,39 +209,6 @@ export const postAgentCheckinHandler: RequestHandler< } }; -export const postAgentAcksHandler: RequestHandler< - TypeOf, - undefined, - TypeOf -> = async (context, request, response) => { - try { - const soClient = getInternalUserSOClient(request); - const res = APIKeyService.parseApiKey(request.headers); - const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string); - - await AgentService.acknowledgeAgentActions(soClient, agent, request.body.action_ids); - - const body = { - action: 'acks', - success: true, - }; - - return response.ok({ body }); - } catch (e) { - if (e.isBoom) { - return response.customError({ - statusCode: e.output.statusCode, - body: { message: e.message }, - }); - } - - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); - } -}; - export const postAgentEnrollHandler: RequestHandler< undefined, undefined, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index 8a65fa9c50e8b3..c85629ea22ad9c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -31,10 +31,12 @@ import { getAgentEventsHandler, postAgentCheckinHandler, postAgentEnrollHandler, - postAgentAcksHandler, postAgentsUnenrollHandler, getAgentStatusForConfigHandler, + getInternalUserSOClient, } from './handlers'; +import { postAgentAcksHandlerBuilder } from './acks_handlers'; +import * as AgentService from '../../services/agents'; export const registerRoutes = (router: IRouter) => { // Get one @@ -101,7 +103,12 @@ export const registerRoutes = (router: IRouter) => { validate: PostAgentAcksRequestSchema, options: { tags: [] }, }, - postAgentAcksHandler + postAgentAcksHandlerBuilder({ + acknowledgeAgentActions: AgentService.acknowledgeAgentActions, + getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId, + getSavedObjectsClientContract: getInternalUserSOClient, + saveAgentEvents: AgentService.saveAgentEvents, + }) ); router.post( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts new file mode 100644 index 00000000000000..3c07463e3af5de --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { Agent, AgentAction, AgentEvent } from '../../../common/types/models'; +import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; +import { acknowledgeAgentActions } from './acks'; +import { isBoom } from 'boom'; + +describe('test agent acks services', () => { + it('should succeed on valid and matched actions', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + const agentActions = await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + actions: [ + { + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + ], + } as unknown) as Agent, + [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action1', + agent_id: 'id', + } as AgentEvent, + ] + ); + expect(agentActions).toEqual([ + ({ + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + } as unknown) as AgentAction, + ]); + }); + + it('should fail for actions that cannot be found on agent actions list', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + try { + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + actions: [ + { + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + ], + } as unknown) as Agent, + [ + ({ + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action2', + agent_id: 'id', + } as unknown) as AgentEvent, + ] + ); + expect(true).toBeFalsy(); + } catch (e) { + expect(isBoom(e)).toBeTruthy(); + } + }); + + it('should fail for events that have types not in the allowed acknowledgement type list', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + try { + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + actions: [ + { + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + ], + } as unknown) as Agent, + [ + ({ + type: 'ACTION', + subtype: 'FAILED', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action1', + agent_id: 'id', + } as unknown) as AgentEvent, + ] + ); + expect(true).toBeFalsy(); + } catch (e) { + expect(isBoom(e)).toBeTruthy(); + } + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index 1732ff9cf5b5c8..892d8cdbe657f3 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -4,25 +4,100 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; -import { Agent, AgentSOAttributes } from '../../types'; -import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { + KibanaRequest, + SavedObjectsBulkCreateObject, + SavedObjectsBulkResponse, + SavedObjectsClientContract, +} from 'kibana/server'; +import Boom from 'boom'; +import { + Agent, + AgentAction, + AgentEvent, + AgentEventSOAttributes, + AgentSOAttributes, +} from '../../types'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; + +const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; export async function acknowledgeAgentActions( soClient: SavedObjectsClientContract, agent: Agent, - actionIds: string[] -) { + agentEvents: AgentEvent[] +): Promise { const now = new Date().toISOString(); - const updatedActions = agent.actions.map(action => { - if (action.sent_at) { - return action; + const agentActionMap: Map = new Map( + agent.actions.map(agentAction => [agentAction.id, agentAction]) + ); + + const matchedUpdatedActions: AgentAction[] = []; + + agentEvents.forEach(agentEvent => { + if (!isAllowedType(agentEvent.type)) { + throw Boom.badRequest(`${agentEvent.type} not allowed for acknowledgment only ACTION_RESULT`); + } + if (agentActionMap.has(agentEvent.action_id!)) { + const action = agentActionMap.get(agentEvent.action_id!) as AgentAction; + if (!action.sent_at) { + action.sent_at = now; + } + matchedUpdatedActions.push(action); + } else { + throw Boom.badRequest('all actions should belong to current agent'); } - return { ...action, sent_at: actionIds.indexOf(action.id) >= 0 ? now : undefined }; }); - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { - actions: updatedActions, - }); + if (matchedUpdatedActions.length > 0) { + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { + actions: matchedUpdatedActions, + }); + } + + return matchedUpdatedActions; +} + +function isAllowedType(eventType: string): boolean { + return ALLOWED_ACKNOWLEDGEMENT_TYPE.indexOf(eventType) >= 0; +} + +export async function saveAgentEvents( + soClient: SavedObjectsClientContract, + events: AgentEvent[] +): Promise> { + const objects: Array> = events.map( + eventData => { + return { + attributes: { + ...eventData, + payload: eventData.payload ? JSON.stringify(eventData.payload) : undefined, + }, + type: AGENT_EVENT_SAVED_OBJECT_TYPE, + }; + } + ); + + return await soClient.bulkCreate(objects); +} + +export interface AcksService { + acknowledgeAgentActions: ( + soClient: SavedObjectsClientContract, + agent: Agent, + actionIds: AgentEvent[] + ) => Promise; + + getAgentByAccessAPIKeyId: ( + soClient: SavedObjectsClientContract, + accessAPIKeyId: string + ) => Promise; + + getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract; + + saveAgentEvents: ( + soClient: SavedObjectsClientContract, + events: AgentEvent[] + ) => Promise>; } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index bcd825fee87258..cdbdf164e834d3 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -68,7 +68,7 @@ export async function getAgent(soClient: SavedObjectsClientContract, agentId: st export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, accessAPIKeyId: string -) { +): Promise { const response = await soClient.find({ type: AGENT_SAVED_OBJECT_TYPE, searchFields: ['access_api_key_id'], diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent.ts b/x-pack/plugins/ingest_manager/server/types/models/agent.ts index 276dddf9e3d1c5..e0d252faaaf87c 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent.ts @@ -44,6 +44,11 @@ const AgentEventBase = { stream_id: schema.maybe(schema.string()), }; +export const AckEventSchema = schema.object({ + ...AgentEventBase, + ...{ action_id: schema.string() }, +}); + export const AgentEventSchema = schema.object({ ...AgentEventBase, }); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 92422274d5cf46..9fe84c12521add 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { AgentEventSchema, AgentTypeSchema } from '../models'; +import { AckEventSchema, AgentEventSchema, AgentTypeSchema } from '../models'; export const GetAgentsRequestSchema = { query: schema.object({ @@ -45,7 +45,7 @@ export const PostAgentEnrollRequestSchema = { export const PostAgentAcksRequestSchema = { body: schema.object({ - action_ids: schema.arrayOf(schema.string()), + events: schema.arrayOf(AckEventSchema), }), params: schema.object({ agentId: schema.string(), diff --git a/x-pack/test/api_integration/apis/fleet/agents/acks.ts b/x-pack/test/api_integration/apis/fleet/agents/acks.ts index 1ab54554d62f0e..a2eba2c23c39d6 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts @@ -59,7 +59,7 @@ export default function(providerContext: FtrProviderContext) { .expect(401); }); - it('should return a 200 if this a valid acks access', async () => { + it('should return a 200 if this a valid acks request', async () => { const { body: apiResponse } = await supertest .post(`/api/ingest_manager/fleet/agents/agent1/acks`) .set('kbn-xsrf', 'xx') @@ -68,12 +68,144 @@ export default function(providerContext: FtrProviderContext) { `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` ) .send({ - action_ids: ['action1'], + events: [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }, + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-05T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a2', + agent_id: 'agent1', + message: 'hello2', + payload: 'payload2', + }, + ], }) .expect(200); - expect(apiResponse.action).to.be('acks'); expect(apiResponse.success).to.be(true); + const { body: eventResponse } = await supertest + .get(`/api/ingest_manager/fleet/agents/agent1/events`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .expect(200); + const expectedEvents = eventResponse.list.filter( + (item: Record) => + item.action_id === '48cebde1-c906-4893-b89f-595d943b72a1' || + item.action_id === '48cebde1-c906-4893-b89f-595d943b72a2' + ); + expect(expectedEvents.length).to.eql(2); + const expectedEvent = expectedEvents.find( + (item: Record) => item.action_id === '48cebde1-c906-4893-b89f-595d943b72a1' + ); + expect(expectedEvent).to.eql({ + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }); + }); + + it('should return a 400 when request event list contains event for another agent id', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/acks`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + events: [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent2', + message: 'hello', + payload: 'payload', + }, + ], + }) + .expect(400); + expect(apiResponse.message).to.eql( + 'agent events contains events with different agent id from currently authorized agent' + ); + }); + + it('should return a 400 when request event list contains action that does not belong to agent current actions', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/acks`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + events: [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }, + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'does-not-exist', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }, + ], + }) + .expect(400); + expect(apiResponse.message).to.eql('all actions should belong to current agent'); + }); + + it('should return a 400 when request event list contains action types that are not allowed for acknowledgement', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/acks`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + events: [ + { + type: 'ACTION', + subtype: 'FAILED', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: '48cebde1-c906-4893-b89f-595d943b72a1', + agent_id: 'agent1', + message: 'hello', + payload: 'payload', + }, + ], + }) + .expect(400); + expect(apiResponse.message).to.eql( + 'ACTION not allowed for acknowledgment only ACTION_RESULT' + ); }); }); } diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 36928018d15a05..9b29767d5162da 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -23,6 +23,18 @@ "type": "PAUSE", "created_at": "2019-09-04T15:01:07+0000", "sent_at": "2019-09-04T15:03:07+0000" + }, + { + "created_at" : "2020-03-15T03:47:15.129Z", + "id" : "48cebde1-c906-4893-b89f-595d943b72a1", + "type" : "CONFIG_CHANGE", + "sent_at": "2020-03-04T15:03:07+0000" + }, + { + "created_at" : "2020-03-16T03:47:15.129Z", + "id" : "48cebde1-c906-4893-b89f-595d943b72a2", + "type" : "CONFIG_CHANGE", + "sent_at": "2020-03-04T15:03:07+0000" }] } } From 132383c28ca53e9e348863e198bf01300d39d122 Mon Sep 17 00:00:00 2001 From: Eric Davis Date: Mon, 16 Mar 2020 17:25:14 -0400 Subject: [PATCH 14/17] [Endpoint] TEST: verify alerts page header says 'Alerts' (#60206) * test to verify alerts page header says alerts * updating test with pr feedback * updating test with pr feedback and better verbiage * updating test with pr feedback for better test titling Co-authored-by: Elastic Machine --- .../public/applications/endpoint/view/alerts/index.tsx | 2 +- x-pack/test/functional/apps/endpoint/alert_list.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx index 9718b4e4ef8cdc..b900a0a35dbf57 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/index.tsx @@ -233,7 +233,7 @@ export const AlertIndex = memo(() => { -

+

{ await esArchiver.load('endpoint/alerts/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/alerts'); }); - it('loads the Alert List Page', async () => { + it('loads in the browser', async () => { await testSubjects.existOrFail('alertListPage'); }); + it('contains the Alert List Page title', async () => { + const alertsTitle = await testSubjects.getVisibleText('alertsViewTitle'); + expect(alertsTitle).to.equal('Alerts'); + }); it('includes alerts search bar', async () => { await testSubjects.existOrFail('alertsSearchBar'); }); From 537fa8c1eb6b959625ddc0ec9599827ad07e0635 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 16 Mar 2020 14:26:47 -0700 Subject: [PATCH 15/17] [Reporting] Fix error handling for job handler in route (#60161) * fix bogus rison error * add generate route test * update test name --- .../server/routes/generate_from_jobparams.ts | 9 +- .../server/routes/generation.test.ts | 140 ++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/reporting/server/routes/generation.test.ts diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts index 49868bb7ad5d53..56622617586f7d 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -82,16 +82,21 @@ export function registerGenerateFromJobParams( } const { exportType } = request.params; + let jobParams; let response; try { - const jobParams = rison.decode(jobParamsRison) as object | null; + jobParams = rison.decode(jobParamsRison) as object | null; if (!jobParams) { throw new Error('missing jobParams!'); } - response = await handler(exportType, jobParams, legacyRequest, h); } catch (err) { throw boom.badRequest(`invalid rison: ${jobParamsRison}`); } + try { + response = await handler(exportType, jobParams, legacyRequest, h); + } catch (err) { + throw handleError(exportType, err); + } return response; }, }); diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts new file mode 100644 index 00000000000000..54d9671692c5de --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.test.ts @@ -0,0 +1,140 @@ +/* + * 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 Hapi from 'hapi'; +import { createMockReportingCore } from '../../test_helpers'; +import { Logger, ServerFacade } from '../../types'; +import { ReportingCore, ReportingSetupDeps } from '../../server/types'; + +jest.mock('./lib/authorized_user_pre_routing', () => ({ + authorizedUserPreRoutingFactory: () => () => ({}), +})); +jest.mock('./lib/reporting_feature_pre_routing', () => ({ + reportingFeaturePreRoutingFactory: () => () => () => ({ + jobTypes: ['unencodedJobType', 'base64EncodedJobType'], + }), +})); + +import { registerJobGenerationRoutes } from './generation'; + +let mockServer: Hapi.Server; +let mockReportingPlugin: ReportingCore; +const mockLogger = ({ + error: jest.fn(), + debug: jest.fn(), +} as unknown) as Logger; + +beforeEach(async () => { + mockServer = new Hapi.Server({ + debug: false, + port: 8080, + routes: { log: { collect: true } }, + }); + mockServer.config = () => ({ get: jest.fn(), has: jest.fn() }); + mockReportingPlugin = await createMockReportingCore(); + mockReportingPlugin.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ toJSON: () => '{ "job": "data" }' })); +}); + +const mockPlugins = { + elasticsearch: { + adminClient: { callAsInternalUser: jest.fn() }, + }, + security: null, +}; + +const getErrorsFromRequest = (request: Hapi.Request) => { + // @ts-ignore error property doesn't exist on RequestLog + return request.logs.filter(log => log.tags.includes('error')).map(log => log.error); // NOTE: error stack is available +}; + +test(`returns 400 if there are no job params`, async () => { + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"A jobParams RISON string is required\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: A jobParams RISON string is required], + ] + `); +}); + +test(`returns 400 if job params is invalid`, async () => { + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + payload: { jobParams: `foo:` }, + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":400,\\"error\\":\\"Bad Request\\",\\"message\\":\\"invalid rison: foo:\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: invalid rison: foo:], + ] + `); +}); + +test(`returns 500 if job handler throws an error`, async () => { + mockReportingPlugin.getEnqueueJob = async () => + jest.fn().mockImplementation(() => ({ + toJSON: () => { + throw new Error('you found me'); + }, + })); + + registerJobGenerationRoutes( + mockReportingPlugin, + (mockServer as unknown) as ServerFacade, + (mockPlugins as unknown) as ReportingSetupDeps, + mockLogger + ); + + const options = { + method: 'POST', + url: '/api/reporting/generate/printablePdf', + payload: { jobParams: `abc` }, + }; + + const { payload, request } = await mockServer.inject(options); + expect(payload).toMatchInlineSnapshot( + `"{\\"statusCode\\":500,\\"error\\":\\"Internal Server Error\\",\\"message\\":\\"An internal server error occurred\\"}"` + ); + + const errorLogs = getErrorsFromRequest(request); + expect(errorLogs).toMatchInlineSnapshot(` + Array [ + [Error: you found me], + [Error: you found me], + ] + `); +}); From ef3261132a43342c4709efdba235a2cdb9ed1e98 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 16 Mar 2020 15:40:52 -0600 Subject: [PATCH 16/17] [Maps] move MapSavedObject type out of telemetry (#60127) * [Maps] move MapSavedObject type out of telemetry * move SavedObject from server to core/types * review feedback * results from check_published_api_changes --- .../kibana-plugin-core-public.savedobject.md | 1 - .../kibana-plugin-core-server.savedobject.md | 1 - src/core/public/public.api.md | 2 + src/core/public/saved_objects/index.ts | 13 ++-- src/core/server/saved_objects/types.ts | 59 ++----------------- src/core/server/server.api.md | 2 + src/core/types/saved_objects.ts | 51 ++++++++++++++++ .../server/maps_telemetry/maps_telemetry.ts | 32 +--------- .../maps/common/map_saved_object_type.ts | 22 +++++++ 9 files changed, 93 insertions(+), 90 deletions(-) create mode 100644 x-pack/plugins/maps/common/map_saved_object_type.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.savedobject.md index 9ced619ad4bfe2..c6bc13b98bc066 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobject.md @@ -4,7 +4,6 @@ ## SavedObject interface - Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobject.md index ebb105c846aff8..0df97b0d4221a2 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobject.md @@ -4,7 +4,6 @@ ## SavedObject interface - Signature: ```typescript diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 46667230edc3b8..fa5dc745e69313 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -944,6 +944,8 @@ export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T ext [K in keyof T]: RecursiveReadonly; }> : T; +// Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface SavedObject { attributes: T; diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index 5015a9c3db78e2..13b4a128936664 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -32,11 +32,6 @@ export { export { SimpleSavedObject } from './simple_saved_object'; export { SavedObjectsStart, SavedObjectsService } from './saved_objects_service'; export { - SavedObject, - SavedObjectAttribute, - SavedObjectAttributes, - SavedObjectAttributeSingle, - SavedObjectReference, SavedObjectsBaseOptions, SavedObjectsFindOptions, SavedObjectsMigrationVersion, @@ -48,3 +43,11 @@ export { SavedObjectsImportError, SavedObjectsImportRetry, } from '../../server/types'; + +export { + SavedObject, + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectAttributeSingle, + SavedObjectReference, +} from '../../types'; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 1d927211b43e50..962965a08f8b2a 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -35,66 +35,17 @@ export { import { LegacyConfig } from '../legacy'; import { SavedObjectUnsanitizedDoc } from './serialization'; import { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; +import { SavedObject } from '../../types'; + export { SavedObjectAttributes, SavedObjectAttribute, SavedObjectAttributeSingle, + SavedObject, + SavedObjectReference, + SavedObjectsMigrationVersion, } from '../../types'; -/** - * Information about the migrations that have been applied to this SavedObject. - * When Kibana starts up, KibanaMigrator detects outdated documents and - * migrates them based on this value. For each migration that has been applied, - * the plugin's name is used as a key and the latest migration version as the - * value. - * - * @example - * migrationVersion: { - * dashboard: '7.1.1', - * space: '6.6.6', - * } - * - * @public - */ -export interface SavedObjectsMigrationVersion { - [pluginName: string]: string; -} - -/** - * @public - */ -export interface SavedObject { - /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ - id: string; - /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ - type: string; - /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ - version?: string; - /** Timestamp of the last time this document had been updated. */ - updated_at?: string; - error?: { - message: string; - statusCode: number; - }; - /** {@inheritdoc SavedObjectAttributes} */ - attributes: T; - /** {@inheritdoc SavedObjectReference} */ - references: SavedObjectReference[]; - /** {@inheritdoc SavedObjectsMigrationVersion} */ - migrationVersion?: SavedObjectsMigrationVersion; -} - -/** - * A reference to another saved object. - * - * @public - */ -export interface SavedObjectReference { - name: string; - type: string; - id: string; -} - /** * * @public diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 84fe081adaae6b..229ffc4d215757 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1595,6 +1595,8 @@ export interface RouteValidatorOptions { // @public export type SafeRouteMethod = 'get' | 'options'; +// Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// // @public (undocumented) export interface SavedObject { attributes: T; diff --git a/src/core/types/saved_objects.ts b/src/core/types/saved_objects.ts index 73eb2db11d62fb..d3faab6c557cd6 100644 --- a/src/core/types/saved_objects.ts +++ b/src/core/types/saved_objects.ts @@ -46,3 +46,54 @@ export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttri export interface SavedObjectAttributes { [key: string]: SavedObjectAttribute; } + +/** + * A reference to another saved object. + * + * @public + */ +export interface SavedObjectReference { + name: string; + type: string; + id: string; +} + +/** + * Information about the migrations that have been applied to this SavedObject. + * When Kibana starts up, KibanaMigrator detects outdated documents and + * migrates them based on this value. For each migration that has been applied, + * the plugin's name is used as a key and the latest migration version as the + * value. + * + * @example + * migrationVersion: { + * dashboard: '7.1.1', + * space: '6.6.6', + * } + * + * @public + */ +export interface SavedObjectsMigrationVersion { + [pluginName: string]: string; +} + +export interface SavedObject { + /** The ID of this Saved Object, guaranteed to be unique for all objects of the same `type` */ + id: string; + /** The type of Saved Object. Each plugin can define it's own custom Saved Object types. */ + type: string; + /** An opaque version number which changes on each successful write operation. Can be used for implementing optimistic concurrency control. */ + version?: string; + /** Timestamp of the last time this document had been updated. */ + updated_at?: string; + error?: { + message: string; + statusCode: number; + }; + /** {@inheritdoc SavedObjectAttributes} */ + attributes: T; + /** {@inheritdoc SavedObjectReference} */ + references: SavedObjectReference[]; + /** {@inheritdoc SavedObjectsMigrationVersion} */ + migrationVersion?: SavedObjectsMigrationVersion; +} diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 3ce46e2955f50c..6a363af9e57d4d 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -19,6 +19,7 @@ import { // @ts-ignore } from '../../common/constants'; import { LayerDescriptor } from '../../common/descriptor_types'; +import { MapSavedObject } from '../../../../../plugins/maps/common/map_saved_object_type'; interface IStats { [key: string]: { @@ -32,33 +33,6 @@ interface ILayerTypeCount { [key: string]: number; } -interface IMapSavedObject { - [key: string]: any; - fields: IFieldType[]; - title: string; - id?: string; - type?: string; - timeFieldName?: string; - fieldFormatMap?: Record< - string, - { - id: string; - params: unknown; - } - >; - attributes?: { - title?: string; - description?: string; - mapStateJSON?: string; - layerListJSON?: string; - uiStateJSON?: string; - bounds?: { - type?: string; - coordinates?: []; - }; - }; -} - function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: number) { const uniqueLayerTypes = _.uniq(_.flatten(layerCountsList.map(lTypes => Object.keys(lTypes)))); @@ -102,7 +76,7 @@ export function buildMapsTelemetry({ indexPatternSavedObjects, settings, }: { - mapSavedObjects: IMapSavedObject[]; + mapSavedObjects: MapSavedObject[]; indexPatternSavedObjects: IIndexPattern[]; settings: SavedObjectAttribute; }): SavedObjectAttributes { @@ -183,7 +157,7 @@ export async function getMapsTelemetry( savedObjectsClient: SavedObjectsClientContract, config: Function ) { - const mapSavedObjects: IMapSavedObject[] = await getMapSavedObjects(savedObjectsClient); + const mapSavedObjects: MapSavedObject[] = await getMapSavedObjects(savedObjectsClient); const indexPatternSavedObjects: IIndexPattern[] = await getIndexPatternSavedObjects( savedObjectsClient ); diff --git a/x-pack/plugins/maps/common/map_saved_object_type.ts b/x-pack/plugins/maps/common/map_saved_object_type.ts new file mode 100644 index 00000000000000..e5b4876186fd84 --- /dev/null +++ b/x-pack/plugins/maps/common/map_saved_object_type.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { SavedObject } from '../../../../src/core/types/saved_objects'; + +export type MapSavedObjectAttributes = { + title?: string; + description?: string; + mapStateJSON?: string; + layerListJSON?: string; + uiStateJSON?: string; + bounds?: { + type?: string; + coordinates?: []; + }; +}; + +export type MapSavedObject = SavedObject; From 4a8fd0afee9262916348c51e98f2ac955e25b2ac Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 16 Mar 2020 15:58:53 -0700 Subject: [PATCH 17/17] Revert "adds new test (#60064)" This reverts commit a946adbf10b0148bf510b0588713dd7a2a04579f. --- .../cypress/integration/detections.spec.ts | 43 +------------------ .../siem/cypress/screens/detections.ts | 4 +- .../plugins/siem/cypress/tasks/detections.ts | 7 --- 3 files changed, 2 insertions(+), 52 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts index c048510c50c365..1624586d4ca145 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/detections.spec.ts @@ -5,14 +5,12 @@ */ import { NUMBER_OF_SIGNALS, - OPEN_CLOSE_SIGNALS_BTN, SELECTED_SIGNALS, SHOWING_SIGNALS, SIGNALS, } from '../screens/detections'; import { - closeFirstSignal, closeSignals, goToClosedSignals, goToOpenedSignals, @@ -28,7 +26,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { DETECTIONS } from '../urls/navigation'; describe('Detections', () => { - beforeEach(() => { + before(() => { esArchiverLoad('signals'); loginAndWaitForPage(DETECTIONS); }); @@ -113,43 +111,4 @@ describe('Detections', () => { .should('eql', expectedNumberOfOpenedSignals.toString()); }); }); - - it('Closes one signal when more than one opened signals are selected', () => { - waitForSignalsToBeLoaded(); - - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .then(numberOfSignals => { - const numberOfSignalsToBeClosed = 1; - const numberOfSignalsToBeSelected = 3; - - cy.get(OPEN_CLOSE_SIGNALS_BTN).should('have.attr', 'disabled'); - selectNumberOfSignals(numberOfSignalsToBeSelected); - cy.get(OPEN_CLOSE_SIGNALS_BTN).should('not.have.attr', 'disabled'); - - closeFirstSignal(); - cy.reload(); - waitForSignalsToBeLoaded(); - waitForSignals(); - - const expectedNumberOfSignals = +numberOfSignals - numberOfSignalsToBeClosed; - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eq', expectedNumberOfSignals.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${expectedNumberOfSignals.toString()} signals`); - - goToClosedSignals(); - waitForSignals(); - - cy.get(NUMBER_OF_SIGNALS) - .invoke('text') - .should('eql', numberOfSignalsToBeClosed.toString()); - cy.get(SHOWING_SIGNALS) - .invoke('text') - .should('eql', `Showing ${numberOfSignalsToBeClosed.toString()} signal`); - cy.get(SIGNALS).should('have.length', numberOfSignalsToBeClosed); - }); - }); }); diff --git a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts index f388ac1215d01f..8b5ba235788072 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/detections.ts @@ -12,9 +12,7 @@ export const MANAGE_SIGNAL_DETECTION_RULES_BTN = '[data-test-subj="manage-signal export const NUMBER_OF_SIGNALS = '[data-test-subj="server-side-event-count"]'; -export const OPEN_CLOSE_SIGNAL_BTN = '[data-test-subj="update-signal-status-button"]'; - -export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] button'; +export const OPEN_CLOSE_SIGNALS_BTN = '[data-test-subj="openCloseSignal"] .siemLinkIcon__label'; export const OPENED_SIGNALS_BTN = '[data-test-subj="openSignals"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts index 3416e3eb81de33..21a0c136b90df8 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/detections.ts @@ -8,7 +8,6 @@ import { CLOSED_SIGNALS_BTN, LOADING_SIGNALS_PANEL, MANAGE_SIGNAL_DETECTION_RULES_BTN, - OPEN_CLOSE_SIGNAL_BTN, OPEN_CLOSE_SIGNALS_BTN, OPENED_SIGNALS_BTN, SIGNALS, @@ -16,12 +15,6 @@ import { } from '../screens/detections'; import { REFRESH_BUTTON } from '../screens/siem_header'; -export const closeFirstSignal = () => { - cy.get(OPEN_CLOSE_SIGNAL_BTN) - .first() - .click({ force: true }); -}; - export const closeSignals = () => { cy.get(OPEN_CLOSE_SIGNALS_BTN).click({ force: true }); };