diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 94296d076189bf..0f1136fd5334ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,11 +28,6 @@ # Canvas /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas -# Code -/x-pack/legacy/plugins/code/ @teams/code -/x-pack/test/functional/apps/code/ @teams/code -/x-pack/test/api_integration/apis/code/ @teams/code - # Logs & Metrics UI /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/legacy/plugins/integrations_manager/ @elastic/epm diff --git a/.i18nrc.json b/.i18nrc.json index 01065201b9d641..51727ce014f58f 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -20,6 +20,7 @@ "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", "navigation": "src/legacy/core_plugins/navigation", + "newsfeed": "src/plugins/newsfeed", "regionMap": "src/legacy/core_plugins/region_map", "server": "src/legacy/server", "statusPage": "src/legacy/core_plugins/status_page", diff --git a/.sass-lint.yml b/.sass-lint.yml index 64309aaae6cc3e..f6c0f5bb83fcb2 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -2,7 +2,6 @@ files: include: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' - - 'src/legacy/ui/public/query_bar/**/*.s+(a|c)ss' - 'src/legacy/ui/public/vislib/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' diff --git a/docs/development/core/public/kibana-plugin-public.app.chromeless.md b/docs/development/core/public/kibana-plugin-public.app.chromeless.md new file mode 100644 index 00000000000000..dc1e19bab80b29 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.app.chromeless.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [App](./kibana-plugin-public.app.md) > [chromeless](./kibana-plugin-public.app.chromeless.md) + +## App.chromeless property + +Hide the UI chrome when the application is mounted. Defaults to `false`. Takes precedence over chrome service visibility settings. + +Signature: + +```typescript +chromeless?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.app.md b/docs/development/core/public/kibana-plugin-public.app.md index 60cac357d1fe0c..c500c080a5feba 100644 --- a/docs/development/core/public/kibana-plugin-public.app.md +++ b/docs/development/core/public/kibana-plugin-public.app.md @@ -16,5 +16,6 @@ export interface App extends AppBase | Property | Type | Description | | --- | --- | --- | +| [chromeless](./kibana-plugin-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | | [mount](./kibana-plugin-public.app.mount.md) | (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount> | A mount function called when the user navigates to this app's route. | diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md index 16c8ffe07fc155..31513bda2e8791 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md @@ -21,12 +21,13 @@ How to configure react-router with a base path: export class MyPlugin implements Plugin { setup({ application }) { application.register({ - id: 'my-app', - async mount(context, params) { - const { renderApp } = await import('./application'); - return renderApp(context, params); - }, - }); + id: 'my-app', + async mount(context, params) { + const { renderApp } = await import('./application'); + return renderApp(context, params); + }, + }); + } } ``` diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index df0b963e2b6271..cec307032094ec 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -109,17 +109,18 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | | [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md). | -| [OverlayBannerMount](./kibana-plugin-public.overlaybannermount.md) | A function that will mount the banner inside the provided element. | -| [OverlayBannerUnmount](./kibana-plugin-public.overlaybannerunmount.md) | A function that will unmount the banner from the element. | +| [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | | [PluginInitializer](./kibana-plugin-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | | [PluginOpaqueId](./kibana-plugin-public.pluginopaqueid.md) | | | [RecursiveReadonly](./kibana-plugin-public.recursivereadonly.md) | | | [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-public.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-public.savedobjectsclient.md) | +| [Toast](./kibana-plugin-public.toast.md) | | | [ToastInput](./kibana-plugin-public.toastinput.md) | Inputs for [IToasts](./kibana-plugin-public.itoasts.md) APIs. | | [ToastInputFields](./kibana-plugin-public.toastinputfields.md) | Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md). | | [ToastsSetup](./kibana-plugin-public.toastssetup.md) | [IToasts](./kibana-plugin-public.itoasts.md) | | [ToastsStart](./kibana-plugin-public.toastsstart.md) | [IToasts](./kibana-plugin-public.itoasts.md) | | [UiSettingsClientContract](./kibana-plugin-public.uisettingsclientcontract.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [UiSettingsClient](./kibana-plugin-public.uisettingsclient.md) | +| [UnmountCallback](./kibana-plugin-public.unmountcallback.md) | A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) | diff --git a/docs/development/core/public/kibana-plugin-public.mountpoint.md b/docs/development/core/public/kibana-plugin-public.mountpoint.md new file mode 100644 index 00000000000000..58f407904a5762 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.mountpoint.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [MountPoint](./kibana-plugin-public.mountpoint.md) + +## MountPoint type + +A function that should mount DOM content inside the provided container element and return a handler to unmount it. + +Signature: + +```typescript +export declare type MountPoint = (element: HTMLElement) => UnmountCallback; +``` diff --git a/docs/development/core/public/kibana-plugin-public.overlaybannermount.md b/docs/development/core/public/kibana-plugin-public.overlaybannermount.md deleted file mode 100644 index 0fd0aca652cf0b..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.overlaybannermount.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayBannerMount](./kibana-plugin-public.overlaybannermount.md) - -## OverlayBannerMount type - -A function that will mount the banner inside the provided element. - -Signature: - -```typescript -export declare type OverlayBannerMount = (element: HTMLElement) => OverlayBannerUnmount; -``` diff --git a/docs/development/core/public/kibana-plugin-public.overlaybannersstart.add.md b/docs/development/core/public/kibana-plugin-public.overlaybannersstart.add.md index 8c3e874804e082..8ce59d5d9ca788 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaybannersstart.add.md +++ b/docs/development/core/public/kibana-plugin-public.overlaybannersstart.add.md @@ -9,14 +9,14 @@ Add a new banner Signature: ```typescript -add(mount: OverlayBannerMount, priority?: number): string; +add(mount: MountPoint, priority?: number): string; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| mount | OverlayBannerMount | | +| mount | MountPoint | | | priority | number | | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.overlaybannersstart.replace.md b/docs/development/core/public/kibana-plugin-public.overlaybannersstart.replace.md index 8f624c285b1800..a8f6915ea9bb7d 100644 --- a/docs/development/core/public/kibana-plugin-public.overlaybannersstart.replace.md +++ b/docs/development/core/public/kibana-plugin-public.overlaybannersstart.replace.md @@ -9,7 +9,7 @@ Replace a banner in place Signature: ```typescript -replace(id: string | undefined, mount: OverlayBannerMount, priority?: number): string; +replace(id: string | undefined, mount: MountPoint, priority?: number): string; ``` ## Parameters @@ -17,7 +17,7 @@ replace(id: string | undefined, mount: OverlayBannerMount, priority?: number): s | Parameter | Type | Description | | --- | --- | --- | | id | string | undefined | | -| mount | OverlayBannerMount | | +| mount | MountPoint | | | priority | number | | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.overlaybannerunmount.md b/docs/development/core/public/kibana-plugin-public.overlaybannerunmount.md deleted file mode 100644 index c9a7c2b8fee929..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.overlaybannerunmount.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [OverlayBannerUnmount](./kibana-plugin-public.overlaybannerunmount.md) - -## OverlayBannerUnmount type - -A function that will unmount the banner from the element. - -Signature: - -```typescript -export declare type OverlayBannerUnmount = () => void; -``` diff --git a/docs/development/core/public/kibana-plugin-public.toast.md b/docs/development/core/public/kibana-plugin-public.toast.md new file mode 100644 index 00000000000000..0cbbf29df073a2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.toast.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [Toast](./kibana-plugin-public.toast.md) + +## Toast type + +Signature: + +```typescript +export declare type Toast = ToastInputFields & { + id: string; +}; +``` diff --git a/docs/development/core/public/kibana-plugin-public.toastinput.md b/docs/development/core/public/kibana-plugin-public.toastinput.md index 75f12b3d945616..9dd20b5899f3a1 100644 --- a/docs/development/core/public/kibana-plugin-public.toastinput.md +++ b/docs/development/core/public/kibana-plugin-public.toastinput.md @@ -9,5 +9,5 @@ Inputs for [IToasts](./kibana-plugin-public.itoasts.md) APIs. Signature: ```typescript -export declare type ToastInput = string | ToastInputFields | Promise; +export declare type ToastInput = string | ToastInputFields; ``` diff --git a/docs/development/core/public/kibana-plugin-public.toastinputfields.md b/docs/development/core/public/kibana-plugin-public.toastinputfields.md index ffcf9e5c6dea29..3a6bc3a5e45da4 100644 --- a/docs/development/core/public/kibana-plugin-public.toastinputfields.md +++ b/docs/development/core/public/kibana-plugin-public.toastinputfields.md @@ -9,7 +9,10 @@ Allowed fields for [ToastInput](./kibana-plugin-public.toastinput.md). Signature: ```typescript -export declare type ToastInputFields = Pick>; +export declare type ToastInputFields = Pick> & { + title?: string | MountPoint; + text?: string | MountPoint; +}; ``` ## Remarks diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.add.md b/docs/development/core/public/kibana-plugin-public.toastsapi.add.md index 8e9648031f0e28..6b651b310e9743 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.add.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.add.md @@ -22,5 +22,5 @@ add(toastOrTitle: ToastInput): Toast; `Toast` -a +a [Toast](./kibana-plugin-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.adddanger.md b/docs/development/core/public/kibana-plugin-public.toastsapi.adddanger.md index 28e596f0c09e3d..67ebad919ed2a0 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.adddanger.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.adddanger.md @@ -22,5 +22,5 @@ addDanger(toastOrTitle: ToastInput): Toast; `Toast` -a +a [Toast](./kibana-plugin-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.adderror.md b/docs/development/core/public/kibana-plugin-public.toastsapi.adderror.md index c8a48b3fa46c9c..39090fb8f1bbef 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.adderror.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.adderror.md @@ -23,5 +23,5 @@ addError(error: Error, options: ErrorToastOptions): Toast; `Toast` -a +a [Toast](./kibana-plugin-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.addsuccess.md b/docs/development/core/public/kibana-plugin-public.toastsapi.addsuccess.md index 0e01dc1364d07f..ce9a9a2fae6911 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.addsuccess.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.addsuccess.md @@ -22,5 +22,5 @@ addSuccess(toastOrTitle: ToastInput): Toast; `Toast` -a +a [Toast](./kibana-plugin-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.addwarning.md b/docs/development/core/public/kibana-plugin-public.toastsapi.addwarning.md index 0e236f2737b128..948181f8257632 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.addwarning.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.addwarning.md @@ -22,5 +22,5 @@ addWarning(toastOrTitle: ToastInput): Toast; `Toast` -a +a [Toast](./kibana-plugin-public.toast.md) diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.md b/docs/development/core/public/kibana-plugin-public.toastsapi.md index e47f6d5c8ac590..ae4a2de9fc75cb 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.md @@ -28,5 +28,5 @@ export declare class ToastsApi implements IToasts | [addSuccess(toastOrTitle)](./kibana-plugin-public.toastsapi.addsuccess.md) | | Adds a new toast pre-configured with the success color and check icon. | | [addWarning(toastOrTitle)](./kibana-plugin-public.toastsapi.addwarning.md) | | Adds a new toast pre-configured with the warning color and help icon. | | [get$()](./kibana-plugin-public.toastsapi.get_.md) | | Observable of the toast messages to show to the user. | -| [remove(toast)](./kibana-plugin-public.toastsapi.remove.md) | | Removes a toast from the current array of toasts if present. | +| [remove(toastOrId)](./kibana-plugin-public.toastsapi.remove.md) | | Removes a toast from the current array of toasts if present. | diff --git a/docs/development/core/public/kibana-plugin-public.toastsapi.remove.md b/docs/development/core/public/kibana-plugin-public.toastsapi.remove.md index 5025c83a666c8a..9f270411752077 100644 --- a/docs/development/core/public/kibana-plugin-public.toastsapi.remove.md +++ b/docs/development/core/public/kibana-plugin-public.toastsapi.remove.md @@ -9,14 +9,14 @@ Removes a toast from the current array of toasts if present. Signature: ```typescript -remove(toast: Toast): void; +remove(toastOrId: Toast | string): void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| toast | Toast | a returned by | +| toastOrId | Toast | string | a [Toast](./kibana-plugin-public.toast.md) returned by [ToastsApi.add()](./kibana-plugin-public.toastsapi.add.md) or its id | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.unmountcallback.md b/docs/development/core/public/kibana-plugin-public.unmountcallback.md new file mode 100644 index 00000000000000..f44562120c9ee5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.unmountcallback.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [UnmountCallback](./kibana-plugin-public.unmountcallback.md) + +## UnmountCallback type + +A function that will unmount the element previously mounted by the associated [MountPoint](./kibana-plugin-public.mountpoint.md) + +Signature: + +```typescript +export declare type UnmountCallback = () => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.get.md b/docs/development/core/server/kibana-plugin-server.basepath.get.md index 04feca7ccc5a87..2b3b6c899e8ded 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.get.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.get.md @@ -9,5 +9,5 @@ returns `basePath` value, specific for an incoming request. Signature: ```typescript -get: (request: LegacyRequest | KibanaRequest) => string; +get: (request: KibanaRequest | LegacyRequest) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-server.basepath.md b/docs/development/core/server/kibana-plugin-server.basepath.md index da833c71bf93b6..478e29696966cd 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.md @@ -16,11 +16,11 @@ export declare class BasePath | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [get](./kibana-plugin-server.basepath.get.md) | | (request: LegacyRequest | KibanaRequest<unknown, unknown, unknown>) => string | returns basePath value, specific for an incoming request. | +| [get](./kibana-plugin-server.basepath.get.md) | | (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest) => string | returns basePath value, specific for an incoming request. | | [prepend](./kibana-plugin-server.basepath.prepend.md) | | (path: string) => string | Prepends path with the basePath. | | [remove](./kibana-plugin-server.basepath.remove.md) | | (path: string) => string | Removes the prepended basePath from the path. | | [serverBasePath](./kibana-plugin-server.basepath.serverbasepath.md) | | string | returns the server's basePathSee [BasePath.get](./kibana-plugin-server.basepath.get.md) for getting the basePath value for a specific request | -| [set](./kibana-plugin-server.basepath.set.md) | | (request: LegacyRequest | KibanaRequest<unknown, unknown, unknown>, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | +| [set](./kibana-plugin-server.basepath.set.md) | | (request: KibanaRequest<unknown, unknown, unknown> | LegacyRequest, requestSpecificBasePath: string) => void | sets basePath value, specific for an incoming request. | ## Remarks diff --git a/docs/development/core/server/kibana-plugin-server.basepath.set.md b/docs/development/core/server/kibana-plugin-server.basepath.set.md index cec70ee853bfa4..1272a134ef5c44 100644 --- a/docs/development/core/server/kibana-plugin-server.basepath.set.md +++ b/docs/development/core/server/kibana-plugin-server.basepath.set.md @@ -9,5 +9,5 @@ sets `basePath` value, specific for an incoming request. Signature: ```typescript -set: (request: LegacyRequest | KibanaRequest, requestSpecificBasePath: string) => void; +set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; ``` diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index caff7f5b1fdc6a..38fceeb47d6fd2 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -9,11 +9,12 @@ for displayed decimal values. . Scroll or search for the setting you want to modify. . Enter a new value for the setting. + [float] [[settings-read-only-access]] === [xpack]#Read only access# -When you have insufficient privileges to edit advanced settings, the following -indicator in Kibana will be displayed. The buttons to edit settings won't be visible. +When you have insufficient privileges to edit advanced settings, the following +indicator in Kibana will be displayed. The buttons to edit settings won't be visible. For more information on granting access to Kibana see <>. [role="screenshot"] @@ -25,9 +26,9 @@ image::images/settings-read-only-badge.png[Example of Advanced Settings Manageme WARNING: Modifying a setting can affect {kib} performance and cause problems that are -difficult to diagnose. Setting a property value to a blank field reverts +difficult to diagnose. Setting a property value to a blank field reverts to the default behavior, which might not be -compatible with other configuration settings. Deleting a custom setting +compatible with other configuration settings. Deleting a custom setting removes it from {kib} permanently. @@ -44,7 +45,7 @@ removes it from {kib} permanently. adapt to the interval between measurements. Keys are http://en.wikipedia.org/wiki/ISO_8601#Time_intervals[ISO8601 intervals]. `dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser. `dateNanosFormat`:: The format to use for displaying https://momentjs.com/docs/#/displaying/format/[pretty formatted dates] of {ref}/date_nanos.html[Elasticsearch date_nanos type]. -`defaultIndex`:: The index to access if no index is set. The default is `null`. +`defaultIndex`:: The index to access if no index is set. The default is `null`. `fields:popularLimit`:: The top N most popular fields to show. `filterEditor:suggestValues`:: Set this property to `false` to prevent the filter editor from suggesting values for fields. `filters:pinnedByDefault`:: Set this property to `true` to make filters have a global state (be pinned) by default. @@ -59,46 +60,46 @@ mentioned use "\_default_". `histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values when necessary. `history:limit`:: In fields that have history, such as query inputs, show this many recent values. -`indexPattern:fieldMapping:lookBack`:: For index patterns containing timestamps in their names, +`indexPattern:fieldMapping:lookBack`:: For index patterns containing timestamps in their names, look for this many recent matching patterns from which to query the field mapping. `indexPattern:placeholder`:: The default placeholder value to use in Management > Index Patterns > Create Index Pattern. -`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields +`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. `metrics:max_buckets`:: The maximum numbers of buckets that a single -data source can return. This might arise when the user selects a +data source can return. This might arise when the user selects a short interval (for example, 1s) for a long time period (1 year). -`query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character -in a query clause. Only applies when experimental query features are -enabled in the query bar. To disallow leading wildcards in Lucene queries, +`query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character +in a query clause. Only applies when experimental query features are +enabled in the query bar. To disallow leading wildcards in Lucene queries, use `query:queryString:options`. `query:queryString:options`:: Options for the Lucene query string parser. Only used when "Query language" is set to Lucene. -`savedObjects:listingLimit`:: The number of objects to fetch for lists of saved objects. +`savedObjects:listingLimit`:: The number of objects to fetch for lists of saved objects. The default value is 1000. Do not set above 10000. -`savedObjects:perPage`:: The number of objects to show on each page of the +`savedObjects:perPage`:: The number of objects to show on each page of the list of saved objects. The default is 5. `search:queryLanguage`:: The query language to use in the query bar. -Choices are <>, a language built specifically for {kib}, and the <>, a language built specifically for {kib}, and the <>. -`shortDots:enable`:: Set this property to `true` to shorten long +`shortDots:enable`:: Set this property to `true` to shorten long field names in visualizations. For example, show `f.b.baz` instead of `foo.bar.baz`. `sort:options`:: Options for the Elasticsearch {ref}/search-request-body.html#request-body-search-sort[sort] parameter. -`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the -URL, which can lead to problems when there is a lot of state information, -and the URL gets very long. -Enabling this setting stores part of the URL in your browser session to keep the +`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the +URL, which can lead to problems when there is a lot of state information, +and the URL gets very long. +Enabling this setting stores part of the URL in your browser session to keep the URL short. `theme:darkMode`:: Set to `true` to enable a dark mode for the {kib} UI. You must refresh the page to apply the setting. -`timepicker:quickRanges`:: The list of ranges to show in the Quick section of -the time filter. This should be an array of objects, with each object containing -`from`, `to` (see {ref}/common-options.html#date-math[accepted formats]), +`timepicker:quickRanges`:: The list of ranges to show in the Quick section of +the time filter. This should be an array of objects, with each object containing +`from`, `to` (see {ref}/common-options.html#date-math[accepted formats]), and `display` (the title to be displayed). `timepicker:refreshIntervalDefaults`:: The default refresh interval for the time filter. Example: `{ "display": "15 seconds", "pause": true, "value": 15000 }`. `timepicker:timeDefaults`:: The default selection in the time filter. `truncate:maxHeight`:: The maximum height that a cell occupies in a table. Set to 0 to disable truncation. -`xPack:defaultAdminEmail`:: Email address for X-Pack admin operations, such as +`xPack:defaultAdminEmail`:: Email address for X-Pack admin operations, such as cluster alert notifications from Monitoring. @@ -107,7 +108,7 @@ cluster alert notifications from Monitoring. === Accessibility settings [horizontal] -`accessibility:disableAnimations`:: Turns off all unnecessary animations in the +`accessibility:disableAnimations`:: Turns off all unnecessary animations in the {kib} UI. Refresh the page to apply the changes. [float] @@ -124,21 +125,21 @@ cluster alert notifications from Monitoring. [horizontal] `context:defaultSize`:: The number of surrounding entries to display in the context view. The default value is 5. `context:step`:: The number by which to increment or decrement the context size. The default value is 5. -`context:tieBreakerFields`:: A comma-separated list of fields to use -for breaking a tie between documents that have the same timestamp value. The first +`context:tieBreakerFields`:: A comma-separated list of fields to use +for breaking a tie between documents that have the same timestamp value. The first field that is present and sortable in the current index pattern is used. `defaultColumns`:: The columns that appear by default on the Discover page. -The default is `_source`. -`discover:aggs:terms:size`:: The number terms that are visualized when clicking +The default is `_source`. +`discover:aggs:terms:size`:: The number terms that are visualized when clicking the Visualize button in the field drop down. The default is `20`. `discover:sampleSize`:: The number of rows to show in the Discover table. `discover:sort:defaultOrder`:: The default sort direction for time-based index patterns. -`discover:searchOnPageLoad`:: Controls whether a search is executed when Discover first loads. +`discover:searchOnPageLoad`:: Controls whether a search is executed when Discover first loads. This setting does not have an effect when loading a saved search. `doc_table:hideTimeColumn`:: Hides the "Time" column in Discover and in all saved searches on dashboards. -`doc_table:highlight`:: Highlights results in Discover and saved searches on dashboards. +`doc_table:highlight`:: Highlights results in Discover and saved searches on dashboards. Highlighting slows requests when -working on big documents. +working on big documents. @@ -150,14 +151,14 @@ working on big documents. [horizontal] `notifications:banner`:: A custom banner intended for temporary notices to all users. Supports https://help.github.com/en/articles/basic-writing-and-formatting-syntax[Markdown]. -`notifications:lifetime:banner`:: The duration, in milliseconds, for banner -notification displays. The default value is 3000000. Set this field to `Infinity` +`notifications:lifetime:banner`:: The duration, in milliseconds, for banner +notification displays. The default value is 3000000. Set this field to `Infinity` to disable banner notifications. -`notifications:lifetime:error`:: The duration, in milliseconds, for error +`notifications:lifetime:error`:: The duration, in milliseconds, for error notification displays. The default value is 300000. Set this field to `Infinity` to disable error notifications. -`notifications:lifetime:info`:: The duration, in milliseconds, for information notification displays. +`notifications:lifetime:info`:: The duration, in milliseconds, for information notification displays. The default value is 5000. Set this field to `Infinity` to disable information notifications. -`notifications:lifetime:warning`:: The duration, in milliseconds, for warning notification +`notifications:lifetime:warning`:: The duration, in milliseconds, for warning notification displays. The default value is 10000. Set this field to `Infinity` to disable warning notifications. @@ -175,8 +176,8 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa === Rollup settings [horizontal] -`rollups:enableIndexPatterns`:: Enables the creation of index patterns that -capture rollup indices, which in turn enables visualizations based on rollup data. +`rollups:enableIndexPatterns`:: Enables the creation of index patterns that +capture rollup indices, which in turn enables visualizations based on rollup data. Refresh the page to apply the changes. @@ -188,22 +189,22 @@ Refresh the page to apply the changes. `courier:batchSearches`:: When disabled, dashboard panels will load individually, and search requests will terminate when users navigate away or update the query. When enabled, dashboard panels will load together when all of the data is loaded, and searches will not terminate. -`courier:customRequestPreference`:: {ref}/search-request-body.html#request-body-search-preference[Request preference] +`courier:customRequestPreference`:: {ref}/search-request-body.html#request-body-search-preference[Request preference] to use when `courier:setRequestPreference` is set to "custom". -`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. +`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. Useful when dashboards consist of visualizations from multiple index patterns. -`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] -setting used for `_msearch` requests sent by {kib}. Set to 0 to disable this +`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] +setting used for `_msearch` requests sent by {kib}. Set to 0 to disable this config and use the {es} default. `courier:setRequestPreference`:: Enables you to set which shards handle your search requests. -* *Session ID:* Restricts operations to execute all search requests on the same shards. +* *Session ID:* Restricts operations to execute all search requests on the same shards. This has the benefit of reusing shard caches across requests. -* *Custom:* Allows you to define your own preference. Use `courier:customRequestPreference` +* *Custom:* Allows you to define your own preference. Use `courier:customRequestPreference` to customize your preference value. -* *None:* Do not set a preference. This might provide better performance -because requests can be spread across all shard copies. However, results might +* *None:* Do not set a preference. This might provide better performance +because requests can be spread across all shard copies. However, results might be inconsistent because different shards might be in different refresh states. -`search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. +`search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. Searching through frozen indices might increase the search time. This setting is off by default. Users must opt-in to include frozen indices. @@ -212,8 +213,8 @@ might increase the search time. This setting is off by default. Users must opt-i === SIEM settings [horizontal] -`siem:defaultAnomalyScore`:: The threshold above which anomalies are displayed in the SIEM app. -`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. +`siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. +`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. `siem:refreshIntervalDefaults`:: The default refresh interval for the SIEM time filter, in milliseconds. `siem:timeDefaults`:: The default period of time in the SIEM time filter. @@ -226,16 +227,16 @@ might increase the search time. This setting is off by default. Users must opt-i `timelion:default_rows`:: The default number of rows to use on a Timelion sheet. `timelion:es.default_index`:: The default index when using the `.es()` query. `timelion:es.timefield`:: The default field containing a timestamp when using the `.es()` query. -`timelion:graphite.url`:: [experimental] Used with graphite queries, this is the URL of your graphite host -in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can be +`timelion:graphite.url`:: [experimental] Used with graphite queries, this is the URL of your graphite host +in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can be selected from a whitelist configured in the `kibana.yml` under `timelion.graphiteUrls`. `timelion:max_buckets`:: The maximum number of buckets a single data source can return. This value is used for calculating automatic intervals in visualizations. `timelion:min_interval`:: The smallest interval to calculate when using "auto". `timelion:quandl.key`:: [experimental] Used with quandl queries, this is your API key from https://www.quandl.com/[www.quandl.com]. -`timelion:showTutorial`:: Shows the Timelion tutorial +`timelion:showTutorial`:: Shows the Timelion tutorial to users when they first open the Timelion app. -`timelion:target_buckets`:: Used for calculating automatic intervals in visualizations, +`timelion:target_buckets`:: Used for calculating automatic intervals in visualizations, this is the number of buckets to try to represent. @@ -246,18 +247,18 @@ this is the number of buckets to try to represent. [horizontal] `visualization:colorMapping`:: Maps values to specified colors in visualizations. -`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed -when highlighting another element of the chart. The lower this number, the more +`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed +when highlighting another element of the chart. The lower this number, the more the highlighted element stands out. This must be a number between 0 and 1. -`visualization:loadingDelay`:: The time to wait before dimming visualizations +`visualization:loadingDelay`:: The time to wait before dimming visualizations during a query. -`visualization:regionmap:showWarnings`:: Shows +`visualization:regionmap:showWarnings`:: Shows a warning in a region map when terms cannot be joined to a shape. `visualization:tileMap:WMSdefaults`:: The default properties for the WMS map server support in the coordinate map. `visualization:tileMap:maxPrecision`:: The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, -and 12 is the maximum. See this +and 12 is the maximum. See this {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[explanation of cell dimensions]. -`visualize:enableLabs`:: Enables users to create, view, and edit experimental visualizations. +`visualize:enableLabs`:: Enables users to create, view, and edit experimental visualizations. If disabled, only visualizations that are considered production-ready are available to the user. @@ -265,6 +266,5 @@ If disabled, only visualizations that are considered production-ready are availa [[kibana-telemetry-settings]] === Usage data settings -Helps improve the Elastic Stack by providing usage statistics for +Helps improve the Elastic Stack by providing usage statistics for basic features. This data will not be shared outside of Elastic. - diff --git a/docs/management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png b/docs/management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png deleted file mode 100644 index 1e9600e4d4eb40..00000000000000 Binary files a/docs/management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png and /dev/null differ diff --git a/docs/management/dashboard_only_mode/images/custom_dashboard_mode_role.png b/docs/management/dashboard_only_mode/images/custom_dashboard_mode_role.png deleted file mode 100644 index e9285ac9143084..00000000000000 Binary files a/docs/management/dashboard_only_mode/images/custom_dashboard_mode_role.png and /dev/null differ diff --git a/docs/management/dashboard_only_mode/images/dashboard-only-user-role.png b/docs/management/dashboard_only_mode/images/dashboard-only-user-role.png deleted file mode 100644 index 0773f0f3a34070..00000000000000 Binary files a/docs/management/dashboard_only_mode/images/dashboard-only-user-role.png and /dev/null differ diff --git a/docs/management/dashboard_only_mode/images/view_only_dashboard.png b/docs/management/dashboard_only_mode/images/view_only_dashboard.png deleted file mode 100644 index a82a09c27e6e8a..00000000000000 Binary files a/docs/management/dashboard_only_mode/images/view_only_dashboard.png and /dev/null differ diff --git a/docs/management/dashboard_only_mode/index.asciidoc b/docs/management/dashboard_only_mode/index.asciidoc deleted file mode 100644 index 97ac4392827dd2..00000000000000 --- a/docs/management/dashboard_only_mode/index.asciidoc +++ /dev/null @@ -1,85 +0,0 @@ -[role="xpack"] -[[xpack-dashboard-only-mode]] -== Dashboard-only mode - -deprecated[7.4.0, "Using the `kibana_dashboard_only_user` role is deprecated. Use <> instead."] - -In dashboard-only mode, users have access to only the *Dashboard* app. -Users can view and filter the dashboards, but cannot create, edit, or delete -them. This enables you to: - -* Show off your dashboards without giving users access to all of {kib} - -* Share your {kib} dashboards without the risk of users accidentally -editing or deleting them - -Dashboard-only mode pairs well with fullscreen mode. -You can share your dashboard with the team responsible -for showing the dashboard on a big-screen monitor, and not worry about it being modified. - -[role="screenshot"] -image:management/dashboard_only_mode/images/view_only_dashboard.png["View Only Dashboard"] - -[[setup-dashboard-only-mode]] -[float] -=== Assign dashboard-only mode -With {security} enabled, you can restrict users to dashboard-only mode by assigning -them the built-in `kibana_dashboard_only_user` role. - -. Go to *Management > Security > Users*. -. Create or edit a user. -. Assign the `kibana_dashboard_only_user` role and a role that <>. -+ -For example, -to enable users to view the dashboards in the sample data sets, you must assign them -the `kibana_dashboard_only_user` role and a role that has -`read` access to the kibana_* indices. -+ -[role="screenshot"] -image:management/dashboard_only_mode/images/dashboard-only-user-role.png["Dashboard Only mode has no editing controls"] - -[IMPORTANT] -=========================================== -* If you assign users the `kibana_dashboard_only_user` role and a role -with write permissions to {kib}, they *will* have write access, -even though the controls remain hidden in {kib}. - -* If you also assign users the reserved `superuser` role, they will have full -access to {kib}. - -=========================================== - -[float] -[[grant-read-access-to-indices]] -=== Grant read access to indices - -The `kibana_dashboard_only_user` role -does not provide access to data indices. -You must also assign the user a role that grants `read` access -to each index you are using. Use *Management > Security > Roles* to create or edit a -role and assign index privileges. -For information on roles and privileges, see {ref}/authorization.html[User authorization]. - -[role="screenshot"] -image:management/dashboard_only_mode/images/custom_dashboard_mode_role.png["Dashboard Only mode has no editing controls"] - - -[float] -[[advanced-dashboard-mode-configuration]] -=== Advanced settings for dashboard only mode - -The `kibana_dashboard_only_user` role grants access to all spaces. -If your setup requires access to a -subset of spaces, you can create a custom role, and then tag it as Dashboard only mode. - -. Go to *Management > Advanced Settings*, and search for `xpackDashboardMode:roles`. -+ -By -default, this is set to -`kibana_dashboard_only_user`. - -. Add as many roles as you require. -+ -[role="screenshot"] -image:management/dashboard_only_mode/images/advanced_dashboard_mode_role_setup.png["Advanced dashboard mode role setup"] - diff --git a/docs/maps/images/grid_to_docs.gif b/docs/maps/images/grid_to_docs.gif new file mode 100644 index 00000000000000..11b396a4fe8727 Binary files /dev/null and b/docs/maps/images/grid_to_docs.gif differ diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index 923b6cc1d6649a..cd01acb2df7de5 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -7,6 +7,14 @@ Use {ref}/search-aggregations.html[aggregations] to plot large data sets without Aggregations group your documents into buckets and calculate metrics for each bucket. Your documents stay in Elasticsearch and only the metrics for each group are returned to your computer. +Use aggregated layers with document layers to show aggregated views when the map shows larger +amounts of the globe and individual documents when the map shows smaller regions. + +In the following example, the Grid aggregation layer is only visible when the map is at zoom levels 0 through 5. The Documents layer is only visible when the map is at zoom levels 4 through 24. +See the <> tutorial for more details on configuring the layers. + +[role="screenshot"] +image::maps/images/grid_to_docs.gif[] [role="xpack"] [[maps-grid-aggregation]] diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 3ff44b97de6356..e6908ca773a2f6 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -150,6 +150,7 @@ image::maps/images/grid_metrics_both.png[] . In the map legend, click *Add layer*. . Click the *Grid aggregation* data source. . Set *Index pattern* to *kibana_sample_data_logs*. +. Set *Show as* to *points*. . Click the *Add layer* button. . Set *Layer name* to `Total Requests and Bytes`. . Set *Zoom range for layer visibility* to the range [0, 9]. @@ -181,7 +182,7 @@ Now that your map is complete, you'll want to save it so others can use it. . In the application toolbar, click *Save*. . Enter `Tutorial web logs map` for the title. -. Click *Confirm Save*. +. Click *Save*. + You have completed the steps for re-creating the sample data map. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index af67ff70c81b59..920c448acf6dbd 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -38,4 +38,10 @@ This page has moved. Please see <>. [role="exclude",id="extend"] == Extend your use case -This page was deleted. See <> and <>. \ No newline at end of file +This page was deleted. See <> and <>. + +[role="exclude",id="xpack-dashboard-only-mode"] +== Dashboard-only mode + +Using the `kibana_dashboard_only_user` role is deprecated. +Use <> instead. diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index e47326a1d20687..8cbbcdcbca3efc 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -23,8 +23,6 @@ xpack.apm.ui.transactionGroupBucketSize:: Number of top transaction groups displ xpack.apm.ui.maxTraceItems:: Max number of child items displayed when viewing trace details. Defaults to `1000`. -apm_oss.apmAgentConfigurationIndex:: Index containing agent configuration settings. Defaults to `.apm-agent-configuration`. - apm_oss.indexPattern:: Index pattern is used for integrations with Machine Learning and Kuery Bar. It must match all apm indices. Defaults to `apm-*`. apm_oss.errorIndices:: Matcher for indices containing error documents. Defaults to `apm-*`. @@ -34,3 +32,8 @@ apm_oss.onboardingIndices:: Matcher for indices containing onboarding documents. apm_oss.spanIndices:: Matcher for indices containing span documents. Defaults to `apm-*`. apm_oss.transactionIndices:: Matcher for indices containing transaction documents. Defaults to `apm-*`. + +apm_oss.metricsIndices:: Matcher for indices containing metric documents. Defaults to `apm-*`. + +apm_oss.sourcemapIndices:: Matcher for indices containing sourcemap documents. Defaults to `apm-*`. + diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 6e7f939a1d2abd..f4434ea7a09f46 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -32,7 +32,8 @@ strongly recommend that you keep the default CSP rules that ship with Kibana. `csp.strict:`:: *Default: `true`* Blocks access to Kibana to any browser that does not enforce even rudimentary CSP rules. In practice, this will disable -support for older, less safe browsers like Internet Explorer. +support for older, less safe browsers like Internet Explorer. +See <> for more information. `csp.warnLegacyBrowsers:`:: *Default: `true`* Shows a warning message after loading Kibana to any browser that does not enforce even rudimentary CSP rules, @@ -240,6 +241,10 @@ Kibana reads this url from an external metadata service, but users can still override this parameter to use their own Tile Map Service. For example: `"https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana"` +`newsfeed.enabled:` :: *Default: `true`* Controls whether to enable the newsfeed +system for the Kibana UI notification center. Set to `false` to disable the +newsfeed system. + `path.data:`:: *Default: `data`* The path where Kibana stores persistent data not saved in Elasticsearch. @@ -315,6 +320,18 @@ supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2 setting this to `true` enables unauthenticated users to access the Kibana server status API and status page. +`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, +users are able to change the telemetry setting at a later time in +<>. If `false`, +{kib} looks at the value of `telemetry.optIn` to determine whether to send +telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` +cannot be `false` at the same time. + +`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. + If `false`, collection of telemetry data is disabled. + To enable telemetry and prevent users from disabling it, + set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. + `vega.enableExternalUrls:`:: *Default: false* Set this value to true to allow Vega to use any URL to access external data sources and images. If false, Vega can only get data from Elasticsearch. `xpack.license_management.enabled`:: *Default: true* Set this value to false to diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index ce0259c690b821..8c03032bb8ac36 100644 --- a/docs/setup/upgrade.asciidoc +++ b/docs/setup/upgrade.asciidoc @@ -2,7 +2,15 @@ == Upgrading {kib} Depending on the {kib} version you're upgrading from, the upgrade process to 7.0 -varies. +varies. + +NOTE: {kib} upgrades automatically when starting a new version, as described in +<>. +Although you do not need to manually back up {kib} before upgrading, we recommend +that you have a backup on hand. You can use +<> to back up {kib} +data by targeting `.kibana*` indices. If you are using the Reporting plugin, +you can also target `.reporting*` indices. [float] [[upgrade-before-you-begin]] @@ -12,7 +20,7 @@ Before you upgrade {kib}: * Consult the <>. * Before you upgrade production servers, test the upgrades in a dev environment. -* Backup your data with {es} {ref}/modules-snapshots.html[snapshots]. +* Back up your data with {es} {ref}/modules-snapshots.html[snapshots]. To roll back to an earlier version, you **must** have a backup of your data. * If you are using custom plugins, check that a compatible version is available. diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index 3f9d8fa17bddd7..2923e42c1ab34b 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -154,7 +154,6 @@ To open an element for editing, put the dashboard in *Edit* mode, and then select *Edit* from the panel menu. The changes you make appear in every dashboard that uses the element. -include::{kib-repo-dir}/management/dashboard_only_mode/index.asciidoc[] diff --git a/docs/user/graph/configuring-graph.asciidoc b/docs/user/graph/configuring-graph.asciidoc index d521f9d8d2846d..5427bdee79ecbb 100644 --- a/docs/user/graph/configuring-graph.asciidoc +++ b/docs/user/graph/configuring-graph.asciidoc @@ -59,14 +59,14 @@ image::user/graph/images/graph-read-only-badge.png[Example of Graph's read only [discrete] [[disable-drill-down]] -=== Disabling drill down configuration +=== Disabling drilldown configuration -By default, users can configure _drill down_ URLs to display additional +By default, users can configure _drilldown_ URLs to display additional information about a selected vertex in a new browser window. For example, -you could configure a drill down URL to perform a web search for the selected +you could configure a drilldown URL to perform a web search for the selected vertex term. -To prevent users from adding drill down URLs, set +To prevent users from adding drilldown URLs, set `xpack.graph.canEditDrillDownUrls` to `false` in `kibana.yml`: [source,yaml] diff --git a/docs/user/graph/getting-started.asciidoc b/docs/user/graph/getting-started.asciidoc index 19f3df341338ec..7b3bd10147966a 100644 --- a/docs/user/graph/getting-started.asciidoc +++ b/docs/user/graph/getting-started.asciidoc @@ -2,30 +2,30 @@ [[graph-getting-started]] == Using Graph -Graph is automatically enabled in {es} and {kib}. +You must index data into {es} before you can create a graph. +<> or get started with a <>. +[float] [[exploring-connections]] -To start exploring connections in your data: +=== Graph connections in your data -. From the side navigation, open the graph explorer. - -. Select an index pattern to specify what indices you want to explore. +. From the side navigation, open *Graph*. + -For example, if you are indexing log data with Logstash, you could select the -`logstash-*` index pattern to visualize connections within the log entries. +If this is your first graph, follow the prompts to create it. +For subsequent graphs, click *New*. + +. Select a data source to explore. -. Select one or more multi-value fields that contain the terms you want to +. Add one or more multi-value fields that contain the terms you want to graph. + -The vertices in the graph are selected from these terms. If you're -visualizing connections between Apache log entries, you could select the -`url.raw` field and the `geo.src` field so you can look at which pages are -being accessed from different locations. +The vertices in the graph are selected from these terms. . Enter a search query to discover relationships between terms in the selected fields. + -For example, to generate a graph of the successful requests to +For example, if you are using the {kib} sample web logs data set, and you want +to generate a graph of the successful requests to particular pages from different locations, you could search for the 200 response code. The weight of the connection between two vertices indicates how strongly they are related. @@ -38,25 +38,86 @@ image::user/graph/images/graph-url-connections.png["URL connections"] [role="screenshot"] image::user/graph/images/graph-link-summary.png["Link summary"] -. Use the toolbar buttons to explore +. Use the control bar on the right to explore additional connections: + -* To display additional vertices that connect to your graph, click Expand -image:user/graph/images/graph-expand-button.jpg[Expand Selection]. +* To display additional vertices that connect to your graph, click the expand icon +image:user/graph/images/graph-expand-button.png[Expand Selection]. * To display additional -connections between the displayed vertices, click Link -image:user/graph/images/graph-link-button.jpg[Add links to existing terms] +connections between the displayed vertices, click the link icon +image:user/graph/images/graph-link-button.png[Add links to existing terms]. * To explore a particular area of the -graph, select the vertices you are interested in, and click Expand or Link. -* To step back through your changes to the graph, click Undo -image:user/graph/images/graph-undo-button.jpg[Undo]. +graph, select the vertices you are interested in, and then click expand or link. +* To step back through your changes to the graph, click undo +image:user/graph/images/graph-undo-button.png[Undo] and redo +image:user/graph/images/graph-redo-button.png[Redo]. . To see more relationships in your data, submit additional queries. + [role="screenshot"] image::user/graph/images/graph-add-query.png["Adding networks"] +. *Save* your graph. + +[float] +[[style-vertex-properties]] +=== Style vertex properties + +Each vertex has a color, icon, and label. To change +the color or icon of all vertices +of a certain field, click the field badge below the search bar, and then +select *Edit settings*. + +To change the color and label of selected vertices, +click the style icon image:user/graph/images/graph-style-button.png[Style] +in the control bar on the right. + + +[float] +[[edit-graph-settings]] +=== Edit graph settings + +By default, *Graph* is configured to tune out noise in your data. +If this isn't a good fit for your data, use *Settings > Advanced settings* +to adjust the way *Graph* queries your data. You can tune the graph to show +only the results relevant to you and to improve performance. +For more information, see <>. + +You can configure the number of vertices that a search or +expand operation adds to the graph. +By default, only the five most relevant terms for any given field are added +at a time. This keeps the graph from overflowing. To increase this number, click +a field below the search bar, select *Edit Settings*, and change *Terms per hop*. + +[float] +[[graph-block-terms]] +=== Block terms from the graph +Documents that match a blocked term are not allowed in the graph. +To block a term, select its vertex and click +the block icon +image:user/graph/images/graph-block-button.png[Block selection] +in the control panel. +For a list of blocked terms, go to *Settings > Blocked terms*. + +[float] +[[graph-drill-down]] +=== Drill down into raw documents +With drilldowns, you can display additional information about a +selected vertex in a new browser window. For example, you might +configure a drilldown URL to perform a web search for the selected vertex term. + +Use the drilldown icon image:user/graph/images/graph-info-icon.png[Drilldown selection] +in the control panel to show the drilldown buttons for the selected vertices. +To configure drilldowns, go to *Settings > Drilldowns*. See also +<>. -NOTE: By default, when you submit a search query, Graph searches all available -fields. You can constrain your search to a particular field using the Lucene -query syntax. For example, `machine.os: osx`. +[float] +[[graph-run-layout]] +=== Run and pause layout +Graph uses a "force layout", where vertices behave like magnets, +pushing off of one another. By default, when you add a new vertex to +the graph, all vertices begin moving. In some cases, the movement might +go on for some time. To freeze the current vertex position, +click the pause icon +image:user/graph/images/graph-pause-button.png[Block selection] +in the control panel. diff --git a/docs/user/graph/images/graph-add-query.png b/docs/user/graph/images/graph-add-query.png old mode 100644 new mode 100755 index 7f0cea7fb3aa47..0b978462ae75e6 Binary files a/docs/user/graph/images/graph-add-query.png and b/docs/user/graph/images/graph-add-query.png differ diff --git a/docs/user/graph/images/graph-block-button.png b/docs/user/graph/images/graph-block-button.png new file mode 100755 index 00000000000000..b378883572f3c3 Binary files /dev/null and b/docs/user/graph/images/graph-block-button.png differ diff --git a/docs/user/graph/images/graph-expand-button.jpg b/docs/user/graph/images/graph-expand-button.jpg deleted file mode 100644 index 296ba9d25ad5e7..00000000000000 Binary files a/docs/user/graph/images/graph-expand-button.jpg and /dev/null differ diff --git a/docs/user/graph/images/graph-expand-button.png b/docs/user/graph/images/graph-expand-button.png new file mode 100755 index 00000000000000..7d9816a8adb936 Binary files /dev/null and b/docs/user/graph/images/graph-expand-button.png differ diff --git a/docs/user/graph/images/graph-info-icon.png b/docs/user/graph/images/graph-info-icon.png new file mode 100644 index 00000000000000..c1e89384b738cc Binary files /dev/null and b/docs/user/graph/images/graph-info-icon.png differ diff --git a/docs/user/graph/images/graph-link-button.jpg b/docs/user/graph/images/graph-link-button.jpg deleted file mode 100644 index 7c7f1770115769..00000000000000 Binary files a/docs/user/graph/images/graph-link-button.jpg and /dev/null differ diff --git a/docs/user/graph/images/graph-link-button.png b/docs/user/graph/images/graph-link-button.png new file mode 100755 index 00000000000000..43ac85537b9bb7 Binary files /dev/null and b/docs/user/graph/images/graph-link-button.png differ diff --git a/docs/user/graph/images/graph-link-summary.png b/docs/user/graph/images/graph-link-summary.png old mode 100644 new mode 100755 index e669a2d79e0e39..ca07f350f975ed Binary files a/docs/user/graph/images/graph-link-summary.png and b/docs/user/graph/images/graph-link-summary.png differ diff --git a/docs/user/graph/images/graph-pause-button.png b/docs/user/graph/images/graph-pause-button.png new file mode 100755 index 00000000000000..e05418d7df7f9c Binary files /dev/null and b/docs/user/graph/images/graph-pause-button.png differ diff --git a/docs/user/graph/images/graph-redo-button.png b/docs/user/graph/images/graph-redo-button.png new file mode 100755 index 00000000000000..5adcfa478652e9 Binary files /dev/null and b/docs/user/graph/images/graph-redo-button.png differ diff --git a/docs/user/graph/images/graph-style-button.png b/docs/user/graph/images/graph-style-button.png new file mode 100644 index 00000000000000..5015e39b8095ae Binary files /dev/null and b/docs/user/graph/images/graph-style-button.png differ diff --git a/docs/user/graph/images/graph-undo-button.jpg b/docs/user/graph/images/graph-undo-button.jpg deleted file mode 100644 index 5c87a2809a4809..00000000000000 Binary files a/docs/user/graph/images/graph-undo-button.jpg and /dev/null differ diff --git a/docs/user/graph/images/graph-undo-button.png b/docs/user/graph/images/graph-undo-button.png new file mode 100755 index 00000000000000..e8a72b8b358c5f Binary files /dev/null and b/docs/user/graph/images/graph-undo-button.png differ diff --git a/docs/user/graph/images/graph-url-connections.png b/docs/user/graph/images/graph-url-connections.png old mode 100644 new mode 100755 index 737c081cd691b5..94c9c4341d9b87 Binary files a/docs/user/graph/images/graph-url-connections.png and b/docs/user/graph/images/graph-url-connections.png differ diff --git a/docs/user/graph/troubleshooting.asciidoc b/docs/user/graph/troubleshooting.asciidoc index ff3568ed41afa2..4ce287396f8091 100644 --- a/docs/user/graph/troubleshooting.asciidoc +++ b/docs/user/graph/troubleshooting.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[graph-troubleshooting]] -== Graph Troubleshooting +== Graph troubleshooting ++++ Troubleshooting ++++ diff --git a/docs/user/monitoring/monitoring-troubleshooting.asciidoc b/docs/user/monitoring/monitoring-troubleshooting.asciidoc index 12a775ad047b08..7e1d6f94f15fa3 100644 --- a/docs/user/monitoring/monitoring-troubleshooting.asciidoc +++ b/docs/user/monitoring/monitoring-troubleshooting.asciidoc @@ -23,6 +23,23 @@ You cannot monitor a version 6.3 or later cluster from {kib} version 6.2 or earl To resolve this issue, upgrade {kib} to 6.3 or later. See {stack-ref}/upgrading-elastic-stack.html[Upgrading the {stack}]. +[float] +=== {filebeat} index is corrupt + +*Symptoms:* + +The *Stack Monitoring* application displays a Monitoring Request error indicating +that an illegal argument exception has occurred because fielddata is disabled on +text fields by default. + +*Resolution* + + . Stop all your {filebeat} instances. + . Delete indices beginning with `filebeat-$VERSION`, where `VERSION` corresponds + to the version of {filebeat} running. +. Restart all your {filebeat} instances. + + [float] === No monitoring data is visible in {kib} diff --git a/package.json b/package.json index cf9158c3a59b8d..8fa9bf1847eb81 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "@elastic/charts": "^14.0.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "1.0.5", - "@elastic/eui": "14.8.0", + "@elastic/eui": "14.9.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -433,7 +433,7 @@ "pngjs": "^3.4.0", "postcss": "^7.0.5", "postcss-url": "^8.0.0", - "prettier": "^1.18.2", + "prettier": "^1.19.1", "proxyquire": "1.8.0", "regenerate": "^1.4.0", "sass-lint": "^1.12.1", diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json index e2f3a59e95a47c..b0ac86b465a621 100644 --- a/packages/kbn-analytics/package.json +++ b/packages/kbn-analytics/package.json @@ -17,6 +17,6 @@ "@babel/cli": "7.5.5", "@kbn/dev-utils": "1.0.0", "@kbn/babel-preset": "1.0.0", - "typescript": "3.5.1" + "typescript": "3.5.3" } } diff --git a/packages/kbn-analytics/src/index.ts b/packages/kbn-analytics/src/index.ts index 63fd115fa75945..6514347b0b1272 100644 --- a/packages/kbn-analytics/src/index.ts +++ b/packages/kbn-analytics/src/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export { createReporter, ReportHTTP, Reporter, ReporterConfig } from './reporter'; +export { ReportHTTP, Reporter, ReporterConfig } from './reporter'; export { UiStatsMetricType, METRIC_TYPE } from './metrics'; export { Report, ReportManager } from './report'; diff --git a/packages/kbn-analytics/src/metrics/index.ts b/packages/kbn-analytics/src/metrics/index.ts index 13b9e5dc59e4e2..ceaf53cbc97536 100644 --- a/packages/kbn-analytics/src/metrics/index.ts +++ b/packages/kbn-analytics/src/metrics/index.ts @@ -17,21 +17,17 @@ * under the License. */ -import { UiStatsMetric, UiStatsMetricType } from './ui_stats'; +import { UiStatsMetric } from './ui_stats'; +import { UserAgentMetric } from './user_agent'; -export { - UiStatsMetric, - createUiStatsMetric, - UiStatsMetricReport, - UiStatsMetricType, -} from './ui_stats'; +export { UiStatsMetric, createUiStatsMetric, UiStatsMetricType } from './ui_stats'; export { Stats } from './stats'; +export { trackUsageAgent } from './user_agent'; -export type Metric = UiStatsMetric; -export type MetricType = keyof typeof METRIC_TYPE; - +export type Metric = UiStatsMetric | UserAgentMetric; export enum METRIC_TYPE { COUNT = 'count', LOADED = 'loaded', CLICK = 'click', + USER_AGENT = 'user_agent', } diff --git a/packages/kbn-analytics/src/metrics/ui_stats.ts b/packages/kbn-analytics/src/metrics/ui_stats.ts index 7615fd20645e28..dc8cdcd3e4a1e9 100644 --- a/packages/kbn-analytics/src/metrics/ui_stats.ts +++ b/packages/kbn-analytics/src/metrics/ui_stats.ts @@ -17,37 +17,33 @@ * under the License. */ -import { Stats } from './stats'; import { METRIC_TYPE } from './'; export type UiStatsMetricType = METRIC_TYPE.CLICK | METRIC_TYPE.LOADED | METRIC_TYPE.COUNT; -export interface UiStatsMetricConfig { - type: T; +export interface UiStatsMetricConfig { + type: UiStatsMetricType; appName: string; eventName: string; count?: number; } -export interface UiStatsMetric { - type: T; +export interface UiStatsMetric { + type: UiStatsMetricType; appName: string; eventName: string; count: number; } -export function createUiStatsMetric({ +export function createUiStatsMetric({ type, appName, eventName, count = 1, -}: UiStatsMetricConfig): UiStatsMetric { - return { type, appName, eventName, count }; -} - -export interface UiStatsMetricReport { - key: string; - appName: string; - eventName: string; - type: UiStatsMetricType; - stats: Stats; +}: UiStatsMetricConfig): UiStatsMetric { + return { + type, + appName, + eventName, + count, + }; } diff --git a/packages/kbn-analytics/src/metrics/user_agent.ts b/packages/kbn-analytics/src/metrics/user_agent.ts new file mode 100644 index 00000000000000..32282dc54bde6d --- /dev/null +++ b/packages/kbn-analytics/src/metrics/user_agent.ts @@ -0,0 +1,35 @@ +/* + * 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 { METRIC_TYPE } from './'; + +export interface UserAgentMetric { + type: METRIC_TYPE.USER_AGENT; + appName: string; + userAgent: string; +} + +export function trackUsageAgent(appName: string): UserAgentMetric { + const userAgent = (window && window.navigator && window.navigator.userAgent) || ''; + + return { + type: METRIC_TYPE.USER_AGENT, + appName, + userAgent, + }; +} diff --git a/packages/kbn-analytics/src/report.ts b/packages/kbn-analytics/src/report.ts index 6187455fa60a55..333bc05d28f9be 100644 --- a/packages/kbn-analytics/src/report.ts +++ b/packages/kbn-analytics/src/report.ts @@ -17,28 +17,47 @@ * under the License. */ -import { UnreachableCaseError } from './util'; -import { Metric, Stats, UiStatsMetricReport, METRIC_TYPE } from './metrics'; +import { UnreachableCaseError, wrapArray } from './util'; +import { Metric, Stats, UiStatsMetricType, METRIC_TYPE } from './metrics'; +const REPORT_VERSION = 1; export interface Report { + reportVersion: typeof REPORT_VERSION; uiStatsMetrics: { - [key: string]: UiStatsMetricReport; + [key: string]: { + key: string; + appName: string; + eventName: string; + type: UiStatsMetricType; + stats: Stats; + }; + }; + userAgent?: { + [key: string]: { + userAgent: string; + key: string; + type: METRIC_TYPE.USER_AGENT; + appName: string; + }; }; } export class ReportManager { + static REPORT_VERSION = REPORT_VERSION; public report: Report; constructor(report?: Report) { this.report = report || ReportManager.createReport(); } - static createReport() { - return { uiStatsMetrics: {} }; + static createReport(): Report { + return { reportVersion: REPORT_VERSION, uiStatsMetrics: {} }; } public clearReport() { this.report = ReportManager.createReport(); } public isReportEmpty(): boolean { - return Object.keys(this.report.uiStatsMetrics).length === 0; + const noUiStats = Object.keys(this.report.uiStatsMetrics).length === 0; + const noUserAgent = !this.report.userAgent || Object.keys(this.report.userAgent).length === 0; + return noUiStats && noUserAgent; } private incrementStats(count: number, stats?: Stats): Stats { const { min = 0, max = 0, sum = 0 } = stats || {}; @@ -54,28 +73,46 @@ export class ReportManager { sum: newSum, }; } - assignReports(newMetrics: Metric[]) { - newMetrics.forEach(newMetric => this.assignReport(this.report, newMetric)); + assignReports(newMetrics: Metric | Metric[]) { + wrapArray(newMetrics).forEach(newMetric => this.assignReport(this.report, newMetric)); } static createMetricKey(metric: Metric): string { switch (metric.type) { + case METRIC_TYPE.USER_AGENT: { + const { appName, type } = metric; + return `${appName}-${type}`; + } case METRIC_TYPE.CLICK: case METRIC_TYPE.LOADED: case METRIC_TYPE.COUNT: { - const { appName, type, eventName } = metric; + const { appName, eventName, type } = metric; return `${appName}-${type}-${eventName}`; } default: - throw new UnreachableCaseError(metric.type); + throw new UnreachableCaseError(metric); } } private assignReport(report: Report, metric: Metric) { + const key = ReportManager.createMetricKey(metric); switch (metric.type) { + case METRIC_TYPE.USER_AGENT: { + const { appName, type, userAgent } = metric; + if (userAgent) { + this.report.userAgent = { + [key]: { + key, + appName, + type, + userAgent: metric.userAgent, + }, + }; + } + return; + } case METRIC_TYPE.CLICK: case METRIC_TYPE.LOADED: case METRIC_TYPE.COUNT: { const { appName, type, eventName, count } = metric; - const key = ReportManager.createMetricKey(metric); const existingStats = (report.uiStatsMetrics[key] || {}).stats; this.report.uiStatsMetrics[key] = { key, @@ -87,7 +124,7 @@ export class ReportManager { return; } default: - throw new UnreachableCaseError(metric.type); + throw new UnreachableCaseError(metric); } } } diff --git a/packages/kbn-analytics/src/reporter.ts b/packages/kbn-analytics/src/reporter.ts index 37d23aa4430902..98e29c1e4329eb 100644 --- a/packages/kbn-analytics/src/reporter.ts +++ b/packages/kbn-analytics/src/reporter.ts @@ -18,7 +18,7 @@ */ import { wrapArray } from './util'; -import { Metric, UiStatsMetric, createUiStatsMetric } from './metrics'; +import { Metric, createUiStatsMetric, trackUsageAgent, UiStatsMetricType } from './metrics'; import { Storage, ReportStorageManager } from './storage'; import { Report, ReportManager } from './report'; @@ -40,10 +40,11 @@ export class Reporter { private reportManager: ReportManager; private storageManager: ReportStorageManager; private debug: boolean; + private retryCount = 0; + private readonly maxRetries = 3; constructor(config: ReporterConfig) { - const { http, storage, debug, checkInterval = 10000, storageKey = 'analytics' } = config; - + const { http, storage, debug, checkInterval = 90000, storageKey = 'analytics' } = config; this.http = http; this.checkInterval = checkInterval; this.interval = null; @@ -59,18 +60,19 @@ export class Reporter { } private flushReport() { + this.retryCount = 0; this.reportManager.clearReport(); this.storageManager.store(this.reportManager.report); } - public start() { + public start = () => { if (!this.interval) { this.interval = setTimeout(() => { this.interval = null; this.sendReports(); }, this.checkInterval); } - } + }; private log(message: any) { if (this.debug) { @@ -79,36 +81,42 @@ export class Reporter { } } - public reportUiStats( + public reportUiStats = ( appName: string, - type: UiStatsMetric['type'], + type: UiStatsMetricType, eventNames: string | string[], count?: number - ) { + ) => { const metrics = wrapArray(eventNames).map(eventName => { - if (this) this.log(`${type} Metric -> (${appName}:${eventName}):`); + this.log(`${type} Metric -> (${appName}:${eventName}):`); const report = createUiStatsMetric({ type, appName, eventName, count }); this.log(report); return report; }); this.saveToReport(metrics); - } + }; + + public reportUserAgent = (appName: string) => { + this.log(`Reporting user-agent.`); + const report = trackUsageAgent(appName); + this.saveToReport([report]); + }; - public async sendReports() { + public sendReports = async () => { if (!this.reportManager.isReportEmpty()) { try { await this.http(this.reportManager.report); this.flushReport(); } catch (err) { this.log(`Error Sending Metrics Report ${err}`); + this.retryCount = this.retryCount + 1; + const versionMismatch = + this.reportManager.report.reportVersion !== ReportManager.REPORT_VERSION; + if (versionMismatch || this.retryCount > this.maxRetries) { + this.flushReport(); + } } } this.start(); - } -} - -export function createReporter(reportedConf: ReporterConfig) { - const reporter = new Reporter(reportedConf); - reporter.start(); - return reporter; + }; } diff --git a/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap index 2836c80f5b5ad6..81fafdb4a7b339 100644 --- a/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap +++ b/packages/kbn-config-schema/src/types/__snapshots__/uri_type.test.ts.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`#scheme returns error when shorter string 1`] = `"expected URI with scheme [http|https] but but got [ftp://elastic.co]."`; +exports[`#scheme returns error when shorter string 1`] = `"expected URI with scheme [http|https] but got [ftp://elastic.co]."`; -exports[`#scheme returns error when shorter string 2`] = `"expected URI with scheme [http|https] but but got [file:///kibana.log]."`; +exports[`#scheme returns error when shorter string 2`] = `"expected URI with scheme [http|https] but got [file:///kibana.log]."`; exports[`#validate throws when returns string 1`] = `"validator failure"`; diff --git a/packages/kbn-config-schema/src/types/uri_type.ts b/packages/kbn-config-schema/src/types/uri_type.ts index f283554de527e3..df1ce9e869d3b4 100644 --- a/packages/kbn-config-schema/src/types/uri_type.ts +++ b/packages/kbn-config-schema/src/types/uri_type.ts @@ -36,7 +36,7 @@ export class URIType extends Type { case 'string.base': return `expected value of type [string] but got [${typeDetect(value)}].`; case 'string.uriCustomScheme': - return `expected URI with scheme [${scheme}] but but got [${value}].`; + return `expected URI with scheme [${scheme}] but got [${value}].`; case 'string.uri': return `value is [${value}] but it must be a valid URI (see RFC 3986).`; } diff --git a/packages/kbn-dev-utils/src/proc_runner/observe_readable.ts b/packages/kbn-dev-utils/src/proc_runner/observe_readable.ts index 8feab74d363212..1a292aff303afd 100644 --- a/packages/kbn-dev-utils/src/proc_runner/observe_readable.ts +++ b/packages/kbn-dev-utils/src/proc_runner/observe_readable.ts @@ -29,10 +29,7 @@ import { first, ignoreElements, mergeMap } from 'rxjs/operators'; */ export function observeReadable(readable: Readable): Rx.Observable { return Rx.race( - Rx.fromEvent(readable, 'end').pipe( - first(), - ignoreElements() - ), + Rx.fromEvent(readable, 'end').pipe(first(), ignoreElements()), Rx.fromEvent(readable, 'error').pipe( first(), diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.ts index 259bfd782d3fbe..21f02325cac662 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.ts +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.ts @@ -112,10 +112,7 @@ describe('#getWritten$()', () => { const done$ = new Rx.Subject(); const promise = log .getWritten$() - .pipe( - takeUntil(done$), - toArray() - ) + .pipe(takeUntil(done$), toArray()) .toPromise(); log.debug('foo'); diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 34a56615ed43a1..ac46dd02757cf9 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -50,7 +50,7 @@ "log-symbols": "^2.2.0", "ncp": "^2.0.0", "ora": "^1.4.0", - "prettier": "^1.18.2", + "prettier": "^1.19.1", "read-pkg": "^5.2.0", "rxjs": "^6.5.3", "spawn-sync": "^1.0.15", diff --git a/packages/kbn-pm/src/commands/bootstrap.test.ts b/packages/kbn-pm/src/commands/bootstrap.test.ts index b6d9a540ac940d..b36246d97c1add 100644 --- a/packages/kbn-pm/src/commands/bootstrap.test.ts +++ b/packages/kbn-pm/src/commands/bootstrap.test.ts @@ -101,7 +101,12 @@ test('handles dependencies of dependencies', async () => { 'packages/baz' ); - const projects = new Map([['kibana', kibana], ['foo', foo], ['bar', bar], ['baz', baz]]); + const projects = new Map([ + ['kibana', kibana], + ['foo', foo], + ['bar', bar], + ['baz', baz], + ]); const projectGraph = buildProjectGraph(projects); const logMock = jest.spyOn(console, 'log').mockImplementation(noop); @@ -133,7 +138,10 @@ test('does not run installer if no deps in package', async () => { 'packages/bar' ); - const projects = new Map([['kibana', kibana], ['bar', bar]]); + const projects = new Map([ + ['kibana', kibana], + ['bar', bar], + ]); const projectGraph = buildProjectGraph(projects); const logMock = jest.spyOn(console, 'log').mockImplementation(noop); @@ -193,7 +201,10 @@ test('calls "kbn:bootstrap" scripts and links executables after installing deps' 'packages/bar' ); - const projects = new Map([['kibana', kibana], ['bar', bar]]); + const projects = new Map([ + ['kibana', kibana], + ['bar', bar], + ]); const projectGraph = buildProjectGraph(projects); jest.spyOn(console, 'log').mockImplementation(noop); diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index 2e5f897894a900..a6b3e8f96f7dba 100644 --- a/packages/kbn-spec-to-console/package.json +++ b/packages/kbn-spec-to-console/package.json @@ -18,7 +18,7 @@ "homepage": "https://github.com/jbudz/spec-to-console#readme", "devDependencies": { "jest": "^24.9.0", - "prettier": "^1.18.2" + "prettier": "^1.19.1" }, "dependencies": { "commander": "^3.0.0", diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 472545b203a9b0..9f4e678c6adf5a 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -23,4 +23,5 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/api_integration/config.js'), require.resolve('../test/plugin_functional/config.js'), require.resolve('../test/interpreter_functional/config.js'), + require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), ]); diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index c26a383719fa1a..366a5b65fbb99f 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1130,7 +1130,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; | ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `import 'ui/apply_filters'` | `import { ApplyFiltersPopover } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives | -| `import 'ui/query_bar'` | `import { QueryBar, QueryBarInput } from '../data/public'` | Directives are deprecated. | +| `import 'ui/query_bar'` | `import { QueryBarInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | | `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 5b1d4affe88405..5be22ea151c323 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -80,6 +80,12 @@ export interface App extends AppBase { * @returns An unmounting function that will be called to unmount the application. */ mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; + + /** + * Hide the UI chrome when the application is mounted. Defaults to `false`. + * Takes precedence over chrome service visibility settings. + */ + chromeless?: boolean; } /** @internal */ @@ -145,12 +151,13 @@ export interface AppMountParameters { * export class MyPlugin implements Plugin { * setup({ application }) { * application.register({ - * id: 'my-app', - * async mount(context, params) { - * const { renderApp } = await import('./application'); - * return renderApp(context, params); - * }, - * }); + * id: 'my-app', + * async mount(context, params) { + * const { renderApp } = await import('./application'); + * return renderApp(context, params); + * }, + * }); + * } * } * ``` * diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 45e94040eeb4a3..3390480e56bdd4 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -26,351 +26,423 @@ import { applicationServiceMock } from '../application/application_service.mock' import { httpServiceMock } from '../http/http_service.mock'; import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; -import { ChromeService } from './chrome_service'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; +import { ChromeService } from './chrome_service'; +import { App } from '../application'; +class FakeApp implements App { + public title = `${this.id} App`; + public mount = () => () => {}; + constructor(public id: string, public chromeless?: boolean) {} +} const store = new Map(); +const originalLocalStorage = window.localStorage; + (window as any).localStorage = { setItem: (key: string, value: string) => store.set(String(key), String(value)), getItem: (key: string) => store.get(String(key)), removeItem: (key: string) => store.delete(String(key)), }; -function defaultStartDeps() { - return { +function defaultStartDeps(availableApps?: App[]) { + const deps = { application: applicationServiceMock.createInternalStartContract(), docLinks: docLinksServiceMock.createStartContract(), http: httpServiceMock.createStartContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), }; + + if (availableApps) { + deps.application.availableApps = new Map(availableApps.map(app => [app.id, app])); + } + + return deps; +} + +async function start({ + options = { browserSupportsCsp: true }, + cspConfigMock = { warnLegacyBrowsers: true }, + startDeps = defaultStartDeps(), +}: { options?: any; cspConfigMock?: any; startDeps?: ReturnType } = {}) { + const service = new ChromeService(options); + + if (cspConfigMock) { + startDeps.injectedMetadata.getCspConfig.mockReturnValue(cspConfigMock); + } + + return { + service, + startDeps, + chrome: await service.start(startDeps), + }; } beforeEach(() => { store.clear(); + window.history.pushState(undefined, '', '#/home?a=b'); +}); + +afterAll(() => { + (window as any).localStorage = originalLocalStorage; }); describe('start', () => { it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => { - const service = new ChromeService({ browserSupportsCsp: false }); - const startDeps = defaultStartDeps(); - startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); - await service.start(startDeps); + const { startDeps } = await start({ options: { browserSupportsCsp: false } }); + expect(startDeps.notifications.toasts.addWarning.mock.calls).toMatchInlineSnapshot(` -Array [ - Array [ - "Your browser does not meet the security requirements for Kibana.", - ], -] -`); + Array [ + Array [ + "Your browser does not meet the security requirements for Kibana.", + ], + ] + `); }); it('does not add legacy browser warning if browser supports CSP', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const startDeps = defaultStartDeps(); - startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); - await service.start(startDeps); + const { startDeps } = await start(); + expect(startDeps.notifications.toasts.addWarning).not.toBeCalled(); }); it('does not add legacy browser warning if warnLegacyBrowsers is disabled', async () => { - const service = new ChromeService({ browserSupportsCsp: false }); - const startDeps = defaultStartDeps(); - startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: false }); - await service.start(startDeps); + const { startDeps } = await start({ + options: { browserSupportsCsp: false }, + cspConfigMock: { warnLegacyBrowsers: false }, + }); + expect(startDeps.notifications.toasts.addWarning).not.toBeCalled(); }); describe('getComponent', () => { it('returns a renderable React component', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); + const { chrome } = await start(); + // Have to do some fanagling to get the type system and enzyme to accept this. // Don't capture the snapshot because it's 600+ lines long. - expect(shallow(React.createElement(() => start.getHeaderComponent()))).toBeDefined(); + expect(shallow(React.createElement(() => chrome.getHeaderComponent()))).toBeDefined(); }); }); describe('brand', () => { it('updates/emits the brand as it changes', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); - const promise = start + const { chrome, service } = await start(); + const promise = chrome .getBrand$() .pipe(toArray()) .toPromise(); - start.setBrand({ + chrome.setBrand({ logo: 'big logo', smallLogo: 'not so big logo', }); - start.setBrand({ + chrome.setBrand({ logo: 'big logo without small logo', }); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` -Array [ - Object {}, - Object { - "logo": "big logo", - "smallLogo": "not so big logo", - }, - Object { - "logo": "big logo without small logo", - "smallLogo": undefined, - }, -] -`); + Array [ + Object {}, + Object { + "logo": "big logo", + "smallLogo": "not so big logo", + }, + Object { + "logo": "big logo without small logo", + "smallLogo": undefined, + }, + ] + `); }); }); describe('visibility', () => { it('updates/emits the visibility', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); - const promise = start + const { chrome, service } = await start(); + const promise = chrome .getIsVisible$() .pipe(toArray()) .toPromise(); - start.setIsVisible(true); - start.setIsVisible(false); - start.setIsVisible(true); + chrome.setIsVisible(true); + chrome.setIsVisible(false); + chrome.setIsVisible(true); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` -Array [ - true, - true, - false, - true, -] -`); + Array [ + true, + true, + false, + true, + ] + `); }); - it('always emits false if embed query string is in hash when set up', async () => { + it('always emits false if embed query string is preset when set up', async () => { window.history.pushState(undefined, '', '#/home?a=b&embed=true'); - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); - const promise = start + const { chrome, service } = await start(); + const promise = chrome + .getIsVisible$() + .pipe(toArray()) + .toPromise(); + + chrome.setIsVisible(true); + chrome.setIsVisible(false); + chrome.setIsVisible(true); + service.stop(); + + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + false, + false, + false, + false, + ] + `); + }); + + it('application-specified visibility on mount', async () => { + const startDeps = defaultStartDeps([ + new FakeApp('alpha'), // An undefined `chromeless` is the same as setting to false. + new FakeApp('beta', true), + new FakeApp('gamma', false), + ]); + const { availableApps, currentAppId$ } = startDeps.application; + const { chrome, service } = await start({ startDeps }); + const promise = chrome + .getIsVisible$() + .pipe(toArray()) + .toPromise(); + + [...availableApps.keys()].forEach(appId => currentAppId$.next(appId)); + service.stop(); + + await expect(promise).resolves.toMatchInlineSnapshot(` + Array [ + true, + true, + false, + true, + ] + `); + }); + + it('changing visibility has no effect on chrome-hiding application', async () => { + const startDeps = defaultStartDeps([new FakeApp('alpha', true)]); + const { currentAppId$ } = startDeps.application; + const { chrome, service } = await start({ startDeps }); + const promise = chrome .getIsVisible$() .pipe(toArray()) .toPromise(); - start.setIsVisible(true); - start.setIsVisible(false); - start.setIsVisible(true); + currentAppId$.next('alpha'); + chrome.setIsVisible(true); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` -Array [ - false, - false, - false, - false, -] -`); + Array [ + true, + false, + false, + ] + `); }); }); describe('is collapsed', () => { it('updates/emits isCollapsed', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); - const promise = start + const { chrome, service } = await start(); + const promise = chrome .getIsCollapsed$() .pipe(toArray()) .toPromise(); - start.setIsCollapsed(true); - start.setIsCollapsed(false); - start.setIsCollapsed(true); + chrome.setIsCollapsed(true); + chrome.setIsCollapsed(false); + chrome.setIsCollapsed(true); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` -Array [ - false, - true, - false, - true, -] -`); + Array [ + false, + true, + false, + true, + ] + `); }); it('only stores true in localStorage', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); + const { chrome } = await start(); - start.setIsCollapsed(true); + chrome.setIsCollapsed(true); expect(store.size).toBe(1); - start.setIsCollapsed(false); + chrome.setIsCollapsed(false); expect(store.size).toBe(0); }); }); describe('application classes', () => { it('updates/emits the application classes', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); - const promise = start + const { chrome, service } = await start(); + const promise = chrome .getApplicationClasses$() .pipe(toArray()) .toPromise(); - start.addApplicationClass('foo'); - start.addApplicationClass('foo'); - start.addApplicationClass('bar'); - start.addApplicationClass('bar'); - start.addApplicationClass('baz'); - start.removeApplicationClass('bar'); - start.removeApplicationClass('foo'); + chrome.addApplicationClass('foo'); + chrome.addApplicationClass('foo'); + chrome.addApplicationClass('bar'); + chrome.addApplicationClass('bar'); + chrome.addApplicationClass('baz'); + chrome.removeApplicationClass('bar'); + chrome.removeApplicationClass('foo'); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` -Array [ - Array [], - Array [ - "foo", - ], - Array [ - "foo", - ], - Array [ - "foo", - "bar", - ], - Array [ - "foo", - "bar", - ], - Array [ - "foo", - "bar", - "baz", - ], - Array [ - "foo", - "baz", - ], - Array [ - "baz", - ], -] -`); + Array [ + Array [], + Array [ + "foo", + ], + Array [ + "foo", + ], + Array [ + "foo", + "bar", + ], + Array [ + "foo", + "bar", + ], + Array [ + "foo", + "bar", + "baz", + ], + Array [ + "foo", + "baz", + ], + Array [ + "baz", + ], + ] + `); }); }); describe('badge', () => { it('updates/emits the current badge', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); - const promise = start + const { chrome, service } = await start(); + const promise = chrome .getBadge$() .pipe(toArray()) .toPromise(); - start.setBadge({ text: 'foo', tooltip: `foo's tooltip` }); - start.setBadge({ text: 'bar', tooltip: `bar's tooltip` }); - start.setBadge(undefined); + chrome.setBadge({ text: 'foo', tooltip: `foo's tooltip` }); + chrome.setBadge({ text: 'bar', tooltip: `bar's tooltip` }); + chrome.setBadge(undefined); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` -Array [ - undefined, - Object { - "text": "foo", - "tooltip": "foo's tooltip", - }, - Object { - "text": "bar", - "tooltip": "bar's tooltip", - }, - undefined, -] -`); + Array [ + undefined, + Object { + "text": "foo", + "tooltip": "foo's tooltip", + }, + Object { + "text": "bar", + "tooltip": "bar's tooltip", + }, + undefined, + ] + `); }); }); describe('breadcrumbs', () => { it('updates/emits the current set of breadcrumbs', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); - const promise = start + const { chrome, service } = await start(); + const promise = chrome .getBreadcrumbs$() .pipe(toArray()) .toPromise(); - start.setBreadcrumbs([{ text: 'foo' }, { text: 'bar' }]); - start.setBreadcrumbs([{ text: 'foo' }]); - start.setBreadcrumbs([{ text: 'bar' }]); - start.setBreadcrumbs([]); + chrome.setBreadcrumbs([{ text: 'foo' }, { text: 'bar' }]); + chrome.setBreadcrumbs([{ text: 'foo' }]); + chrome.setBreadcrumbs([{ text: 'bar' }]); + chrome.setBreadcrumbs([]); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` -Array [ - Array [], - Array [ - Object { - "text": "foo", - }, - Object { - "text": "bar", - }, - ], - Array [ - Object { - "text": "foo", - }, - ], - Array [ - Object { - "text": "bar", - }, - ], - Array [], -] -`); + Array [ + Array [], + Array [ + Object { + "text": "foo", + }, + Object { + "text": "bar", + }, + ], + Array [ + Object { + "text": "foo", + }, + ], + Array [ + Object { + "text": "bar", + }, + ], + Array [], + ] + `); }); }); describe('help extension', () => { it('updates/emits the current help extension', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); - const promise = start + const { chrome, service } = await start(); + const promise = chrome .getHelpExtension$() .pipe(toArray()) .toPromise(); - start.setHelpExtension(() => () => undefined); - start.setHelpExtension(undefined); + chrome.setHelpExtension(() => () => undefined); + chrome.setHelpExtension(undefined); service.stop(); await expect(promise).resolves.toMatchInlineSnapshot(` -Array [ - undefined, - [Function], - undefined, -] -`); + Array [ + undefined, + [Function], + undefined, + ] + `); }); }); }); describe('stop', () => { it('completes applicationClass$, isCollapsed$, breadcrumbs$, isVisible$, and brand$ observables', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); + const { chrome, service } = await start(); const promise = Rx.combineLatest( - start.getBrand$(), - start.getApplicationClasses$(), - start.getIsCollapsed$(), - start.getBreadcrumbs$(), - start.getIsVisible$(), - start.getHelpExtension$() + chrome.getBrand$(), + chrome.getApplicationClasses$(), + chrome.getIsCollapsed$(), + chrome.getBreadcrumbs$(), + chrome.getIsVisible$(), + chrome.getHelpExtension$() ).toPromise(); service.stop(); @@ -378,18 +450,17 @@ describe('stop', () => { }); it('completes immediately if service already stopped', async () => { - const service = new ChromeService({ browserSupportsCsp: true }); - const start = await service.start(defaultStartDeps()); + const { chrome, service } = await start(); service.stop(); await expect( Rx.combineLatest( - start.getBrand$(), - start.getApplicationClasses$(), - start.getIsCollapsed$(), - start.getBreadcrumbs$(), - start.getIsVisible$(), - start.getHelpExtension$() + chrome.getBrand$(), + chrome.getApplicationClasses$(), + chrome.getIsCollapsed$(), + chrome.getBreadcrumbs$(), + chrome.getIsVisible$(), + chrome.getHelpExtension$() ).toPromise() ).resolves.toBe(undefined); }); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index a5532faec19ede..e686f03413dd52 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -18,9 +18,9 @@ */ import React from 'react'; -import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs'; +import { BehaviorSubject, Observable, ReplaySubject, combineLatest, of, merge } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; -import * as Url from 'url'; +import { parse } from 'url'; import { i18n } from '@kbn/i18n'; import { IconType, Breadcrumb as EuiBreadcrumb } from '@elastic/eui'; @@ -41,11 +41,6 @@ export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle }; const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed'; -function isEmbedParamInHash() { - const { query } = Url.parse(String(window.location.hash).slice(1), true); - return Boolean(query.embed); -} - /** @public */ export interface ChromeBadge { text: string; @@ -79,6 +74,9 @@ interface StartDeps { /** @internal */ export class ChromeService { + private isVisible$!: Observable; + private appHidden$!: Observable; + private toggleHidden$!: BehaviorSubject; private readonly stop$ = new ReplaySubject(1); private readonly navControls = new NavControlsService(); private readonly navLinks = new NavLinksService(); @@ -87,6 +85,38 @@ export class ChromeService { constructor(private readonly params: ConstructorParams) {} + /** + * These observables allow consumers to toggle the chrome visibility via either: + * 1. Using setIsVisible() to trigger the next chromeHidden$ + * 2. Setting `chromeless` when registering an application, which will + * reset the visibility whenever the next application is mounted + * 3. Having "embed" in the query string + */ + private initVisibility(application: StartDeps['application']) { + // Start off the chrome service hidden if "embed" is in the hash query string. + const isEmbedded = 'embed' in parse(location.hash.slice(1), true).query; + + this.toggleHidden$ = new BehaviorSubject(isEmbedded); + this.appHidden$ = merge( + // Default the app being hidden to the same value initial value as the chrome visibility + // in case the application service has not emitted an app ID yet, since we want to trigger + // combineLatest below regardless of having an application value yet. + of(isEmbedded), + application.currentAppId$.pipe( + map( + appId => + !!appId && + application.availableApps.has(appId) && + !!application.availableApps.get(appId)!.chromeless + ) + ) + ); + this.isVisible$ = combineLatest(this.appHidden$, this.toggleHidden$).pipe( + map(([appHidden, chromeHidden]) => !(appHidden || chromeHidden)), + takeUntil(this.stop$) + ); + } + public async start({ application, docLinks, @@ -94,11 +124,10 @@ export class ChromeService { injectedMetadata, notifications, }: StartDeps): Promise { - const FORCE_HIDDEN = isEmbedParamInHash(); + this.initVisibility(application); const appTitle$ = new BehaviorSubject('Kibana'); const brand$ = new BehaviorSubject({}); - const isVisible$ = new BehaviorSubject(true); const isCollapsed$ = new BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY)); const applicationClasses$ = new BehaviorSubject>(new Set()); const helpExtension$ = new BehaviorSubject(undefined); @@ -139,10 +168,7 @@ export class ChromeService { forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()} helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))} homeHref={http.basePath.prepend('/app/kibana#/home')} - isVisible$={isVisible$.pipe( - map(visibility => (FORCE_HIDDEN ? false : visibility)), - takeUntil(this.stop$) - )} + isVisible$={this.isVisible$} kibanaVersion={injectedMetadata.getKibanaVersion()} legacyMode={injectedMetadata.getLegacyMode()} navLinks$={navLinks.getNavLinks$()} @@ -166,15 +192,9 @@ export class ChromeService { ); }, - getIsVisible$: () => - isVisible$.pipe( - map(visibility => (FORCE_HIDDEN ? false : visibility)), - takeUntil(this.stop$) - ), + getIsVisible$: () => this.isVisible$, - setIsVisible: (visibility: boolean) => { - isVisible$.next(visibility); - }, + setIsVisible: (isVisible: boolean) => this.toggleHidden$.next(!isVisible), getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)), diff --git a/src/core/public/chrome/constants.ts b/src/core/public/chrome/constants.ts index 3411f6f629a13d..c8e53b38c618eb 100644 --- a/src/core/public/chrome/constants.ts +++ b/src/core/public/chrome/constants.ts @@ -18,6 +18,6 @@ */ export const ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/'; -export const KIBANA_FEEDBACK_LINK = 'https://www.elastic.co/kibana/feedback'; -export const KIBANA_ASK_ELASTIC_LINK = 'https://www.elastic.co/kibana/ask-elastic'; +export const KIBANA_FEEDBACK_LINK = 'https://www.elastic.co/products/kibana/feedback'; +export const KIBANA_ASK_ELASTIC_LINK = 'https://www.elastic.co/products/kibana/ask-elastic'; export const GITHUB_CREATE_ISSUE_LINK = 'https://github.com/elastic/kibana/issues/new/choose'; diff --git a/src/core/public/chrome/nav_links/nav_links_service.test.ts b/src/core/public/chrome/nav_links/nav_links_service.test.ts index 8c135b3c4c49f0..5a45491df28e7b 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.test.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.test.ts @@ -19,27 +19,34 @@ import { NavLinksService } from './nav_links_service'; import { take, map, takeLast } from 'rxjs/operators'; -import { LegacyApp } from '../../application'; +import { App, LegacyApp } from '../../application'; const mockAppService = { - availableApps: new Map(), - availableLegacyApps: new Map([ - [ - 'legacyApp1', - { id: 'legacyApp1', order: 0, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' }, - ], - [ - 'legacyApp2', + availableApps: new Map( + ([ + { id: 'app1', order: 0, title: 'App 1', icon: 'app1' }, { - id: 'legacyApp2', + id: 'app2', order: -10, + title: 'App 2', + euiIconType: 'canvasApp', + }, + { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true }, + ] as App[]).map(app => [app.id, app]) + ), + availableLegacyApps: new Map( + ([ + { id: 'legacyApp1', order: 5, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' }, + { + id: 'legacyApp2', + order: -5, title: 'Legacy App 2', euiIconType: 'canvasApp', appUrl: '/app2', }, - ], - ['legacyApp3', { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3' }], - ]), + { id: 'legacyApp3', order: 15, title: 'Legacy App 3', appUrl: '/app3' }, + ] as LegacyApp[]).map(app => [app.id, app]) + ), } as any; const mockHttp = { @@ -58,6 +65,18 @@ describe('NavLinksService', () => { }); describe('#getNavLinks$()', () => { + it('does not include `chromeless` applications', async () => { + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map(links => links.map(l => l.id)) + ) + .toPromise() + ).not.toContain('chromelessApp'); + }); + it('sorts navlinks by `order` property', async () => { expect( await start @@ -67,7 +86,7 @@ describe('NavLinksService', () => { map(links => links.map(l => l.id)) ) .toPromise() - ).toEqual(['legacyApp2', 'legacyApp1', 'legacyApp3']); + ).toEqual(['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3']); }); it('emits multiple values', async () => { @@ -78,8 +97,8 @@ describe('NavLinksService', () => { service.stop(); expect(emittedLinks).toEqual([ - ['legacyApp2', 'legacyApp1', 'legacyApp3'], - ['legacyApp2', 'legacyApp1', 'legacyApp3'], + ['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3'], + ['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3'], ]); }); @@ -105,7 +124,13 @@ describe('NavLinksService', () => { describe('#getAll()', () => { it('returns a sorted array of navlinks', () => { - expect(start.getAll().map(l => l.id)).toEqual(['legacyApp2', 'legacyApp1', 'legacyApp3']); + expect(start.getAll().map(l => l.id)).toEqual([ + 'app2', + 'legacyApp2', + 'app1', + 'legacyApp1', + 'legacyApp3', + ]); }); }); @@ -130,7 +155,20 @@ describe('NavLinksService', () => { map(links => links.map(l => l.id)) ) .toPromise() - ).toEqual(['legacyApp2', 'legacyApp1', 'legacyApp3']); + ).toEqual(['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3']); + }); + + it('does nothing on chromeless applications', async () => { + start.showOnly('chromelessApp'); + expect( + await start + .getNavLinks$() + .pipe( + take(1), + map(links => links.map(l => l.id)) + ) + .toPromise() + ).toEqual(['app2', 'legacyApp2', 'app1', 'legacyApp1', 'legacyApp3']); }); it('removes all other links', async () => { @@ -157,7 +195,7 @@ describe('NavLinksService', () => { "icon": "legacyApp1", "id": "legacyApp1", "legacy": true, - "order": 0, + "order": 5, "title": "Legacy App 1", } `); diff --git a/src/core/public/chrome/nav_links/nav_links_service.ts b/src/core/public/chrome/nav_links/nav_links_service.ts index affc639faf0b85..31a729f90cd932 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -99,17 +99,19 @@ export class NavLinksService { private readonly stop$ = new ReplaySubject(1); public start({ application, http }: StartDeps): ChromeNavLinks { - const appLinks = [...application.availableApps].map( - ([appId, app]) => - [ - appId, - new NavLinkWrapper({ - ...app, - legacy: false, - baseUrl: relativeToAbsolute(http.basePath.prepend(`/app/${appId}`)), - }), - ] as [string, NavLinkWrapper] - ); + const appLinks = [...application.availableApps] + .filter(([, app]) => !app.chromeless) + .map( + ([appId, app]) => + [ + appId, + new NavLinkWrapper({ + ...app, + legacy: false, + baseUrl: relativeToAbsolute(http.basePath.prepend(`/app/${appId}`)), + }), + ] as [string, NavLinkWrapper] + ); const legacyAppLinks = [...application.availableLegacyApps].map( ([appId, app]) => @@ -130,10 +132,7 @@ export class NavLinksService { return { getNavLinks$: () => { - return navLinks$.pipe( - map(sortNavLinks), - takeUntil(this.stop$) - ); + return navLinks$.pipe(map(sortNavLinks), takeUntil(this.stop$)); }, get(id: string) { @@ -192,7 +191,10 @@ export class NavLinksService { } function sortNavLinks(navLinks: ReadonlyMap) { - return sortBy([...navLinks.values()].map(link => link.properties), 'order'); + return sortBy( + [...navLinks.values()].map(link => link.properties), + 'order' + ); } function relativeToAbsolute(url: string) { diff --git a/src/core/public/chrome/recently_accessed/recently_accessed_service.test.ts b/src/core/public/chrome/recently_accessed/recently_accessed_service.test.ts index cca16ddcd2a813..3c9713a93144a9 100644 --- a/src/core/public/chrome/recently_accessed/recently_accessed_service.test.ts +++ b/src/core/public/chrome/recently_accessed/recently_accessed_service.test.ts @@ -106,10 +106,7 @@ describe('RecentlyAccessed#start()', () => { const stop$ = new Subject(); const observedValues$ = recentlyAccessed .get$() - .pipe( - bufferCount(3), - takeUntil(stop$) - ) + .pipe(bufferCount(3), takeUntil(stop$)) .toPromise(); recentlyAccessed.add('/app/item1', 'Item 1', 'item1'); recentlyAccessed.add('/app/item2', 'Item 2', 'item2'); diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index d78504a899a345..1ee41fe64418ec 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -174,7 +174,10 @@ describe('#setup()', () => { it('injects legacy dependency to context#setup()', async () => { const pluginA = Symbol(); const pluginB = Symbol(); - const pluginDependencies = new Map([[pluginA, []], [pluginB, [pluginA]]]); + const pluginDependencies = new Map([ + [pluginA, []], + [pluginB, [pluginA]], + ]); MockPluginsService.getOpaqueIds.mockReturnValue(pluginDependencies); await setupCore(); diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 7391cf7f9454cc..e040b29814900a 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -117,13 +117,7 @@ export { InterceptedHttpResponse, } from './http'; -export { - OverlayStart, - OverlayBannerMount, - OverlayBannerUnmount, - OverlayBannersStart, - OverlayRef, -} from './overlays'; +export { OverlayStart, OverlayBannersStart, OverlayRef } from './overlays'; export { Toast, @@ -136,6 +130,8 @@ export { ErrorToastOptions, } from './notifications'; +export { MountPoint, UnmountCallback } from './types'; + /** * Core services exposed to the `Plugin` setup lifecycle * diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts index ef35fd2aa78ac3..1110097c1c92bd 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -68,18 +68,27 @@ describe('setup.getPlugins()', () => { it('returns injectedMetadata.uiPlugins', () => { const injectedMetadata = new InjectedMetadataService({ injectedMetadata: { - uiPlugins: [{ id: 'plugin-1', plugin: {} }, { id: 'plugin-2', plugin: {} }], + uiPlugins: [ + { id: 'plugin-1', plugin: {} }, + { id: 'plugin-2', plugin: {} }, + ], }, } as any); const plugins = injectedMetadata.setup().getPlugins(); - expect(plugins).toEqual([{ id: 'plugin-1', plugin: {} }, { id: 'plugin-2', plugin: {} }]); + expect(plugins).toEqual([ + { id: 'plugin-1', plugin: {} }, + { id: 'plugin-2', plugin: {} }, + ]); }); it('returns frozen version of uiPlugins', () => { const injectedMetadata = new InjectedMetadataService({ injectedMetadata: { - uiPlugins: [{ id: 'plugin-1', plugin: {} }, { id: 'plugin-2', plugin: {} }], + uiPlugins: [ + { id: 'plugin-1', plugin: {} }, + { id: 'plugin-2', plugin: {} }, + ], }, } as any); diff --git a/src/core/public/notifications/toasts/__snapshots__/global_toast_list.test.tsx.snap b/src/core/public/notifications/toasts/__snapshots__/global_toast_list.test.tsx.snap index 29b289592b2ef5..ca09d4a14bd7a6 100644 --- a/src/core/public/notifications/toasts/__snapshots__/global_toast_list.test.tsx.snap +++ b/src/core/public/notifications/toasts/__snapshots__/global_toast_list.test.tsx.snap @@ -3,7 +3,7 @@ exports[`renders matching snapshot 1`] = ` diff --git a/src/core/public/notifications/toasts/global_toast_list.test.tsx b/src/core/public/notifications/toasts/global_toast_list.test.tsx index c6c127acbb0336..61d73ac2331886 100644 --- a/src/core/public/notifications/toasts/global_toast_list.test.tsx +++ b/src/core/public/notifications/toasts/global_toast_list.test.tsx @@ -57,9 +57,9 @@ it('subscribes to toasts$ on mount and unsubscribes on unmount', () => { it('passes latest value from toasts$ to ', () => { const el = shallow( render({ - toasts$: Rx.from([[], [1], [1, 2]]) as any, + toasts$: Rx.from([[], [{ id: 1 }], [{ id: 1 }, { id: 2 }]]) as any, }) ); - expect(el.find(EuiGlobalToastList).prop('toasts')).toEqual([1, 2]); + expect(el.find(EuiGlobalToastList).prop('toasts')).toEqual([{ id: 1 }, { id: 2 }]); }); diff --git a/src/core/public/notifications/toasts/global_toast_list.tsx b/src/core/public/notifications/toasts/global_toast_list.tsx index 57dc899016264f..f96a0a6f362bf8 100644 --- a/src/core/public/notifications/toasts/global_toast_list.tsx +++ b/src/core/public/notifications/toasts/global_toast_list.tsx @@ -17,20 +17,28 @@ * under the License. */ -import { EuiGlobalToastList, EuiGlobalToastListToast as Toast } from '@elastic/eui'; - +import { EuiGlobalToastList, EuiGlobalToastListToast as EuiToast } from '@elastic/eui'; import React from 'react'; import * as Rx from 'rxjs'; +import { MountWrapper } from '../../utils'; +import { Toast } from './toasts_api'; + interface Props { toasts$: Rx.Observable; - dismissToast: (t: Toast) => void; + dismissToast: (toastId: string) => void; } interface State { toasts: Toast[]; } +const convertToEui = (toast: Toast): EuiToast => ({ + ...toast, + title: typeof toast.title === 'function' ? : toast.title, + text: typeof toast.text === 'function' ? : toast.text, +}); + export class GlobalToastList extends React.Component { public state: State = { toasts: [], @@ -54,8 +62,8 @@ export class GlobalToastList extends React.Component { return ( this.props.dismissToast(id)} /** * This prop is overriden by the individual toasts that are added. * Use `Infinity` here so that it's obvious a timeout hasn't been diff --git a/src/core/public/notifications/toasts/index.ts b/src/core/public/notifications/toasts/index.ts index 83c2d52f3d77a6..6e9de116833646 100644 --- a/src/core/public/notifications/toasts/index.ts +++ b/src/core/public/notifications/toasts/index.ts @@ -18,5 +18,11 @@ */ export { ToastsService, ToastsSetup, ToastsStart } from './toasts_service'; -export { ErrorToastOptions, ToastsApi, ToastInput, IToasts, ToastInputFields } from './toasts_api'; -export { EuiGlobalToastListToast as Toast } from '@elastic/eui'; +export { + ErrorToastOptions, + ToastsApi, + ToastInput, + IToasts, + ToastInputFields, + Toast, +} from './toasts_api'; diff --git a/src/core/public/notifications/toasts/toasts_api.test.ts b/src/core/public/notifications/toasts/toasts_api.test.ts index 38e6d2a2229900..f99a28617aa5c8 100644 --- a/src/core/public/notifications/toasts/toasts_api.test.ts +++ b/src/core/public/notifications/toasts/toasts_api.test.ts @@ -91,7 +91,7 @@ describe('#get$()', () => { toasts.add('foo'); onToasts.mockClear(); - toasts.remove({ id: 'bar' }); + toasts.remove('bar'); expect(onToasts).not.toHaveBeenCalled(); }); }); @@ -136,7 +136,7 @@ describe('#remove()', () => { it('ignores unknown toast', async () => { const toasts = new ToastsApi(toastDeps()); toasts.add('Test'); - toasts.remove({ id: 'foo' }); + toasts.remove('foo'); const currentToasts = await getCurrentToasts(toasts); expect(currentToasts).toHaveLength(1); diff --git a/src/core/public/notifications/toasts/toasts_api.tsx b/src/core/public/notifications/toasts/toasts_api.tsx index 24514cb11548b5..b49bafda5b26e6 100644 --- a/src/core/public/notifications/toasts/toasts_api.tsx +++ b/src/core/public/notifications/toasts/toasts_api.tsx @@ -17,11 +17,13 @@ * under the License. */ -import { EuiGlobalToastListToast as Toast } from '@elastic/eui'; +import { EuiGlobalToastListToast as EuiToast } from '@elastic/eui'; import React from 'react'; import * as Rx from 'rxjs'; import { ErrorToast } from './error_toast'; +import { MountPoint } from '../../types'; +import { mountReactNode } from '../../utils'; import { UiSettingsClientContract } from '../../ui_settings'; import { OverlayStart } from '../../overlays'; @@ -33,13 +35,20 @@ import { OverlayStart } from '../../overlays'; * * @public */ -export type ToastInputFields = Pick>; +export type ToastInputFields = Pick> & { + title?: string | MountPoint; + text?: string | MountPoint; +}; + +export type Toast = ToastInputFields & { + id: string; +}; /** * Inputs for {@link IToasts} APIs. * @public */ -export type ToastInput = string | ToastInputFields | Promise; +export type ToastInput = string | ToastInputFields; /** * Options available for {@link IToasts} APIs. @@ -59,13 +68,12 @@ export interface ErrorToastOptions { toastMessage?: string; } -const normalizeToast = (toastOrTitle: ToastInput) => { +const normalizeToast = (toastOrTitle: ToastInput): ToastInputFields => { if (typeof toastOrTitle === 'string') { return { title: toastOrTitle, }; } - return toastOrTitle; }; @@ -123,11 +131,12 @@ export class ToastsApi implements IToasts { /** * Removes a toast from the current array of toasts if present. - * @param toast - a {@link Toast} returned by {@link ToastApi.add} + * @param toastOrId - a {@link Toast} returned by {@link ToastsApi.add} or its id */ - public remove(toast: Toast) { + public remove(toastOrId: Toast | string) { + const toRemove = typeof toastOrId === 'string' ? toastOrId : toastOrId.id; const list = this.toasts$.getValue(); - const listWithoutToast = list.filter(t => t !== toast); + const listWithoutToast = list.filter(t => t.id !== toRemove); if (listWithoutToast.length !== list.length) { this.toasts$.next(listWithoutToast); } @@ -191,7 +200,7 @@ export class ToastsApi implements IToasts { iconType: 'alert', title: options.title, toastLifeTimeMs: this.uiSettings.get('notifications:lifetime:error'), - text: ( + text: mountReactNode( this.api!.remove(toast)} + dismissToast={(toastId: string) => this.api!.remove(toastId)} toasts$={this.api!.get$()} /> , diff --git a/src/core/public/overlays/banners/banners_service.tsx b/src/core/public/overlays/banners/banners_service.tsx index 799ca43c7fa93b..31d49b5952e879 100644 --- a/src/core/public/overlays/banners/banners_service.tsx +++ b/src/core/public/overlays/banners/banners_service.tsx @@ -25,33 +25,20 @@ import { PriorityMap } from './priority_map'; import { BannersList } from './banners_list'; import { UiSettingsClientContract } from '../../ui_settings'; import { I18nStart } from '../../i18n'; +import { MountPoint } from '../../types'; import { UserBannerService } from './user_banner_service'; -/** - * A function that will unmount the banner from the element. - * @public - */ -export type OverlayBannerUnmount = () => void; - -/** - * A function that will mount the banner inside the provided element. - * @param element an element to render into - * @returns a {@link OverlayBannerUnmount} - * @public - */ -export type OverlayBannerMount = (element: HTMLElement) => OverlayBannerUnmount; - /** @public */ export interface OverlayBannersStart { /** * Add a new banner * - * @param mount {@link OverlayBannerMount} + * @param mount {@link MountPoint} * @param priority optional priority order to display this banner. Higher priority values are shown first. * @returns a unique identifier for the given banner to be used with {@link OverlayBannersStart.remove} and * {@link OverlayBannersStart.replace} */ - add(mount: OverlayBannerMount, priority?: number): string; + add(mount: MountPoint, priority?: number): string; /** * Remove a banner @@ -65,12 +52,12 @@ export interface OverlayBannersStart { * Replace a banner in place * * @param id the unique identifier for the banner returned by {@link OverlayBannersStart.add} - * @param mount {@link OverlayBannerMount} + * @param mount {@link MountPoint} * @param priority optional priority order to display this banner. Higher priority values are shown first. * @returns a new identifier for the given banner to be used with {@link OverlayBannersStart.remove} and * {@link OverlayBannersStart.replace} */ - replace(id: string | undefined, mount: OverlayBannerMount, priority?: number): string; + replace(id: string | undefined, mount: MountPoint, priority?: number): string; /** @internal */ get$(): Observable; @@ -80,7 +67,7 @@ export interface OverlayBannersStart { /** @internal */ export interface OverlayBanner { readonly id: string; - readonly mount: OverlayBannerMount; + readonly mount: MountPoint; readonly priority: number; } @@ -116,7 +103,7 @@ export class OverlayBannersService { return true; }, - replace(id: string | undefined, mount: OverlayBannerMount, priority = 0) { + replace(id: string | undefined, mount: MountPoint, priority = 0) { if (!id || !banners$.value.has(id)) { return this.add(mount, priority); } diff --git a/src/core/public/overlays/banners/index.ts b/src/core/public/overlays/banners/index.ts index 9e908bd6280038..a68dfa7ebadac0 100644 --- a/src/core/public/overlays/banners/index.ts +++ b/src/core/public/overlays/banners/index.ts @@ -17,9 +17,4 @@ * under the License. */ -export { - OverlayBannerMount, - OverlayBannerUnmount, - OverlayBannersStart, - OverlayBannersService, -} from './banners_service'; +export { OverlayBannersStart, OverlayBannersService } from './banners_service'; diff --git a/src/core/public/overlays/banners/priority_map.test.ts b/src/core/public/overlays/banners/priority_map.test.ts index 13d81989417f1a..2b16682c13aadd 100644 --- a/src/core/public/overlays/banners/priority_map.test.ts +++ b/src/core/public/overlays/banners/priority_map.test.ts @@ -42,7 +42,10 @@ describe('PriorityMap', () => { map = map.add('b', { priority: 3 }); map = map.add('c', { priority: 2 }); map = map.remove('c'); - expect([...map]).toEqual([['b', { priority: 3 }], ['a', { priority: 1 }]]); + expect([...map]).toEqual([ + ['b', { priority: 3 }], + ['a', { priority: 1 }], + ]); }); it('adds duplicate priorities to end', () => { diff --git a/src/core/public/overlays/index.ts b/src/core/public/overlays/index.ts index c49548abee0df3..ff03e5dffb2ca8 100644 --- a/src/core/public/overlays/index.ts +++ b/src/core/public/overlays/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { OverlayBannerMount, OverlayBannerUnmount, OverlayBannersStart } from './banners'; +export { OverlayBannersStart } from './banners'; export { OverlayService, OverlayStart, OverlayRef } from './overlay_service'; diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index cfac4c36480534..0d8887774e900f 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -223,10 +223,13 @@ test('`PluginsService.setup` exposes dependent setup contracts to plugins', asyn test('`PluginsService.setup` does not set missing dependent setup contracts', async () => { plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; - mockPluginInitializers.set('pluginD', jest.fn(() => ({ - setup: jest.fn(), - start: jest.fn(), - })) as any); + mockPluginInitializers.set( + 'pluginD', + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + })) as any + ); const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); @@ -268,10 +271,13 @@ test('`PluginsService.start` exposes dependent start contracts to plugins', asyn test('`PluginsService.start` does not set missing dependent start contracts', async () => { plugins = [{ id: 'pluginD', plugin: createManifest('pluginD', { optional: ['missing'] }) }]; - mockPluginInitializers.set('pluginD', jest.fn(() => ({ - setup: jest.fn(), - start: jest.fn(), - })) as any); + mockPluginInitializers.set( + 'pluginD', + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(), + })) as any + ); const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index a596ea394abdaf..1e97d8e066d091 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -5,17 +5,18 @@ ```ts import { Breadcrumb } from '@elastic/eui'; +import { EuiGlobalToastListToast } from '@elastic/eui'; import { IconType } from '@elastic/eui'; import { Observable } from 'rxjs'; import React from 'react'; import * as Rx from 'rxjs'; import { ShallowPromise } from '@kbn/utility-types'; -import { EuiGlobalToastListToast as Toast } from '@elastic/eui'; import { UiSettingsParams as UiSettingsParams_2 } from 'src/core/server/types'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; // @public export interface App extends AppBase { + chromeless?: boolean; mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; } @@ -618,6 +619,9 @@ export interface LegacyNavLink { url: string; } +// @public +export type MountPoint = (element: HTMLElement) => UnmountCallback; + // @public (undocumented) export interface NotificationsSetup { // (undocumented) @@ -630,12 +634,9 @@ export interface NotificationsStart { toasts: ToastsStart; } -// @public -export type OverlayBannerMount = (element: HTMLElement) => OverlayBannerUnmount; - // @public (undocumented) export interface OverlayBannersStart { - add(mount: OverlayBannerMount, priority?: number): string; + add(mount: MountPoint, priority?: number): string; // Warning: (ae-forgotten-export) The symbol "OverlayBanner" needs to be exported by the entry point index.d.ts // // @internal (undocumented) @@ -643,12 +644,9 @@ export interface OverlayBannersStart { // (undocumented) getComponent(): JSX.Element; remove(id: string): boolean; - replace(id: string | undefined, mount: OverlayBannerMount, priority?: number): string; + replace(id: string | undefined, mount: MountPoint, priority?: number): string; } -// @public -export type OverlayBannerUnmount = () => void; - // @public export interface OverlayRef { close(): Promise; @@ -916,35 +914,36 @@ export class SimpleSavedObject { _version?: SavedObject['version']; } -export { Toast } +// Warning: (ae-missing-release-tag) "Toast" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Toast = ToastInputFields & { + id: string; +}; // @public -export type ToastInput = string | ToastInputFields | Promise; +export type ToastInput = string | ToastInputFields; // @public -export type ToastInputFields = Pick>; +export type ToastInputFields = Pick> & { + title?: string | MountPoint; + text?: string | MountPoint; +}; // @public export class ToastsApi implements IToasts { constructor(deps: { uiSettings: UiSettingsClientContract; }); - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported add(toastOrTitle: ToastInput): Toast; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported addDanger(toastOrTitle: ToastInput): Toast; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported addError(error: Error, options: ErrorToastOptions): Toast; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported addSuccess(toastOrTitle: ToastInput): Toast; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported addWarning(toastOrTitle: ToastInput): Toast; get$(): Rx.Observable; // @internal (undocumented) registerOverlays(overlays: OverlayStart): void; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: Reexported declarations are not supported - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ToastApi" - remove(toast: Toast): void; + remove(toastOrId: Toast | string): void; } // @public (undocumented) @@ -990,5 +989,8 @@ export interface UiSettingsState { [key: string]: UiSettingsParams_2 & UserProvidedValues_2; } +// @public +export type UnmountCallback = () => void; + ``` diff --git a/src/legacy/core_plugins/data/public/query/query_service.ts b/src/core/public/types.ts similarity index 62% rename from src/legacy/core_plugins/data/public/query/query_service.ts rename to src/core/public/types.ts index f6f229496f49dc..4b12d5bc6da51f 100644 --- a/src/legacy/core_plugins/data/public/query/query_service.ts +++ b/src/core/public/types.ts @@ -17,34 +17,21 @@ * under the License. */ -import { fromUser, toUser, getQueryLog } from './query_bar'; - /** - * Query Service + * A function that should mount DOM content inside the provided container element + * and return a handler to unmount it. + * + * @param element the container element to render into + * @returns a {@link UnmountCallback} that unmount the element on call. * - * @internal + * @public */ -export class QueryService { - public setup() { - return { - helpers: { - fromUser, - toUser, - getQueryLog, - }, - }; - } - - public start() { - // nothing to do here yet - } - - public stop() { - // nothing to do here yet - } -} +export type MountPoint = (element: HTMLElement) => UnmountCallback; -/** @public */ -export type QuerySetup = ReturnType; - -export * from './query_bar'; +/** + * A function that will unmount the element previously mounted by + * the associated {@link MountPoint} + * + * @public + */ +export type UnmountCallback = () => void; diff --git a/src/core/public/ui_settings/ui_settings_api.test.ts b/src/core/public/ui_settings/ui_settings_api.test.ts index 048ae2ccbae7f8..1170c42cea704c 100644 --- a/src/core/public/ui_settings/ui_settings_api.test.ts +++ b/src/core/public/ui_settings/ui_settings_api.test.ts @@ -183,10 +183,7 @@ describe('#getLoadingCount$()', () => { const done$ = new Rx.Subject(); const promise = uiSettingsApi .getLoadingCount$() - .pipe( - takeUntil(done$), - toArray() - ) + .pipe(takeUntil(done$), toArray()) .toPromise(); await uiSettingsApi.batchSet('foo', 'bar'); @@ -214,10 +211,7 @@ describe('#getLoadingCount$()', () => { const done$ = new Rx.Subject(); const promise = uiSettingsApi .getLoadingCount$() - .pipe( - takeUntil(done$), - toArray() - ) + .pipe(takeUntil(done$), toArray()) .toPromise(); await uiSettingsApi.batchSet('foo', 'bar'); @@ -250,7 +244,10 @@ describe('#stop', () => { uiSettingsApi.stop(); // both observables should emit the same values, and complete before the request is done loading - await expect(promise).resolves.toEqual([[0, 1], [0, 1]]); + await expect(promise).resolves.toEqual([ + [0, 1], + [0, 1], + ]); await batchSetPromise; }); }); diff --git a/src/core/public/ui_settings/ui_settings_client.test.ts b/src/core/public/ui_settings/ui_settings_client.test.ts index 8a481fe1704ddf..c58ba14d0da3e1 100644 --- a/src/core/public/ui_settings/ui_settings_client.test.ts +++ b/src/core/public/ui_settings/ui_settings_client.test.ts @@ -83,10 +83,7 @@ describe('#get$', () => { const { config } = setup(); const values = await config .get$('dateFormat') - .pipe( - take(1), - toArray() - ) + .pipe(take(1), toArray()) .toPromise(); expect(values).toEqual(['Browser']); @@ -122,10 +119,7 @@ You can use \`config.get("unknown key", defaultValue)\`, which will just return const values = await config .get$('dateFormat') - .pipe( - take(2), - toArray() - ) + .pipe(take(2), toArray()) .toPromise(); expect(values).toEqual(['Browser', 'new format']); @@ -144,10 +138,7 @@ You can use \`config.get("unknown key", defaultValue)\`, which will just return const values = await config .get$('dateFormat', 'my default') - .pipe( - take(3), - toArray() - ) + .pipe(take(3), toArray()) .toPromise(); expect(values).toEqual(['my default', 'new format', 'my default']); diff --git a/src/core/public/utils/index.ts b/src/core/public/utils/index.ts index a432094b150480..cf826eb276252d 100644 --- a/src/core/public/utils/index.ts +++ b/src/core/public/utils/index.ts @@ -19,3 +19,4 @@ export { shareWeakReplay } from './share_weak_replay'; export { Sha256 } from './crypto'; +export { MountWrapper, mountReactNode } from './mount'; diff --git a/src/core/public/utils/mount.tsx b/src/core/public/utils/mount.tsx new file mode 100644 index 00000000000000..dbd7d5da435a6a --- /dev/null +++ b/src/core/public/utils/mount.tsx @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useRef } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { MountPoint } from '../types'; + +/** + * MountWrapper is a react component to mount a {@link MountPoint} inside a react tree. + */ +export const MountWrapper: React.FunctionComponent<{ mount: MountPoint }> = ({ mount }) => { + const element = useRef(null); + useEffect(() => mount(element.current!), [mount]); + return
; +}; + +/** + * Mount converter for react components. + * + * @param component to get a mount for + */ +export const mountReactNode = (component: React.ReactNode): MountPoint => ( + element: HTMLElement +) => { + render({component}, element); + return () => unmountComponentAtNode(element); +}; diff --git a/src/core/public/utils/share_weak_replay.test.ts b/src/core/public/utils/share_weak_replay.test.ts index dcf599f6d1e10f..6eaa140e5afad5 100644 --- a/src/core/public/utils/share_weak_replay.test.ts +++ b/src/core/public/utils/share_weak_replay.test.ts @@ -153,10 +153,7 @@ Array [ }); it('resubscribes if parent completes', async () => { - const shared = counter().pipe( - take(4), - shareWeakReplay(4) - ); + const shared = counter().pipe(take(4), shareWeakReplay(4)); await expect(Promise.all([record(shared.pipe(take(1))), record(shared)])).resolves .toMatchInlineSnapshot(` @@ -199,10 +196,7 @@ Array [ it('supports parents that complete synchronously', async () => { const next = jest.fn(); const complete = jest.fn(); - const shared = counter({ async: false }).pipe( - take(3), - shareWeakReplay(1) - ); + const shared = counter({ async: false }).pipe(take(3), shareWeakReplay(1)); shared.subscribe({ next, complete }); expect(next.mock.calls).toMatchInlineSnapshot(` diff --git a/src/core/server/config/ensure_deep_object.ts b/src/core/server/config/ensure_deep_object.ts index 0b24190741b10b..58865d13c1afa4 100644 --- a/src/core/server/config/ensure_deep_object.ts +++ b/src/core/server/config/ensure_deep_object.ts @@ -34,19 +34,16 @@ export function ensureDeepObject(obj: any): any { return obj.map(item => ensureDeepObject(item)); } - return Object.keys(obj).reduce( - (fullObject, propertyKey) => { - const propertyValue = obj[propertyKey]; - if (!propertyKey.includes(separator)) { - fullObject[propertyKey] = ensureDeepObject(propertyValue); - } else { - walk(fullObject, propertyKey.split(separator), propertyValue); - } - - return fullObject; - }, - {} as any - ); + return Object.keys(obj).reduce((fullObject, propertyKey) => { + const propertyValue = obj[propertyKey]; + if (!propertyKey.includes(separator)) { + fullObject[propertyKey] = ensureDeepObject(propertyValue); + } else { + walk(fullObject, propertyKey.split(separator), propertyValue); + } + + return fullObject; + }, {} as any); } function walk(obj: any, keys: string[], value: any) { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 35e83da4ef30c8..2a5631ad1c3801 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -39,21 +39,11 @@ * @packageDocumentation */ -import { - ElasticsearchServiceSetup, - InternalElasticsearchServiceSetup, - IScopedClusterClient, -} from './elasticsearch'; -import { InternalHttpServiceSetup, HttpServiceSetup } from './http'; +import { ElasticsearchServiceSetup, IScopedClusterClient } from './elasticsearch'; +import { HttpServiceSetup } from './http'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; -import { SavedObjectsServiceStart } from './saved_objects'; - -import { - InternalUiSettingsServiceSetup, - IUiSettingsClient, - UiSettingsServiceSetup, -} from './ui_settings'; +import { IUiSettingsClient, UiSettingsServiceSetup } from './ui_settings'; import { SavedObjectsClientContract } from './saved_objects/types'; export { bootstrap } from './bootstrap'; @@ -177,7 +167,6 @@ export { export { IUiSettingsClient, UiSettingsParams, - InternalUiSettingsServiceSetup, UiSettingsType, UiSettingsServiceSetup, UserProvidedValues, @@ -251,19 +240,4 @@ export interface CoreSetup { */ export interface CoreStart {} // eslint-disable-line @typescript-eslint/no-empty-interface -/** @internal */ -export interface InternalCoreSetup { - context: ContextSetup; - http: InternalHttpServiceSetup; - elasticsearch: InternalElasticsearchServiceSetup; - uiSettings: InternalUiSettingsServiceSetup; -} - -/** - * @internal - */ -export interface InternalCoreStart { - savedObjects: SavedObjectsServiceStart; -} - export { ContextSetup, PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId }; diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts new file mode 100644 index 00000000000000..1330c5aee64fd7 --- /dev/null +++ b/src/core/server/internal_types.ts @@ -0,0 +1,39 @@ +/* + * 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 { InternalElasticsearchServiceSetup } from './elasticsearch'; +import { InternalHttpServiceSetup } from './http'; +import { InternalUiSettingsServiceSetup } from './ui_settings'; +import { ContextSetup } from './context'; +import { SavedObjectsServiceStart } from './saved_objects'; + +/** @internal */ +export interface InternalCoreSetup { + context: ContextSetup; + http: InternalHttpServiceSetup; + elasticsearch: InternalElasticsearchServiceSetup; + uiSettings: InternalUiSettingsServiceSetup; +} + +/** + * @internal + */ +export interface InternalCoreStart { + savedObjects: SavedObjectsServiceStart; +} diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index b7c55a8af7c18d..99963ad9ce3e89 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -20,7 +20,8 @@ import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; import { first, map, publishReplay, tap } from 'rxjs/operators'; import { CoreService } from '../../types'; -import { InternalCoreSetup, InternalCoreStart, CoreSetup, CoreStart } from '../'; +import { CoreSetup, CoreStart } from '../'; +import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { SavedObjectsLegacyUiExports } from '../types'; import { Config } from '../config'; import { CoreContext } from '../core_context'; diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 2964e34c370b1c..38fe519567a635 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -28,7 +28,7 @@ import { PluginWrapper } from './plugin'; import { DiscoveredPlugin, DiscoveredPluginInternal, PluginName } from './types'; import { PluginsConfig, PluginsConfigType } from './plugins_config'; import { PluginsSystem } from './plugins_system'; -import { InternalCoreSetup } from '..'; +import { InternalCoreSetup } from '../internal_types'; /** @public */ export interface PluginsServiceSetup { diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 9f7d8e4f351728..34acb66d4e9310 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -77,18 +77,15 @@ export class PluginsSystem { this.log.debug(`Setting up plugin "${pluginName}"...`); const pluginDeps = new Set([...plugin.requiredPlugins, ...plugin.optionalPlugins]); - const pluginDepContracts = Array.from(pluginDeps).reduce( - (depContracts, dependencyName) => { - // Only set if present. Could be absent if plugin does not have server-side code or is a - // missing optional dependency. - if (contracts.has(dependencyName)) { - depContracts[dependencyName] = contracts.get(dependencyName); - } - - return depContracts; - }, - {} as Record - ); + const pluginDepContracts = Array.from(pluginDeps).reduce((depContracts, dependencyName) => { + // Only set if present. Could be absent if plugin does not have server-side code or is a + // missing optional dependency. + if (contracts.has(dependencyName)) { + depContracts[dependencyName] = contracts.get(dependencyName); + } + + return depContracts; + }, {} as Record); contracts.set( pluginName, @@ -116,18 +113,15 @@ export class PluginsSystem { this.log.debug(`Starting plugin "${pluginName}"...`); const plugin = this.plugins.get(pluginName)!; const pluginDeps = new Set([...plugin.requiredPlugins, ...plugin.optionalPlugins]); - const pluginDepContracts = Array.from(pluginDeps).reduce( - (depContracts, dependencyName) => { - // Only set if present. Could be absent if plugin does not have server-side code or is a - // missing optional dependency. - if (contracts.has(dependencyName)) { - depContracts[dependencyName] = contracts.get(dependencyName); - } - - return depContracts; - }, - {} as Record - ); + const pluginDepContracts = Array.from(pluginDeps).reduce((depContracts, dependencyName) => { + // Only set if present. Could be absent if plugin does not have server-side code or is a + // missing optional dependency. + if (contracts.has(dependencyName)) { + depContracts[dependencyName] = contracts.get(dependencyName); + } + + return depContracts; + }, {} as Record); contracts.set( pluginName, diff --git a/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts index 3bac17bc46686f..61e4d752445c4f 100644 --- a/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts +++ b/src/core/server/saved_objects/mappings/lib/get_root_properties_objects.ts @@ -39,17 +39,14 @@ const blacklist = ['migrationVersion', 'references']; export function getRootPropertiesObjects(mappings: IndexMapping) { const rootProperties = getRootProperties(mappings); - return Object.entries(rootProperties).reduce( - (acc, [key, value]) => { - // we consider the existence of the properties or type of object to designate that this is an object datatype - if ( - !blacklist.includes(key) && - ((value as ComplexFieldMapping).properties || value.type === 'object') - ) { - acc[key] = value; - } - return acc; - }, - {} as MappingProperties - ); + return Object.entries(rootProperties).reduce((acc, [key, value]) => { + // we consider the existence of the properties or type of object to designate that this is an object datatype + if ( + !blacklist.includes(key) && + ((value as ComplexFieldMapping).properties || value.type === 'object') + ) { + acc[key] = value; + } + return acc; + }, {} as MappingProperties); } diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 54b9938decb0a9..51d4a8ad50ad63 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -246,15 +246,17 @@ export class SavedObjectsRepository { const expectedResult = { esRequestIndex: requestIndexCounter++, requestedId: object.id, - rawMigratedDoc: this._serializer.savedObjectToRaw(this._migrator.migrateDocument({ - id: object.id, - type: object.type, - attributes: object.attributes, - migrationVersion: object.migrationVersion, - namespace, - updated_at: time, - references: object.references || [], - }) as SanitizedSavedObjectDoc), + rawMigratedDoc: this._serializer.savedObjectToRaw( + this._migrator.migrateDocument({ + id: object.id, + type: object.type, + attributes: object.attributes, + migrationVersion: object.migrationVersion, + namespace, + updated_at: time, + references: object.references || [], + }) as SanitizedSavedObjectDoc + ), }; bulkCreateParams.push( diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 73626775381d7d..97a04a4a4efaba 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -449,11 +449,11 @@ export interface AuthToolkit { export class BasePath { // @internal constructor(serverBasePath?: string); - get: (request: LegacyRequest | KibanaRequest) => string; + get: (request: KibanaRequest | LegacyRequest) => string; prepend: (path: string) => string; remove: (path: string) => string; readonly serverBasePath: string; - set: (request: LegacyRequest | KibanaRequest, requestSpecificBasePath: string) => void; + set: (request: KibanaRequest | LegacyRequest, requestSpecificBasePath: string) => void; } // Warning: (ae-forgotten-export) The symbol "BootstrapArgs" needs to be exported by the entry point index.d.ts @@ -712,36 +712,6 @@ export interface IndexSettingsDeprecationInfo { [indexName: string]: DeprecationInfo[]; } -// @internal (undocumented) -export interface InternalCoreSetup { - // (undocumented) - context: ContextSetup; - // Warning: (ae-forgotten-export) The symbol "InternalElasticsearchServiceSetup" needs to be exported by the entry point index.d.ts - // - // (undocumented) - elasticsearch: InternalElasticsearchServiceSetup; - // Warning: (ae-forgotten-export) The symbol "InternalHttpServiceSetup" needs to be exported by the entry point index.d.ts - // - // (undocumented) - http: InternalHttpServiceSetup; - // (undocumented) - uiSettings: InternalUiSettingsServiceSetup; -} - -// @internal (undocumented) -export interface InternalCoreStart { - // Warning: (ae-forgotten-export) The symbol "SavedObjectsServiceStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) - savedObjects: SavedObjectsServiceStart; -} - -// @internal (undocumented) -export interface InternalUiSettingsServiceSetup { - asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; - register(settings: Record): void; -} - // @public export interface IRouter { delete:

(route: RouteConfig, handler: RequestHandler) => void; @@ -839,7 +809,7 @@ export interface LegacyRequest extends Request { // @public @deprecated (undocumented) export interface LegacyServiceSetupDeps { - // Warning: (ae-incompatible-release-tags) The symbol "core" is marked as @public, but its signature references "InternalCoreSetup" which is marked as @internal + // Warning: (ae-forgotten-export) The symbol "InternalCoreSetup" needs to be exported by the entry point index.d.ts // // (undocumented) core: InternalCoreSetup & { @@ -851,7 +821,7 @@ export interface LegacyServiceSetupDeps { // @public @deprecated (undocumented) export interface LegacyServiceStartDeps { - // Warning: (ae-incompatible-release-tags) The symbol "core" is marked as @public, but its signature references "InternalCoreStart" which is marked as @internal + // Warning: (ae-forgotten-export) The symbol "InternalCoreStart" needs to be exported by the entry point index.d.ts // // (undocumented) core: InternalCoreStart & { diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index aee6461580654e..f912a31901ad83 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -70,7 +70,10 @@ test('injects legacy dependency to context#setup()', async () => { const pluginA = Symbol(); const pluginB = Symbol(); - const pluginDependencies = new Map([[pluginA, []], [pluginB, [pluginA]]]); + const pluginDependencies = new Map([ + [pluginA, []], + [pluginB, [pluginA]], + ]); mockPluginsService.discover.mockResolvedValue(pluginDependencies); await server.setup(); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 46974e204c7a42..6c38de03f0f2d3 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -39,7 +39,8 @@ import { config as uiSettingsConfig } from './ui_settings'; import { mapToObject } from '../utils/'; import { ContextService } from './context'; import { SavedObjectsServiceSetup } from './saved_objects/saved_objects_service'; -import { RequestHandlerContext, InternalCoreSetup } from '.'; +import { RequestHandlerContext } from '.'; +import { InternalCoreSetup } from './internal_types'; const coreId = Symbol('core'); diff --git a/src/core/server/ui_settings/ui_settings_client.ts b/src/core/server/ui_settings/ui_settings_client.ts index 423ff2a1dfd909..1a0f29f6ae6d93 100644 --- a/src/core/server/ui_settings/ui_settings_client.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -83,14 +83,11 @@ export class UiSettingsClient implements IUiSettingsClient { async getAll() { const raw = await this.getRaw(); - return Object.keys(raw).reduce( - (all, key) => { - const item = raw[key]; - all[key] = ('userValue' in item ? item.userValue : item.value) as T; - return all; - }, - {} as Record - ); + return Object.keys(raw).reduce((all, key) => { + const item = raw[key]; + all[key] = ('userValue' in item ? item.userValue : item.value) as T; + return all; + }, {} as Record); } async getUserProvided(): Promise> { diff --git a/src/core/utils/context.ts b/src/core/utils/context.ts index 022c3e43300322..775c8906754100 100644 --- a/src/core/utils/context.ts +++ b/src/core/utils/context.ts @@ -254,23 +254,20 @@ export class ContextContainer> return [...this.contextProviders] .sort(sortByCoreFirst(this.coreId)) .filter(([contextName]) => contextsToBuild.has(contextName)) - .reduce( - async (contextPromise, [contextName, { provider, source: providerSource }]) => { - const resolvedContext = await contextPromise; + .reduce(async (contextPromise, [contextName, { provider, source: providerSource }]) => { + const resolvedContext = await contextPromise; - // For the next provider, only expose the context available based on the dependencies of the plugin that - // registered that provider. - const exposedContext = pick(resolvedContext, [ - ...this.getContextNamesForSource(providerSource), - ]) as Partial>; + // For the next provider, only expose the context available based on the dependencies of the plugin that + // registered that provider. + const exposedContext = pick(resolvedContext, [ + ...this.getContextNamesForSource(providerSource), + ]) as Partial>; - return { - ...resolvedContext, - [contextName]: await provider(exposedContext, ...contextArgs), - }; - }, - Promise.resolve({}) as Promise> - ); + return { + ...resolvedContext, + [contextName]: await provider(exposedContext, ...contextArgs), + }; + }, Promise.resolve({}) as Promise>); } private getContextNamesForSource( diff --git a/src/core/utils/map_utils.test.ts b/src/core/utils/map_utils.test.ts index 0d9b2a6129de0a..315ae3328c47f6 100644 --- a/src/core/utils/map_utils.test.ts +++ b/src/core/utils/map_utils.test.ts @@ -42,7 +42,11 @@ describe('groupIntoMap', () => { const groupBy = (item: { id: number }) => item.id; expect(groupIntoMap([{ id: 1 }, { id: 2 }, { id: 3 }], groupBy)).toEqual( - new Map([[1, [{ id: 1 }]], [2, [{ id: 2 }]], [3, [{ id: 3 }]]]) + new Map([ + [1, [{ id: 1 }]], + [2, [{ id: 2 }]], + [3, [{ id: 3 }]], + ]) ); }); @@ -93,7 +97,12 @@ describe('mapValuesOfMap', () => { map.set(even, 2); map.set(odd, 1); - expect(mapValuesOfMap(map, mapper)).toEqual(new Map([[even, 6], [odd, 3]])); + expect(mapValuesOfMap(map, mapper)).toEqual( + new Map([ + [even, 6], + [odd, 3], + ]) + ); expect(map.get(odd)).toEqual(1); expect(map.get(even)).toEqual(2); }); diff --git a/src/core/utils/merge.ts b/src/core/utils/merge.ts index aead3f35ba841b..8e5d9f4860d955 100644 --- a/src/core/utils/merge.ts +++ b/src/core/utils/merge.ts @@ -66,20 +66,17 @@ const mergeObjects = , U extends Record - [...new Set([...Object.keys(baseObj), ...Object.keys(overrideObj)])].reduce( - (merged, key) => { - const baseVal = baseObj[key]; - const overrideVal = overrideObj[key]; + [...new Set([...Object.keys(baseObj), ...Object.keys(overrideObj)])].reduce((merged, key) => { + const baseVal = baseObj[key]; + const overrideVal = overrideObj[key]; - if (isMergable(baseVal) && isMergable(overrideVal)) { - merged[key] = mergeObjects(baseVal, overrideVal); - } else if (overrideVal !== undefined) { - merged[key] = overrideVal; - } else if (baseVal !== undefined) { - merged[key] = baseVal; - } + if (isMergable(baseVal) && isMergable(overrideVal)) { + merged[key] = mergeObjects(baseVal, overrideVal); + } else if (overrideVal !== undefined) { + merged[key] = overrideVal; + } else if (baseVal !== undefined) { + merged[key] = baseVal; + } - return merged; - }, - {} as any - ); + return merged; + }, {} as any); diff --git a/src/core/utils/pick.ts b/src/core/utils/pick.ts index 77854f9af680b4..08288343d90778 100644 --- a/src/core/utils/pick.ts +++ b/src/core/utils/pick.ts @@ -18,14 +18,11 @@ */ export function pick(obj: T, keys: K[]): Pick { - return keys.reduce( - (acc, key) => { - if (obj.hasOwnProperty(key)) { - acc[key] = obj[key]; - } + return keys.reduce((acc, key) => { + if (obj.hasOwnProperty(key)) { + acc[key] = obj[key]; + } - return acc; - }, - {} as Pick - ); + return acc; + }, {} as Pick); } diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 3ae7d33d24d68a..5c0462ce86fa94 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -49,9 +49,18 @@ export const CleanClientModulesOnDLLTask = { `${baseDir}/src/plugins/*/server/index.js`, `!${baseDir}/src/plugins/**/public` ]); + const discoveredNewPlatformXpackPlugins = await globby([ + `${baseDir}/x-pack/plugins/*/server/index.js`, + `!${baseDir}/x-pack/plugins/**/public` + ]); // Compose all the needed entries - const serverEntries = [ ...mainCodeEntries, ...discoveredLegacyCorePluginEntries, ...discoveredPluginEntries]; + const serverEntries = [ + ...mainCodeEntries, + ...discoveredLegacyCorePluginEntries, + ...discoveredPluginEntries, + ...discoveredNewPlatformXpackPlugins + ]; // Get the dependencies found searching through the server // side code entries that were provided diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 0926ef365c894c..6609b905b81eca 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -181,6 +181,7 @@ kibana_vars=( xpack.security.secureCookies xpack.security.sessionTimeout telemetry.enabled + telemetry.sendUsageFrom ) longopts='' diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 0c785a84bb4692..f5c20da89dcfaf 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -103,6 +103,7 @@ export default { 'packages/kbn-pm/dist/index.js' ], snapshotSerializers: [ + '/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts', '/node_modules/enzyme-to-json/serializer', ], reporters: [ diff --git a/src/dev/license_checker/valid.ts b/src/dev/license_checker/valid.ts index 8fe09db0a58746..9142955185a1a0 100644 --- a/src/dev/license_checker/valid.ts +++ b/src/dev/license_checker/valid.ts @@ -36,24 +36,21 @@ interface Options { * violations or returns undefined. */ export function assertLicensesValid({ packages, validLicenses }: Options) { - const invalidMsgs = packages.reduce( - (acc, pkg) => { - const invalidLicenses = pkg.licenses.filter(license => !validLicenses.includes(license)); + const invalidMsgs = packages.reduce((acc, pkg) => { + const invalidLicenses = pkg.licenses.filter(license => !validLicenses.includes(license)); - if (pkg.licenses.length && !invalidLicenses.length) { - return acc; - } + if (pkg.licenses.length && !invalidLicenses.length) { + return acc; + } - return acc.concat(dedent` + return acc.concat(dedent` ${pkg.name} version: ${pkg.version} all licenses: ${pkg.licenses} invalid licenses: ${invalidLicenses.join(', ')} path: ${pkg.relative} `); - }, - [] as string[] - ); + }, [] as string[]); if (invalidMsgs.length) { throw createFailError( diff --git a/src/legacy/core_plugins/console/index.ts b/src/legacy/core_plugins/console/index.ts index 30a85f4e7d342b..caef3ff6f99f38 100644 --- a/src/legacy/core_plugins/console/index.ts +++ b/src/legacy/core_plugins/console/index.ts @@ -56,7 +56,6 @@ export default function(kibana: any) { const npSrc = resolve(__dirname, 'np_ready/public'); let defaultVars: any; - const apps: any[] = []; return new kibana.Plugin({ id: 'console', require: ['elasticsearch'], @@ -181,8 +180,6 @@ export default function(kibana: any) { }, uiExports: { - apps, - hacks: ['plugins/console/quarantined/hacks/register'], devTools: [`${npSrc}/legacy`], styleSheetPaths: resolve(__dirname, 'public/quarantined/index.scss'), diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx index 4e5afbdb5821e9..518630c5a07c1c 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/main/main.tsx @@ -70,7 +70,7 @@ export function Main() { }; return ( - <> +

setShowSettings(false)} /> : null} {showHelp ? setShowHelp(false)} /> : null} - +
); } diff --git a/src/legacy/core_plugins/console/np_ready/public/legacy.ts b/src/legacy/core_plugins/console/np_ready/public/legacy.ts index 1a2d312823f6f2..8c60ff23648be3 100644 --- a/src/legacy/core_plugins/console/np_ready/public/legacy.ts +++ b/src/legacy/core_plugins/console/np_ready/public/legacy.ts @@ -24,80 +24,31 @@ import 'brace/mode/json'; import 'brace/mode/text'; /* eslint-disable @kbn/eslint/no-restricted-paths */ -import { toastNotifications as notifications } from 'ui/notify'; import { npSetup, npStart } from 'ui/new_platform'; -import uiRoutes from 'ui/routes'; -import { DOC_LINK_VERSION } from 'ui/documentation_links'; import { I18nContext } from 'ui/i18n'; import { ResizeChecker } from 'ui/resize_checker'; -import 'ui/capabilities/route_setup'; /* eslint-enable @kbn/eslint/no-restricted-paths */ -import template from '../../public/quarantined/index.html'; -import { App, AppUnmount, NotificationsSetup } from '../../../../../core/public'; - export interface XPluginSet { + devTools: DevToolsSetup; + feature_catalogue: FeatureCatalogueSetup; __LEGACY: { I18nContext: any; ResizeChecker: any; - docLinkVersion: string; }; } import { plugin } from '.'; +import { DevToolsSetup } from '../../../../../plugins/dev_tools/public'; +import { FeatureCatalogueSetup } from '../../../../../plugins/feature_catalogue/public'; const pluginInstance = plugin({} as any); -const anyObject = {} as any; - -uiRoutes.when('/dev_tools/console', { - requireUICapability: 'dev_tools.show', - controller: function RootController($scope) { - // Stub out this config for now... - $scope.topNavMenu = []; - - $scope.initReactApp = () => { - const targetElement = document.querySelector('#consoleRoot'); - if (!targetElement) { - const message = `Could not mount Console App!`; - npSetup.core.fatalErrors.add(message); - throw new Error(message); - } - - let unmount: AppUnmount | Promise; - - const mockedSetupCore = { - ...npSetup.core, - notifications: (notifications as unknown) as NotificationsSetup, - application: { - register(app: App): void { - try { - unmount = app.mount(anyObject, { element: targetElement, appBasePath: '' }); - } catch (e) { - npSetup.core.fatalErrors.add(e); - } - }, - registerMountContext() {}, - }, - }; - - pluginInstance.setup(mockedSetupCore, { - ...npSetup.plugins, - __LEGACY: { - I18nContext, - ResizeChecker, - docLinkVersion: DOC_LINK_VERSION, - }, - }); - pluginInstance.start(npStart.core); - - $scope.$on('$destroy', async () => { - if (unmount) { - const fn = await unmount; - fn(); - } - }); - }; +pluginInstance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + I18nContext, + ResizeChecker, }, - template, }); +pluginInstance.start(npStart.core); diff --git a/src/legacy/core_plugins/console/np_ready/public/plugin.ts b/src/legacy/core_plugins/console/np_ready/public/plugin.ts index 188a738d597944..f02b0b5e729992 100644 --- a/src/legacy/core_plugins/console/np_ready/public/plugin.ts +++ b/src/legacy/core_plugins/console/np_ready/public/plugin.ts @@ -18,26 +18,55 @@ */ import { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import { PluginInitializerContext, Plugin, CoreStart, CoreSetup } from '../../../../../core/public'; import { XPluginSet } from './legacy'; -import { boot } from './application'; export class ConsoleUIPlugin implements Plugin { // @ts-ignore constructor(private readonly ctx: PluginInitializerContext) {} - async setup({ application, notifications }: CoreSetup, pluginSet: XPluginSet) { + async setup({ notifications }: CoreSetup, pluginSet: XPluginSet) { const { - __LEGACY: { docLinkVersion, I18nContext, ResizeChecker }, + __LEGACY: { I18nContext, ResizeChecker }, + devTools, + feature_catalogue, } = pluginSet; - application.register({ + feature_catalogue.register({ + id: 'console', + title: i18n.translate('console.devToolsTitle', { + defaultMessage: 'Console', + }), + description: i18n.translate('console.devToolsDescription', { + defaultMessage: 'Skip cURL and use this JSON interface to work with your data directly.', + }), + icon: 'consoleApp', + path: '/app/kibana#/dev_tools/console', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + + devTools.register({ id: 'console', order: 1, - title: 'Console', - mount(ctx, { element }) { - render(boot({ docLinkVersion, I18nContext, ResizeChecker, notifications }), element); + title: i18n.translate('console.consoleDisplayName', { + defaultMessage: 'Console', + }), + enableRouting: false, + async mount(ctx, { element }) { + const { boot } = await import('./application'); + render( + boot({ + docLinkVersion: ctx.core.docLinks.DOC_LINK_VERSION, + I18nContext, + ResizeChecker, + notifications, + }), + element + ); return () => { unmountComponentAtNode(element); }; diff --git a/src/legacy/core_plugins/console/public/quarantined/index.html b/src/legacy/core_plugins/console/public/quarantined/index.html deleted file mode 100644 index 66a693d4b2af78..00000000000000 --- a/src/legacy/core_plugins/console/public/quarantined/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss b/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss index 5333aff8b87da3..9e2478cb0704ea 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/_index.scss @@ -1,3 +1,4 @@ @import 'variables'; @import 'global_filter_group'; @import 'global_filter_item'; +@import 'filter_editor/index'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss new file mode 100644 index 00000000000000..a5fac10e4693fb --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_filter_editor.scss @@ -0,0 +1,3 @@ +.globalFilterEditor__fieldInput { + max-width: $euiSize * 13; +} diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss new file mode 100644 index 00000000000000..21ba32ec6a6fea --- /dev/null +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/_index.scss @@ -0,0 +1 @@ +@import 'filter_editor'; \ No newline at end of file diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx index 5dd5c056477896..84da576e8205cd 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx @@ -30,6 +30,7 @@ import { EuiPopoverTitle, EuiSpacer, EuiSwitch, + EuiSwitchEvent, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; @@ -245,6 +246,7 @@ class FilterEditorUI extends Component { private renderFieldInput() { const { selectedIndexPattern, selectedField } = this.state; const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : []; + return ( { onChange={this.onFieldChange} singleSelection={{ asPlainText: true }} isClearable={false} + className="globalFilterEditor__fieldInput" data-test-subj="filterFieldSuggestionList" /> @@ -431,7 +434,7 @@ class FilterEditorUI extends Component { this.setState({ selectedOperator, params }); }; - private onCustomLabelSwitchChange = (event: React.ChangeEvent) => { + private onCustomLabelSwitchChange = (event: EuiSwitchEvent) => { const useCustomLabel = event.target.checked; const customLabel = event.target.checked ? '' : null; this.setState({ useCustomLabel, customLabel }); diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index dbff5096f2287d..7ee3e375c0967e 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -17,7 +17,8 @@ * under the License. */ -import { mockFields, mockIndexPattern } from '../../../../index_patterns'; +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { stubIndexPattern, stubFields } from '../../../../../../../../plugins/data/public/stubs'; import { IndexPattern, Field } from '../../../../index'; import { buildFilter, @@ -45,8 +46,8 @@ import { esFilters } from '../../../../../../../../plugins/data/public'; jest.mock('ui/new_platform'); -const mockedFields = mockFields as Field[]; -const mockedIndexPattern = mockIndexPattern as IndexPattern; +const mockedFields = stubFields as Field[]; +const mockedIndexPattern = stubIndexPattern as IndexPattern; describe('Filter editor utils', () => { describe('getQueryDslFromFilter', () => { @@ -171,14 +172,14 @@ describe('Filter editor utils', () => { describe('getOperatorOptions', () => { it('returns range for number fields', () => { - const [field] = mockFields.filter(({ type }) => type === 'number'); + const [field] = stubFields.filter(({ type }) => type === 'number'); const operatorOptions = getOperatorOptions(field as Field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).not.toBeUndefined(); }); it('does not return range for string fields', () => { - const [field] = mockFields.filter(({ type }) => type === 'string'); + const [field] = stubFields.filter(({ type }) => type === 'string'); const operatorOptions = getOperatorOptions(field as Field); const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); expect(rangeOperator).toBeUndefined(); diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 60828b4a2a2025..2412541e8c5c8f 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -38,7 +38,7 @@ export { IndexPatterns, StaticIndexPattern, } from './index_patterns'; -export { Query, QueryBarInput } from './query'; +export { QueryBarInput } from './query'; export { SearchBar, SearchBarProps, SavedQueryAttributes, SavedQuery } from './search'; /** @public static code */ @@ -58,6 +58,4 @@ export { IndexPatternMissingIndices, NoDefaultIndexPattern, NoDefinedIndexPatterns, - mockFields, - mockIndexPattern, } from './index_patterns'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts index 4767b6d3a3ca7a..2c58af9deaf49d 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -52,11 +52,13 @@ export class IndexPatterns { } private async refreshSavedObjectsCache() { - this.savedObjectsCache = (await this.savedObjectsClient.find({ - type: 'index-pattern', - fields: [], - perPage: 10000, - })).savedObjects; + this.savedObjectsCache = ( + await this.savedObjectsClient.find({ + type: 'index-pattern', + fields: [], + perPage: 10000, + }) + ).savedObjects; } getIds = async (refresh: boolean = false) => { diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index bdeeb787c983d6..9ce1b5f2e4a208 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -24,11 +24,11 @@ import { NotificationsStart, } from 'src/core/public'; import { Field, FieldList, FieldListInterface, FieldType } from './fields'; -import { createFlattenHitWrapper } from './index_patterns'; import { createIndexPatternSelect } from './components'; import { setNotifications } from './services'; import { + createFlattenHitWrapper, formatHitProvider, IndexPattern, IndexPatterns, @@ -92,8 +92,6 @@ export { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE, isFilterable, validateIndexPattern, - mockFields, - mockIndexPattern, } from './utils'; /** @public */ diff --git a/src/legacy/core_plugins/data/public/index_patterns/utils.ts b/src/legacy/core_plugins/data/public/index_patterns/utils.ts index 62f5ddbe9e2b05..1c877f4f142513 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/utils.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/utils.ts @@ -19,8 +19,7 @@ import { find, get } from 'lodash'; -import { Field, FieldType } from './fields'; -import { StaticIndexPattern } from './index_patterns'; +import { Field } from './fields'; import { getFilterableKbnTypeNames } from '../../../../../plugins/data/public'; import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; @@ -139,69 +138,3 @@ export function getRoutes() { sourceFilters: '/management/kibana/index_patterns/{{id}}?_a=(tab:sourceFilters)', }; } - -export const mockFields: FieldType[] = [ - { - name: 'machine.os', - esTypes: ['text'], - type: 'string', - aggregatable: false, - searchable: false, - filterable: true, - }, - { - name: 'machine.os.raw', - type: 'string', - esTypes: ['keyword'], - aggregatable: true, - searchable: true, - filterable: true, - }, - { - name: 'not.filterable', - type: 'string', - esTypes: ['text'], - aggregatable: true, - searchable: false, - filterable: false, - }, - { - name: 'bytes', - type: 'number', - esTypes: ['long'], - aggregatable: true, - searchable: true, - filterable: true, - }, - { - name: '@timestamp', - type: 'date', - esTypes: ['date'], - aggregatable: true, - searchable: true, - filterable: true, - }, - { - name: 'clientip', - type: 'ip', - esTypes: ['ip'], - aggregatable: true, - searchable: true, - filterable: true, - }, - { - name: 'bool.field', - type: 'boolean', - esTypes: ['boolean'], - aggregatable: true, - searchable: true, - filterable: true, - }, -]; - -export const mockIndexPattern: StaticIndexPattern = { - id: 'logstash-*', - fields: mockFields, - title: 'logstash-*', - timeFieldName: '@timestamp', -}; diff --git a/src/legacy/core_plugins/data/public/mocks.ts b/src/legacy/core_plugins/data/public/mocks.ts index d3b5944127965d..39d1296ddf8bc4 100644 --- a/src/legacy/core_plugins/data/public/mocks.ts +++ b/src/legacy/core_plugins/data/public/mocks.ts @@ -18,12 +18,10 @@ */ import { indexPatternsServiceMock } from './index_patterns/index_patterns_service.mock'; -import { queryServiceMock } from './query/query_service.mock'; function createDataSetupMock() { return { indexPatterns: indexPatternsServiceMock.createSetupContract(), - query: queryServiceMock.createSetupContract(), }; } diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 76beb4ee560535..2059f61fde59ed 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -19,7 +19,6 @@ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { SearchService, SearchStart, createSearchBar, StatetfulSearchBarProps } from './search'; -import { QueryService, QuerySetup } from './query'; import { IndexPatternsService, IndexPatternsSetup, IndexPatternsStart } from './index_patterns'; import { Storage, IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; import { DataPublicPluginStart } from '../../../../plugins/data/public'; @@ -42,7 +41,6 @@ export interface DataPluginStartDependencies { * @public */ export interface DataSetup { - query: QuerySetup; indexPatterns: IndexPatternsSetup; } @@ -52,7 +50,6 @@ export interface DataSetup { * @public */ export interface DataStart { - query: QuerySetup; indexPatterns: IndexPatternsStart; search: SearchStart; ui: { @@ -74,7 +71,6 @@ export interface DataStart { export class DataPlugin implements Plugin { private readonly indexPatterns: IndexPatternsService = new IndexPatternsService(); - private readonly query: QueryService = new QueryService(); private readonly search: SearchService = new SearchService(); private setupApi!: DataSetup; @@ -85,7 +81,6 @@ export class DataPlugin implements Plugin PersistedLog: mockPersistedLogFactory, })); -jest.mock('../lib/fetch_index_patterns', () => ({ +jest.mock('./fetch_index_patterns', () => ({ fetchIndexPatterns: mockFetchIndexPatterns, })); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx index 5576427b1592a6..31a17315db7dd3 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx @@ -38,18 +38,22 @@ import { AutocompleteSuggestion, AutocompleteSuggestionType, PersistedLog, + toUser, + fromUser, + matchPairs, + getQueryLog, + Query, } from '../../../../../../../plugins/data/public'; import { withKibana, KibanaReactContextValue, + toMountPoint, } from '../../../../../../../plugins/kibana_react/public'; import { IndexPattern, StaticIndexPattern } from '../../../index_patterns'; -import { Query, getQueryLog } from '../index'; -import { fromUser, matchPairs, toUser } from '../lib'; import { QueryLanguageSwitcher } from './language_switcher'; import { SuggestionsComponent } from './typeahead/suggestions_component'; -import { fetchIndexPatterns } from '../lib/fetch_index_patterns'; import { IDataPluginServices } from '../../../types'; +import { fetchIndexPatterns } from './fetch_index_patterns'; interface Props { kibana: KibanaReactContextValue; @@ -361,7 +365,7 @@ export class QueryBarInputUI extends Component { id: 'data.query.queryBar.KQLNestedQuerySyntaxInfoTitle', defaultMessage: 'KQL nested query syntax', }), - text: ( + text: toMountPoint(

(queryLanguage ? getQueryLog(uiSettings!, storage, appName, queryLanguage) : undefined), - [queryLanguage] + () => + queryLanguage && uiSettings && storage && appName + ? getQueryLog(uiSettings!, storage, appName, queryLanguage) + : undefined, + [appName, queryLanguage, uiSettings, storage] ); function onClickSubmitButton(event: React.MouseEvent) { @@ -298,7 +305,7 @@ function QueryBarTopRowUI(props: Props) { id: 'data.query.queryBar.luceneSyntaxWarningTitle', defaultMessage: 'Lucene syntax warning', }), - text: ( + text: toMountPoint(

void; onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void; // User has saved the current state as a saved query onSaved?: (savedQuery: SavedQuery) => void; @@ -207,6 +207,18 @@ class SearchBarUI extends Component { ); } + /* + * This Function is here to show the toggle in saved query form + * in case you the date range (from/to) + */ + private shouldRenderTimeFilterInSavedQueryForm() { + const { dateRangeFrom, dateRangeTo, showDatePicker } = this.props; + return ( + showDatePicker || + (!showDatePicker && dateRangeFrom !== undefined && dateRangeTo !== undefined) + ); + } + public setFilterBarHeight = () => { requestAnimationFrame(() => { const height = @@ -300,6 +312,9 @@ class SearchBarUI extends Component { dateRangeFrom: queryAndDateRange.dateRange.from, dateRangeTo: queryAndDateRange.dateRange.to, }); + if (this.props.onQueryChange) { + this.props.onQueryChange(queryAndDateRange); + } }; public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => { @@ -441,7 +456,7 @@ class SearchBarUI extends Component { onSave={this.onSave} onClose={() => this.setState({ showSaveQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null} {this.state.showSaveNewQueryModal ? ( @@ -450,7 +465,7 @@ class SearchBarUI extends Component { onSave={savedQueryMeta => this.onSave(savedQueryMeta, true)} onClose={() => this.setState({ showSaveNewQueryModal: false })} showFilterOption={this.props.showFilterBar} - showTimeFilterOption={this.props.showDatePicker} + showTimeFilterOption={this.shouldRenderTimeFilterInSavedQueryForm()} /> ) : null}

diff --git a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx index ebde9d60b0b51e..f369bf997c1a91 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx @@ -17,9 +17,7 @@ * under the License. */ -import { RefreshInterval, TimeRange } from 'src/plugins/data/public'; -import { Query } from '../../query/query_bar'; -import { esFilters } from '../../../../../../plugins/data/public'; +import { RefreshInterval, TimeRange, Query, esFilters } from 'src/plugins/data/public'; export * from './components'; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js index 96c0802d3772a2..ea029af9e48908 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/list_control_editor.test.js @@ -236,7 +236,7 @@ test('handleCheckboxOptionChange - multiselect', async () => { component.update(); const checkbox = findTestSubject(component, 'listControlMultiselectInput'); - checkbox.simulate('change', { target: { checked: true } }); + checkbox.simulate('click'); sinon.assert.notCalled(handleFieldNameChange); sinon.assert.notCalled(handleIndexPatternChange); sinon.assert.notCalled(handleNumberOptionChange); @@ -247,7 +247,9 @@ test('handleCheckboxOptionChange - multiselect', async () => { expectedControlIndex, expectedOptionName, sinon.match((evt) => { - if (evt.target.checked === true) { + // Synthetic `evt.target.checked` does not get altered by EuiSwitch, + // but its aria attribute is correctly updated + if (evt.target.getAttribute('aria-checked') === 'true') { return true; } return false; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js index 39f5f6a50a5a63..8784f0e79ca8d2 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js +++ b/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.test.js @@ -47,8 +47,8 @@ describe('OptionsTab', () => { it('should update updateFiltersOnChange', () => { const component = mountWithIntl(); - const checkbox = component.find('[data-test-subj="inputControlEditorUpdateFiltersOnChangeCheckbox"] input[type="checkbox"]'); - checkbox.simulate('change', { target: { checked: true } }); + const checkbox = component.find('[data-test-subj="inputControlEditorUpdateFiltersOnChangeCheckbox"] button'); + checkbox.simulate('click'); expect(props.setValue).toHaveBeenCalledTimes(1); expect(props.setValue).toHaveBeenCalledWith('updateFiltersOnChange', true); @@ -56,8 +56,8 @@ describe('OptionsTab', () => { it('should update useTimeFilter', () => { const component = mountWithIntl(); - const checkbox = component.find('[data-test-subj="inputControlEditorUseTimeFilterCheckbox"] input[type="checkbox"]'); - checkbox.simulate('change', { target: { checked: true } }); + const checkbox = component.find('[data-test-subj="inputControlEditorUseTimeFilterCheckbox"] button'); + checkbox.simulate('click'); expect(props.setValue).toHaveBeenCalledTimes(1); expect(props.setValue).toHaveBeenCalledWith('useTimeFilter', true); @@ -65,8 +65,8 @@ describe('OptionsTab', () => { it('should update pinFilters', () => { const component = mountWithIntl(); - const checkbox = component.find('[data-test-subj="inputControlEditorPinFiltersCheckbox"] input[type="checkbox"]'); - checkbox.simulate('change', { target: { checked: true } }); + const checkbox = component.find('[data-test-subj="inputControlEditorPinFiltersCheckbox"] button'); + checkbox.simulate('click'); expect(props.setValue).toHaveBeenCalledTimes(1); expect(props.setValue).toHaveBeenCalledWith('pinFilters', true); diff --git a/src/legacy/core_plugins/interpreter/public/renderers/visualization.tsx b/src/legacy/core_plugins/interpreter/public/renderers/visualization.tsx index 9de6cdeaf5ec3e..f15cdf23fe15b8 100644 --- a/src/legacy/core_plugins/interpreter/public/renderers/visualization.tsx +++ b/src/legacy/core_plugins/interpreter/public/renderers/visualization.tsx @@ -21,7 +21,7 @@ import chrome from 'ui/chrome'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; // @ts-ignore -import { VisProvider } from '../../../../ui/public/visualize/loader/vis'; +import { Vis } from '../../../../ui/public/visualize/loader/vis'; import { Visualization } from '../../../../ui/public/visualize/components'; export const visualization = () => ({ @@ -33,8 +33,6 @@ export const visualization = () => ({ const visType = config.visType || visConfig.type; const $injector = await chrome.dangerouslyGetActiveInjector(); const $rootScope = $injector.get('$rootScope') as any; - const Private = $injector.get('Private') as any; - const Vis = Private(VisProvider); if (handlers.vis) { // special case in visualize, we need to render first (without executing the expression), for maps to work diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx index c7ada18f9e1f25..2ca4ed1e2343d0 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/components/options/metrics_axes/index.tsx @@ -83,9 +83,11 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) // stores previous aggs' custom labels const [lastCustomLabels, setLastCustomLabels] = useState({} as { [key: string]: string }); // stores previous aggs' field and type - const [lastSeriesAgg, setLastSeriesAgg] = useState({} as { - [key: string]: { type: string; field: string }; - }); + const [lastSeriesAgg, setLastSeriesAgg] = useState( + {} as { + [key: string]: { type: string; field: string }; + } + ); const updateAxisTitle = () => { const axes = cloneDeep(stateParams.valueAxes); diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 24cd4369123956..c7cda8aec0165d 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -62,7 +62,7 @@ export default function (kibana) { uiExports: { hacks: [ - 'plugins/kibana/dev_tools/hacks/hide_empty_tools', + 'plugins/kibana/dev_tools', ], fieldFormats: ['plugins/kibana/field_formats/register'], savedObjectTypes: [ diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx index 5fa3a938ed9dfc..656b54040ad99a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx @@ -26,19 +26,16 @@ import { IInjector } from 'ui/chrome'; // @ts-ignore import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter'; -// @ts-ignore -import { getFilterGenerator } from 'ui/filter_manager'; - import { AppStateClass as TAppStateClass, AppState as TAppState, } from 'ui/state_management/app_state'; import { KbnUrl } from 'ui/url/kbn_url'; -import { TimeRange } from 'src/plugins/data/public'; +import { TimeRange, Query } from 'src/plugins/data/public'; import { IndexPattern } from 'ui/index_patterns'; import { IPrivate } from 'ui/private'; -import { StaticIndexPattern, Query, SavedQuery } from 'plugins/data'; +import { StaticIndexPattern, SavedQuery } from 'plugins/data'; import moment from 'moment'; import { Subscription } from 'rxjs'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index 548a66297a3f9c..d82b89339b0d00 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -50,12 +50,13 @@ import { import { KbnUrl } from 'ui/url/kbn_url'; import { IndexPattern } from 'ui/index_patterns'; import { IPrivate } from 'ui/private'; -import { Query, SavedQuery } from 'src/legacy/core_plugins/data/public'; +import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { SaveOptions } from 'ui/saved_objects/saved_object'; import { capabilities } from 'ui/capabilities'; import { Subscription } from 'rxjs'; import { npStart } from 'ui/new_platform'; import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; +import { Query } from '../../../../../plugins/data/public'; import { start as data } from '../../../data/public/legacy'; import { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts index 8ffabe5add1c34..1a42ed837a9de3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state_manager.ts @@ -27,9 +27,9 @@ import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; import { Moment } from 'moment'; import { DashboardContainer } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public'; +import { Query } from 'src/plugins/data/public'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; import { esFilters } from '../../../../../../src/plugins/data/public'; -import { Query } from '../../../data/public'; import { getAppStateDefaults, migrateAppState } from './lib'; import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts index 8522495b9dedb2..e82fc58670e392 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts @@ -17,8 +17,7 @@ * under the License. */ -import { Query } from 'src/legacy/core_plugins/data/public'; -import { esFilters } from '../../../../../../plugins/data/public'; +import { esFilters, Query } from '../../../../../../plugins/data/public'; export interface Pre600FilterQuery { // pre 6.0.0 global query:queryString:options were stored per dashboard and would diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 5b860b0a2cc7c1..5b24aa13f4f77a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -19,9 +19,7 @@ import { SearchSource } from 'ui/courier'; import { SavedObject } from 'ui/saved_objects/saved_object'; -import { RefreshInterval } from 'src/plugins/data/public'; -import { Query } from 'src/legacy/core_plugins/data/public'; -import { esFilters } from '../../../../../../plugins/data/public'; +import { esFilters, Query, RefreshInterval } from '../../../../../../plugins/data/public'; export interface SavedObjectDashboard extends SavedObject { id?: string; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/types.ts index 5aaca7b62094f8..3c2c87a502da46 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/types.ts @@ -18,7 +18,6 @@ */ import { AppState } from 'ui/state_management/app_state'; -import { Query } from 'src/legacy/core_plugins/data/public'; import { AppState as TAppState } from 'ui/state_management/app_state'; import { ViewMode } from 'src/plugins/embeddable/public'; import { @@ -29,7 +28,7 @@ import { RawSavedDashboardPanel640To720, RawSavedDashboardPanel730ToLatest, } from './migrations/types'; -import { esFilters } from '../../../../../plugins/data/public'; +import { Query, esFilters } from '../../../../../plugins/data/public'; export type NavAction = (anchorElement?: any) => void; diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/_index.scss b/src/legacy/core_plugins/kibana/public/dev_tools/_index.scss index 563b140fd2eade..2e88d2e1285e3c 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/_index.scss +++ b/src/legacy/core_plugins/kibana/public/dev_tools/_index.scss @@ -16,3 +16,6 @@ } } +.devApp { + height: 100%; +} diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/application.tsx b/src/legacy/core_plugins/kibana/public/dev_tools/application.tsx new file mode 100644 index 00000000000000..3945d8d8dc8568 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dev_tools/application.tsx @@ -0,0 +1,184 @@ +/* + * 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 { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiTab, EuiTabs, EuiToolTip } from '@elastic/eui'; +import { HashRouter as Router, Switch, Route, Redirect } from 'react-router-dom'; +import * as React from 'react'; +import ReactDOM from 'react-dom'; +import { useEffect, useRef } from 'react'; + +import { AppMountContext } from 'kibana/public'; +import { DevTool } from '../../../../../plugins/dev_tools/public'; + +interface DevToolsWrapperProps { + devTools: readonly DevTool[]; + activeDevTool: DevTool; + appMountContext: AppMountContext; + updateRoute: (newRoute: string) => void; +} + +interface MountedDevToolDescriptor { + devTool: DevTool; + mountpoint: HTMLElement; + unmountHandler: () => void; +} + +function DevToolsWrapper({ + devTools, + activeDevTool, + appMountContext, + updateRoute, +}: DevToolsWrapperProps) { + const mountedTool = useRef(null); + + useEffect( + () => () => { + if (mountedTool.current) { + mountedTool.current.unmountHandler(); + } + }, + [] + ); + + return ( +
+ + {devTools.map(currentDevTool => ( + + { + if (!currentDevTool.disabled) { + updateRoute(`/dev_tools/${currentDevTool.id}`); + } + }} + > + {currentDevTool.title} + + + ))} + +
{ + if ( + element && + (mountedTool.current === null || + mountedTool.current.devTool !== activeDevTool || + mountedTool.current.mountpoint !== element) + ) { + if (mountedTool.current) { + mountedTool.current.unmountHandler(); + } + const unmountHandler = await activeDevTool.mount(appMountContext, { + element, + appBasePath: '', + }); + mountedTool.current = { + devTool: activeDevTool, + mountpoint: element, + unmountHandler, + }; + } + }} + /> +
+ ); +} + +function redirectOnMissingCapabilities(appMountContext: AppMountContext) { + if (!appMountContext.core.application.capabilities.dev_tools.show) { + window.location.hash = '/home'; + return true; + } + return false; +} + +function setBadge(appMountContext: AppMountContext) { + if (appMountContext.core.application.capabilities.dev_tools.save) { + return; + } + appMountContext.core.chrome.setBadge({ + text: i18n.translate('kbn.devTools.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('kbn.devTools.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save', + }), + iconType: 'glasses', + }); +} + +function setBreadcrumbs(appMountContext: AppMountContext) { + appMountContext.core.chrome.setBreadcrumbs([ + { + text: i18n.translate('kbn.devTools.k7BreadcrumbsDevToolsLabel', { + defaultMessage: 'Dev Tools', + }), + href: '#/dev_tools', + }, + ]); +} + +export function renderApp( + element: HTMLElement, + appMountContext: AppMountContext, + basePath: string, + devTools: readonly DevTool[] +) { + if (redirectOnMissingCapabilities(appMountContext)) { + return () => {}; + } + setBadge(appMountContext); + setBreadcrumbs(appMountContext); + ReactDOM.render( + + + + {devTools.map(devTool => ( + ( + + )} + /> + ))} + + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +} diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js b/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js deleted file mode 100644 index 25c7b945b9dfb4..00000000000000 --- a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/__tests__/hide_empty_tools.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { hideEmptyDevTools } from '../hide_empty_tools'; -import { npStart } from 'ui/new_platform'; - -describe('hide dev tools', function () { - let updateNavLink; - - function PrivateWithoutTools() { - return []; - } - - function PrivateWithTools() { - return ['tool1', 'tool2']; - } - - function isHidden() { - return updateNavLink.calledWith('kibana:dev_tools', { hidden: true }); - } - - beforeEach(function () { - const coreNavLinks = npStart.core.chrome.navLinks; - updateNavLink = sinon.spy(coreNavLinks, 'update'); - }); - - it('should hide the app if there are no dev tools', function () { - hideEmptyDevTools(PrivateWithTools); - expect(isHidden()).to.be(false); - }); - - it('should not hide the app if there are tools', function () { - hideEmptyDevTools(PrivateWithoutTools); - expect(isHidden()).to.be(true); - }); - - afterEach(function () { - updateNavLink.restore(); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/index.js b/src/legacy/core_plugins/kibana/public/dev_tools/index.js deleted file mode 100644 index e36e75f6837ab8..00000000000000 --- a/src/legacy/core_plugins/kibana/public/dev_tools/index.js +++ /dev/null @@ -1,77 +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 uiRoutes from 'ui/routes'; -import { i18n } from '@kbn/i18n'; -import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; -import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -import 'ui/directives/kbn_href'; -import './directives/dev_tools_app'; - -uiRoutes - .when('/dev_tools', { - resolve: { - redirect(Private, kbnUrl) { - const items = Private(DevToolsRegistryProvider).inOrder; - kbnUrl.redirect(items[0].url.substring(1)); - } - } - }); - -uiRoutes.defaults(/^\/dev_tools(\/|$)/, { - badge: uiCapabilities => { - if (uiCapabilities.dev_tools.save) { - return undefined; - } - - return { - text: i18n.translate('kbn.devTools.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('kbn.devTools.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save', - }), - iconType: 'glasses' - }; - }, - k7Breadcrumbs: () => [ - { - text: i18n.translate('kbn.devTools.k7BreadcrumbsDevToolsLabel', { - defaultMessage: 'Dev Tools' - }), - href: '#/dev_tools' - } - ] -}); - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'console', - title: i18n.translate('kbn.devTools.consoleTitle', { - defaultMessage: 'Console' - }), - description: i18n.translate('kbn.devTools.consoleDescription', { - defaultMessage: 'Skip cURL and use this JSON interface to work with your data directly.' - }), - icon: 'consoleApp', - path: '/app/kibana#/dev_tools/console', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN - }; -}); diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/index.ts b/src/legacy/core_plugins/kibana/public/dev_tools/index.ts new file mode 100644 index 00000000000000..74708e36a98aac --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dev_tools/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { npSetup, npStart } from 'ui/new_platform'; + +import { DevToolsPlugin } from './plugin'; +import { localApplicationService } from '../local_application_service'; + +const instance = new DevToolsPlugin(); + +instance.setup(npSetup.core, { + __LEGACY: { + localApplicationService, + }, +}); +instance.start(npStart.core, { + newPlatformDevTools: npStart.plugins.devTools, +}); diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/partials/dev_tools_app.html b/src/legacy/core_plugins/kibana/public/dev_tools/partials/dev_tools_app.html deleted file mode 100644 index 6c076092c76d5d..00000000000000 --- a/src/legacy/core_plugins/kibana/public/dev_tools/partials/dev_tools_app.html +++ /dev/null @@ -1,22 +0,0 @@ -
- - -
-
diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts b/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts new file mode 100644 index 00000000000000..ec9af1a6acd929 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dev_tools/plugin.ts @@ -0,0 +1,71 @@ +/* + * 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. + */ + +// This import makes sure dev tools are registered before the app is. +import 'uiExports/devTools'; + +import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; + +import { LocalApplicationService } from '../local_application_service'; +import { DevTool, DevToolsStart } from '../../../../../plugins/dev_tools/public'; + +export interface DevToolsPluginSetupDependencies { + __LEGACY: { + localApplicationService: LocalApplicationService; + }; +} + +export interface DevToolsPluginStartDependencies { + newPlatformDevTools: DevToolsStart; +} + +export class DevToolsPlugin implements Plugin { + private getSortedDevTools: (() => readonly DevTool[]) | null = null; + + public setup( + core: CoreSetup, + { __LEGACY: { localApplicationService } }: DevToolsPluginSetupDependencies + ) { + localApplicationService.register({ + id: 'dev_tools', + title: 'Dev Tools', + mount: async (appMountContext, params) => { + if (!this.getSortedDevTools) { + throw new Error('not started yet'); + } + const { renderApp } = await import('./application'); + return renderApp( + params.element, + appMountContext, + params.appBasePath, + this.getSortedDevTools() + ); + }, + }); + } + + public start(core: CoreStart, { newPlatformDevTools }: DevToolsPluginStartDependencies) { + this.getSortedDevTools = newPlatformDevTools.getSortedDevTools; + if (this.getSortedDevTools().length === 0) { + core.chrome.navLinks.update('kibana:dev_tools', { + hidden: true, + }); + } + } +} diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/_stubs.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/_stubs.js index f472ff9250eb5b..b3d37083b37f73 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/_stubs.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/api/__tests__/_stubs.js @@ -26,7 +26,8 @@ export function createIndexPatternsStub() { get: sinon.spy(indexPatternId => Promise.resolve({ id: indexPatternId, - isTimeNanosBased: () => false + isTimeNanosBased: () => false, + popularizeField: () => {}, }) ), }; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js index b136b03bd500bf..5a445a65939eda 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js @@ -19,32 +19,33 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import sinon from 'sinon'; import { getServices } from '../../../../kibana_services'; import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; - +import { createIndexPatternsStub } from '../../api/__tests__/_stubs'; +import { npStart } from 'ui/new_platform'; describe('context app', function () { beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.module(function createServiceStubs($provide) { + $provide.value('indexPatterns', createIndexPatternsStub()); + })); + describe('action addFilter', function () { - let filterManagerStub; let addFilter; beforeEach(ngMock.inject(function createPrivateStubs(Private) { - filterManagerStub = createQueryFilterStub(); - Private.stub(getServices().FilterBarQueryFilterProvider, filterManagerStub); - + Private.stub(getServices().FilterBarQueryFilterProvider); addFilter = Private(QueryParameterActionsProvider).addFilter; })); it('should pass the given arguments to the filterManager', function () { const state = createStateStub(); + const filterManagerAddStub = npStart.plugins.data.query.filterManager.addFilters; addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); - const filterManagerAddStub = filterManagerStub.addFilters; //get the generated filter const generatedFilter = filterManagerAddStub.firstCall.args[0][0]; const queryKeys = Object.keys(generatedFilter.query.match_phrase); @@ -55,20 +56,12 @@ describe('context app', function () { it('should pass the index pattern id to the filterManager', function () { const state = createStateStub(); + const filterManagerAddStub = npStart.plugins.data.query.filterManager.addFilters; addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); - const filterManagerAddStub = filterManagerStub.addFilters; const generatedFilter = filterManagerAddStub.firstCall.args[0][0]; - expect(filterManagerAddStub.calledOnce).to.be(true); expect(generatedFilter.meta.index).to.eql('INDEX_PATTERN_ID'); }); }); }); - -function createQueryFilterStub() { - return { - addFilters: sinon.stub(), - getAppFilters: sinon.stub(), - }; -} diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/actions.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/actions.js index 9f7b180e8fe7db..10fe6c0e2eda1f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/actions.js @@ -18,7 +18,8 @@ */ import _ from 'lodash'; -import { getServices, getFilterGenerator } from '../../../kibana_services'; +import { generateFilters } from '../../../../../../../../plugins/data/public'; +import { npStart } from 'ui/new_platform'; import { MAX_CONTEXT_SIZE, @@ -27,9 +28,8 @@ import { } from './constants'; -export function QueryParameterActionsProvider(indexPatterns, Private) { - const queryFilter = Private(getServices().FilterBarQueryFilterProvider); - const filterGen = getFilterGenerator(queryFilter); +export function QueryParameterActionsProvider(indexPatterns) { + const { filterManager } = npStart.plugins.data.query; const setPredecessorCount = (state) => (predecessorCount) => ( state.queryParameters.predecessorCount = clamp( @@ -55,13 +55,13 @@ export function QueryParameterActionsProvider(indexPatterns, Private) { ); const updateFilters = () => filters => { - queryFilter.setFilters(filters); + filterManager.setFilters(filters); }; const addFilter = (state) => async (field, values, operation) => { const indexPatternId = state.queryParameters.indexPatternId; - const newFilters = filterGen.generate(field, values, operation, indexPatternId); - queryFilter.addFilters(newFilters); + const newFilters = generateFilters(filterManager, field, values, operation, indexPatternId); + filterManager.addFilters(newFilters); const indexPattern = await indexPatterns.get(indexPatternId); indexPattern.popularizeField(field.name, 1); }; diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js index ed5049aa912e0a..8ee23bfb005a2a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js @@ -31,7 +31,6 @@ import './doc_table'; import { getSort } from './doc_table/lib/get_sort'; import { getSortForSearchSource } from './doc_table/lib/get_sort_for_search_source'; import * as columnActions from './doc_table/actions/columns'; -import * as filterActions from './doc_table/actions/filter'; import indexTemplate from './discover.html'; import { showOpenSearchPanel } from '../top_nav/show_open_search_panel'; @@ -41,7 +40,6 @@ import { getPainlessError } from './get_painless_error'; import { angular, buildVislibDimensions, - getFilterGenerator, getRequestInspectorStats, getResponseInspectorStats, getServices, @@ -57,7 +55,7 @@ import { subscribeWithScope, tabifyAggResponse, vislibSeriesResponseHandlerProvider, - VisProvider, + Vis, SavedObjectSaveModal, } from '../kibana_services'; @@ -76,7 +74,7 @@ const { import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; import { extractTimeFilter, changeTimeFilter } from '../../../../data/public'; import { start as data } from '../../../../data/public/legacy'; - +import { generateFilters } from '../../../../../../plugins/data/public'; const { savedQueryService } = data.search.services; @@ -190,13 +188,11 @@ function discoverController( localStorage, uiCapabilities ) { - const Vis = Private(VisProvider); const responseHandler = vislibSeriesResponseHandlerProvider().handler; const getUnhashableStates = Private(getUnhashableStatesProvider); const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider); const queryFilter = Private(FilterBarQueryFilterProvider); - const filterGen = getFilterGenerator(queryFilter); const inspectorAdapters = { requests: new RequestAdapter() @@ -901,7 +897,8 @@ function discoverController( // TODO: On array fields, negating does not negate the combination, rather all terms $scope.filterQuery = function (field, values, operation) { $scope.indexPattern.popularizeField(field, 1); - filterActions.addFilter(field, values, operation, $scope.indexPattern.id, $scope.state, filterGen); + const newFilters = generateFilters(queryFilter, field, values, operation, $scope.indexPattern.id); + return queryFilter.addFilters(newFilters); }; $scope.addColumn = function addColumn(columnName) { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/actions/filter.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/actions/filter.js deleted file mode 100644 index 1f5db791469b95..00000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/actions/filter.js +++ /dev/null @@ -1,66 +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 { addFilter } from '../../actions/filter'; -import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import NoDigestPromises from 'test_utils/no_digest_promises'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; - -function getFilterGeneratorStub() { - return { - add: sinon.stub() - }; -} - -describe('doc table filter actions', function () { - NoDigestPromises.activateForSuite(); - - let filterGen; - let indexPattern; - - beforeEach(ngMock.module( - 'kibana', - function ($provide) { - $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); - } - )); - - beforeEach(ngMock.inject(function (Private) { - indexPattern = Private(StubbedLogstashIndexPatternProvider); - filterGen = getFilterGeneratorStub(); - })); - - describe('add', function () { - - it('should defer to the FilterManager when dealing with a lucene query', function () { - const state = { - query: { query: 'foo', language: 'lucene' } - }; - const args = ['foo', ['bar'], '+', indexPattern, ]; - addFilter('foo', ['bar'], '+', indexPattern, state, filterGen); - expect(filterGen.add.calledOnce).to.be(true); - expect(filterGen.add.calledWith(...args)).to.be(true); - }); - - }); - - -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx index badfbb4b14a4c1..5054f7b4bdad16 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.test.tsx @@ -121,7 +121,7 @@ describe('DiscoverFieldSearch', () => { // @ts-ignore (aggregtableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); - missingSwitch.simulate('change', { target: { value: false } }); + missingSwitch.simulate('click'); expect(onChange).toBeCalledTimes(2); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx index 3d93487d9e6ccd..d5f6b63d121992 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field_search.tsx @@ -29,6 +29,7 @@ import { EuiPopoverTitle, EuiSelect, EuiSwitch, + EuiSwitchEvent, EuiForm, EuiFormRow, EuiButtonGroup, @@ -154,7 +155,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { setActiveFiltersCount(activeFiltersCount + diff); }; - const handleMissingChange = (e: React.ChangeEvent) => { + const handleMissingChange = (e: EuiSwitchEvent) => { const missingValue = e.target.checked; handleValueChange('missing', missingValue); }; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index 732fb6d2e4e709..c575465a377e24 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -25,10 +25,12 @@ import { npStart } from 'ui/new_platform'; import { esFilters, TimeRange, + FilterManager, onlyDisabledFiltersChanged, + generateFilters, getTime, + Query, } from '../../../../../../plugins/data/public'; -import { Query } from '../../../../data/public'; import { APPLY_FILTER_TRIGGER, Container, @@ -43,7 +45,6 @@ import { getSortForSearchSource } from '../angular/doc_table/lib/get_sort_for_se import { Adapters, angular, - getFilterGenerator, getRequestInspectorStats, getResponseInspectorStats, getServices, @@ -72,18 +73,6 @@ interface SearchScope extends ng.IScope { isLoading?: boolean; } -export interface FilterManager { - generate: ( - field: { - name: string; - scripted: boolean; - }, - values: string | string[], - operation: string, - index: number - ) => esFilters.Filter[]; -} - interface SearchEmbeddableConfig { $rootScope: ng.IRootScopeService; $compile: ng.ICompileService; @@ -107,7 +96,7 @@ export class SearchEmbeddable extends Embeddable private autoRefreshFetchSubscription?: Subscription; private subscription?: Subscription; public readonly type = SEARCH_EMBEDDABLE_TYPE; - private filterGen: FilterManager; + private filterManager: FilterManager; private abortController?: AbortController; private prevTimeRange?: TimeRange; @@ -134,7 +123,7 @@ export class SearchEmbeddable extends Embeddable parent ); - this.filterGen = getFilterGenerator(queryFilter); + this.filterManager = queryFilter as FilterManager; this.savedSearch = savedSearch; this.$rootScope = $rootScope; this.$compile = $compile; @@ -251,7 +240,7 @@ export class SearchEmbeddable extends Embeddable }; searchScope.filter = async (field, value, operator) => { - let filters = this.filterGen.generate(field, value, operator, indexPattern.id); + let filters = generateFilters(this.filterManager, field, value, operator, indexPattern.id); filters = filters.map(filter => ({ ...filter, $state: { store: esFilters.FilterStateStore.APP_STATE }, diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts index 5473ec0e7b8b4d..2d940ad8cba98a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts @@ -17,13 +17,11 @@ * under the License. */ -import { TimeRange } from 'src/plugins/data/public'; -import { Query } from 'src/legacy/core_plugins/data/public'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from 'src/plugins/embeddable/public'; import { StaticIndexPattern } from '../kibana_services'; import { SavedSearch } from '../types'; import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; -import { esFilters } from '../../../../../../plugins/data/public'; +import { esFilters, TimeRange, Query } from '../../../../../../plugins/data/public'; export interface SearchInput extends EmbeddableInput { timeRange: TimeRange; diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index b78d05e68acade..d0eb115e32676e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -84,8 +84,6 @@ export { angular }; export { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/build_pipeline'; // @ts-ignore export { callAfterBindingsWorkaround } from 'ui/compat'; -// @ts-ignore -export { getFilterGenerator } from 'ui/filter_manager'; export { getRequestInspectorStats, getResponseInspectorStats, @@ -114,7 +112,7 @@ export { tabifyAggResponse } from 'ui/agg_response/tabify'; export { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib'; // EXPORT types -export { VisProvider } from 'ui/vis'; +export { Vis } from 'ui/vis'; export { StaticIndexPattern, IndexPatterns, IndexPattern, FieldType } from 'ui/index_patterns'; export { SearchSource } from 'ui/courier'; export { ElasticSearchHit } from 'ui/registry/doc_views_types'; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index fe741a357cbfe7..c5b9d86b57aaea 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -37,7 +37,6 @@ import 'uiExports/navbarExtensions'; import 'uiExports/contextMenuActions'; import 'uiExports/managementSections'; import 'uiExports/indexManagement'; -import 'uiExports/devTools'; import 'uiExports/docViews'; import 'uiExports/embeddableFactories'; import 'uiExports/embeddableActions'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts index a6ed2e36839f4b..4ecc3583e76cea 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts @@ -62,7 +62,10 @@ describe('extractExportDetails', () => { [ [ objLine('1', 'index-pattern'), - detailsLine(1, [{ id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }]), + detailsLine(1, [ + { id: '2', type: 'index-pattern' }, + { id: '3', type: 'index-pattern' }, + ]), ].join(''), ], { @@ -75,7 +78,10 @@ describe('extractExportDetails', () => { expect(result).toEqual({ exportedCount: 1, missingRefCount: 2, - missingReferences: [{ id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }], + missingReferences: [ + { id: '2', type: 'index-pattern' }, + { id: '3', type: 'index-pattern' }, + ], }); }); diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 60cf7c7ec19288..0b75c6ffa1ffbb 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -33,11 +33,12 @@ import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from '../../../../expressions/public/np_ready/public/types'; import { start as expressions } from '../../../../expressions/public/legacy'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; -import { Query } from '../../../../data/public'; import { TimeRange, + Query, onlyDisabledFiltersChanged, esFilters, + mapAndFlattenFilters, } from '../../../../../../plugins/data/public'; import { EmbeddableInput, @@ -47,7 +48,6 @@ import { APPLY_FILTER_TRIGGER, } from '../../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../../plugins/kibana_utils/public'; -import { mapAndFlattenFilters } from '../../../../../../plugins/data/public'; const getKeys = (o: T): Array => Object.keys(o) as Array; diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index f8adaed0bf584a..aec80b8d13551a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -25,7 +25,7 @@ * NOTE: It's a type of SavedObject, but specific to visualizations. */ -import { VisProvider } from 'ui/vis'; +import { Vis } from 'ui/vis'; import { uiModules } from 'ui/modules'; import { updateOldState } from 'ui/vis/vis_update_state'; import { VisualizeConstants } from '../visualize_constants'; @@ -39,7 +39,6 @@ import { uiModules .get('app/visualize') .factory('SavedVis', function (Promise, savedSearches, Private) { - const Vis = Private(VisProvider); const SavedObject = Private(SavedObjectProvider); createLegacyClass(SavedVis).inherits(SavedObject); function SavedVis(opts) { diff --git a/src/legacy/core_plugins/newsfeed/constants.ts b/src/legacy/core_plugins/newsfeed/constants.ts new file mode 100644 index 00000000000000..55a0c51c2ac653 --- /dev/null +++ b/src/legacy/core_plugins/newsfeed/constants.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const PLUGIN_ID = 'newsfeed'; +export const DEFAULT_SERVICE_URLROOT = 'https://feeds.elastic.co'; +export const DEV_SERVICE_URLROOT = 'https://feeds-staging.elastic.co'; +export const DEFAULT_SERVICE_PATH = '/kibana/v{VERSION}.json'; diff --git a/src/legacy/core_plugins/newsfeed/index.ts b/src/legacy/core_plugins/newsfeed/index.ts new file mode 100644 index 00000000000000..cf8852be09a1ef --- /dev/null +++ b/src/legacy/core_plugins/newsfeed/index.ts @@ -0,0 +1,71 @@ +/* + * 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 { resolve } from 'path'; +import { LegacyPluginApi, LegacyPluginSpec, ArrayOrItem } from 'src/legacy/plugin_discovery/types'; +import { Legacy } from 'kibana'; +import { NewsfeedPluginInjectedConfig } from '../../../plugins/newsfeed/types'; +import { + PLUGIN_ID, + DEFAULT_SERVICE_URLROOT, + DEV_SERVICE_URLROOT, + DEFAULT_SERVICE_PATH, +} from './constants'; + +// eslint-disable-next-line import/no-default-export +export default function(kibana: LegacyPluginApi): ArrayOrItem { + const pluginSpec: Legacy.PluginSpecOptions = { + id: PLUGIN_ID, + config(Joi: any) { + // NewsfeedPluginInjectedConfig in Joi form + return Joi.object({ + enabled: Joi.boolean().default(true), + service: Joi.object({ + pathTemplate: Joi.string().default(DEFAULT_SERVICE_PATH), + urlRoot: Joi.when('$prod', { + is: true, + then: Joi.string().default(DEFAULT_SERVICE_URLROOT), + otherwise: Joi.string().default(DEV_SERVICE_URLROOT), + }), + }).default(), + defaultLanguage: Joi.string().default('en'), + mainInterval: Joi.number().default(120 * 1000), // (2min) How often to retry failed fetches, and/or check if newsfeed items need to be refreshed from remote + fetchInterval: Joi.number().default(86400 * 1000), // (1day) How often to fetch remote and reset the last fetched time + }).default(); + }, + uiExports: { + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + injectDefaultVars(server): NewsfeedPluginInjectedConfig { + const config = server.config(); + return { + newsfeed: { + service: { + pathTemplate: config.get('newsfeed.service.pathTemplate') as string, + urlRoot: config.get('newsfeed.service.urlRoot') as string, + }, + defaultLanguage: config.get('newsfeed.defaultLanguage') as string, + mainInterval: config.get('newsfeed.mainInterval') as number, + fetchInterval: config.get('newsfeed.fetchInterval') as number, + }, + }; + }, + }, + }; + return new kibana.Plugin(pluginSpec); +} diff --git a/src/legacy/core_plugins/newsfeed/package.json b/src/legacy/core_plugins/newsfeed/package.json new file mode 100644 index 00000000000000..d4d753f32b0f96 --- /dev/null +++ b/src/legacy/core_plugins/newsfeed/package.json @@ -0,0 +1,4 @@ +{ + "name": "newsfeed", + "version": "kibana" +} diff --git a/src/legacy/core_plugins/newsfeed/public/index.scss b/src/legacy/core_plugins/newsfeed/public/index.scss new file mode 100644 index 00000000000000..a77132379041cb --- /dev/null +++ b/src/legacy/core_plugins/newsfeed/public/index.scss @@ -0,0 +1,3 @@ +@import 'src/legacy/ui/public/styles/styling_constants'; + +@import './np_ready/components/header_alert/_index'; diff --git a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss b/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss new file mode 100644 index 00000000000000..e25dbd25daaf5b --- /dev/null +++ b/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/_index.scss @@ -0,0 +1,27 @@ +@import '@elastic/eui/src/components/header/variables'; + +.kbnNews__flyout { + top: $euiHeaderChildSize + 1px; + height: calc(100% - #{$euiHeaderChildSize}); +} + +.kbnNewsFeed__headerAlert.euiHeaderAlert { + margin-bottom: $euiSizeL; + padding: 0 $euiSizeS $euiSizeL; + border-bottom: $euiBorderThin; + border-top: none; + + .euiHeaderAlert__title { + @include euiTitle('xs'); + margin-bottom: $euiSizeS; + } + + .euiHeaderAlert__text { + @include euiFontSizeS; + margin-bottom: $euiSize; + } + + .euiHeaderAlert__action { + @include euiFontSizeS; + } +} diff --git a/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx b/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx new file mode 100644 index 00000000000000..c3c3e4144fca8a --- /dev/null +++ b/src/legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import { EuiFlexGroup, EuiFlexItem, EuiI18n } from '@elastic/eui'; + +interface IEuiHeaderAlertProps { + action: JSX.Element; + className?: string; + date: string; + text: string; + title: string; + badge?: JSX.Element; + rest?: string[]; +} + +export const EuiHeaderAlert = ({ + action, + className, + date, + text, + title, + badge, + ...rest +}: IEuiHeaderAlertProps) => { + const classes = classNames('euiHeaderAlert', 'kbnNewsFeed__headerAlert', className); + + const badgeContent = badge || null; + + return ( + + {(dismiss: any) => ( +
+ + +
{date}
+
+ {badgeContent} +
+ +
{title}
+
{text}
+
{action}
+
+ )} +
+ ); +}; + +EuiHeaderAlert.propTypes = { + action: PropTypes.node, + className: PropTypes.string, + date: PropTypes.node.isRequired, + text: PropTypes.node, + title: PropTypes.node.isRequired, + badge: PropTypes.node, +}; diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js index 9bfa413257967c..b57fbd637f0b72 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -22,7 +22,7 @@ import ngMock from 'ng_mock'; import _ from 'lodash'; import ChoroplethLayer from '../choropleth_layer'; import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern'; -import * as visModule from 'ui/vis'; +import { Vis } from 'ui/vis'; import { ImageComparator } from 'test_utils/image_comparator'; import worldJson from './world.json'; import EMS_CATALOGUE from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json'; @@ -50,7 +50,6 @@ const PIXEL_DIFF = 96; describe('RegionMapsVisualizationTests', function () { let domNode; let RegionMapsVisualization; - let Vis; let indexPattern; let vis; let dependencies; @@ -113,7 +112,6 @@ describe('RegionMapsVisualizationTests', function () { visualizationsSetup.types.registerVisualization(() => createRegionMapTypeDefinition(dependencies)); } - Vis = Private(visModule.VisProvider); RegionMapsVisualization = createRegionMapVisualization(dependencies); indexPattern = Private(LogstashIndexPatternStubProvider); diff --git a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx index 9749c7fa8e2f9a..8306b3274a9144 100644 --- a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx +++ b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx @@ -82,7 +82,10 @@ function RegionMapOptions(props: RegionMapOptionsProps) { const setField = useCallback( (paramName: 'selectedJoinField', value: FileLayerField['name']) => { if (stateParams.selectedLayer) { - setValue(paramName, stateParams.selectedLayer.fields.find(f => f.name === value)); + setValue( + paramName, + stateParams.selectedLayer.fields.find(f => f.name === value) + ); } }, [setValue, stateParams.selectedLayer] diff --git a/src/legacy/core_plugins/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts index d7f34d1f8f8eb0..7b0c62276f2902 100644 --- a/src/legacy/core_plugins/telemetry/common/constants.ts +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -59,6 +59,12 @@ export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-state */ export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization'; +/** + * The type name used to publish telemetry plugin stats. + * @type {string} + */ +export const TELEMETRY_STATS_TYPE = 'telemetry'; + /** * UI metric usage type * @type {string} diff --git a/src/legacy/core_plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts index 50a25423b5eb82..9993f2dbf0b861 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -27,12 +27,13 @@ import { i18n } from '@kbn/i18n'; import mappings from './mappings.json'; import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants'; import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated'; -import { telemetryPlugin, getTelemetryOptIn } from './server'; +import { telemetryPlugin, replaceTelemetryInjectedVars, FetcherTask } from './server'; import { createLocalizationUsageCollector, createTelemetryUsageCollector, createUiMetricUsageCollector, + createTelemetryPluginUsageCollector, } from './server/collectors'; const ENDPOINT_VERSION = 'v2'; @@ -46,20 +47,15 @@ const telemetry = (kibana: any) => { config(Joi: typeof JoiNamespace) { return Joi.object({ enabled: Joi.boolean().default(true), + allowChangingOptInStatus: Joi.boolean().default(true), optIn: Joi.when('allowChangingOptInStatus', { is: false, - then: Joi.valid(true), - otherwise: Joi.boolean() - .allow(null) - .default(null), + then: Joi.valid(true).default(true), + otherwise: Joi.boolean().default(true), }), - allowChangingOptInStatus: Joi.boolean().default(true), // `config` is used internally and not intended to be set config: Joi.string().default(Joi.ref('$defaultConfigPath')), banner: Joi.boolean().default(true), - lastVersionChecked: Joi.string() - .allow('') - .default(''), url: Joi.when('$dev', { is: true, then: Joi.string().default( @@ -69,6 +65,18 @@ const telemetry = (kibana: any) => { `https://telemetry.elastic.co/xpack/${ENDPOINT_VERSION}/send` ), }), + optInStatusUrl: Joi.when('$dev', { + is: true, + then: Joi.string().default( + `https://telemetry-staging.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send` + ), + otherwise: Joi.string().default( + `https://telemetry.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send` + ), + }), + sendUsageFrom: Joi.string() + .allow(['server', 'browser']) + .default('browser'), }).default(); }, uiExports: { @@ -89,30 +97,8 @@ const telemetry = (kibana: any) => { }, }, async replaceInjectedVars(originalInjectedVars: any, request: any) { - const config = request.server.config(); - const optIn = config.get('telemetry.optIn'); - const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); - const currentKibanaVersion = getCurrentKibanaVersion(request.server); - let telemetryOptedIn: boolean | null; - - if (typeof optIn === 'boolean' && !allowChangingOptInStatus) { - // When not allowed to change optIn status and an optIn value is set, we'll overwrite with that - telemetryOptedIn = optIn; - } else { - telemetryOptedIn = await getTelemetryOptIn({ - request, - currentKibanaVersion, - }); - if (telemetryOptedIn === null) { - // In the senario there's no value set in telemetryOptedIn, we'll return optIn value - telemetryOptedIn = optIn; - } - } - - return { - ...originalInjectedVars, - telemetryOptedIn, - }; + const telemetryInjectedVars = await replaceTelemetryInjectedVars(request); + return Object.assign({}, originalInjectedVars, telemetryInjectedVars); }, injectDefaultVars(server: Server) { const config = server.config(); @@ -123,17 +109,23 @@ const telemetry = (kibana: any) => { config.get('telemetry.allowChangingOptInStatus') !== false && getXpackConfigWithDeprecated(config, 'telemetry.banner'), telemetryOptedIn: config.get('telemetry.optIn'), + telemetryOptInStatusUrl: config.get('telemetry.optInStatusUrl'), allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'), + telemetrySendUsageFrom: config.get('telemetry.sendUsageFrom'), }; }, hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'], mappings, }, - async init(server: Server) { + postInit(server: Server) { + const fetcherTask = new FetcherTask(server); + fetcherTask.start(); + }, + init(server: Server) { const initializerContext = { env: { packageInfo: { - version: getCurrentKibanaVersion(server), + version: server.config().get('pkg.version'), }, }, config: { @@ -156,9 +148,9 @@ const telemetry = (kibana: any) => { log: server.log, } as any) as CoreSetup; - await telemetryPlugin(initializerContext).setup(coreSetup); - + telemetryPlugin(initializerContext).setup(coreSetup); // register collectors + server.usage.collectorSet.register(createTelemetryPluginUsageCollector(server)); server.usage.collectorSet.register(createLocalizationUsageCollector(server)); server.usage.collectorSet.register(createTelemetryUsageCollector(server)); server.usage.collectorSet.register(createUiMetricUsageCollector(server)); @@ -168,7 +160,3 @@ const telemetry = (kibana: any) => { // eslint-disable-next-line import/no-default-export export default telemetry; - -function getCurrentKibanaVersion(server: Server): string { - return server.config().get('pkg.version'); -} diff --git a/src/legacy/core_plugins/telemetry/mappings.json b/src/legacy/core_plugins/telemetry/mappings.json index 1245ef88f58929..95c6ebfc7dc793 100644 --- a/src/legacy/core_plugins/telemetry/mappings.json +++ b/src/legacy/core_plugins/telemetry/mappings.json @@ -4,7 +4,15 @@ "enabled": { "type": "boolean" }, + "sendUsageFrom": { + "ignore_above": 256, + "type": "keyword" + }, + "lastReported": { + "type": "date" + }, "lastVersionChecked": { + "ignore_above": 256, "type": "keyword" } } diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap index b96313fd700ac5..a7f8d72e016f87 100644 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap +++ b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap @@ -34,7 +34,8 @@ exports[`TelemetryForm renders as expected when allows to change optIn status 1` save={[Function]} setting={ Object { - "defVal": false, + "ariaName": "Provide usage statistics", + "defVal": true, "description":

Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic. diff --git a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js index 80eb2da59c47e8..6c6ace71af4d09 100644 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js +++ b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js @@ -33,6 +33,7 @@ import { getConfigTelemetryDesc, PRIVACY_STATEMENT_URL } from '../../common/cons import { OptInExampleFlyout } from './opt_in_details_component'; import { Field } from 'ui/management'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data']; @@ -116,7 +117,8 @@ export class TelemetryForm extends Component { type: 'boolean', value: telemetryOptInProvider.getOptIn() || false, description: this.renderDescription(), - defVal: false, + defVal: true, + ariaName: i18n.translate('telemetry.provideUsageStatisticsLabel', { defaultMessage: 'Provide usage statistics' }) }} save={this.toggleOptIn} clear={this.toggleOptIn} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts index 364871380a529c..1930d65d5c09b9 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts +++ b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts @@ -25,13 +25,21 @@ import { isUnauthenticated } from '../services'; import { Telemetry } from './telemetry'; // @ts-ignore import { fetchTelemetry } from './fetch_telemetry'; +// @ts-ignore +import { isOptInHandleOldSettings } from './welcome_banner/handle_old_settings'; +import { TelemetryOptInProvider } from '../services'; function telemetryInit($injector: any) { const $http = $injector.get('$http'); + const Private = $injector.get('Private'); + const config = $injector.get('config'); + const telemetryOptInProvider = Private(TelemetryOptInProvider); const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); + const telemetryOptedIn = isOptInHandleOldSettings(config, telemetryOptInProvider); + const sendUsageFrom = npStart.core.injectedMetadata.getInjectedVar('telemetrySendUsageFrom'); - if (telemetryEnabled) { + if (telemetryEnabled && telemetryOptedIn && sendUsageFrom === 'browser') { // no telemetry for non-logged in users if (isUnauthenticated()) { return; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js index 6e9a9fc8443ba0..54557f100f4aa4 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js @@ -54,7 +54,7 @@ const getTelemetryOptInProvider = ({ simulateFailure = false, simulateError = fa addBasePath: (url) => url }; - const provider = new TelemetryOptInProvider(injector, chrome); + const provider = new TelemetryOptInProvider(injector, chrome, false); if (simulateError) { provider.setOptIn = () => Promise.reject('unhandled error'); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js index 31091e19520533..4f0f2983477e07 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js @@ -27,8 +27,9 @@ import { CONFIG_TELEMETRY } from '../../../common/constants'; * @param {Object} config The advanced settings config object. * @return {Boolean} {@code true} if the banner should still be displayed. {@code false} if the banner should not be displayed. */ +const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport'; + export async function handleOldSettings(config, telemetryOptInProvider) { - const CONFIG_ALLOW_REPORT = 'xPackMonitoring:allowReport'; const CONFIG_SHOW_BANNER = 'xPackMonitoring:showBanner'; const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null); const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null); @@ -62,3 +63,24 @@ export async function handleOldSettings(config, telemetryOptInProvider) { return true; } + + +export async function isOptInHandleOldSettings(config, telemetryOptInProvider) { + const currentOptInSettting = telemetryOptInProvider.getOptIn(); + + if (typeof currentOptInSettting === 'boolean') { + return currentOptInSettting; + } + + const oldTelemetrySetting = config.get(CONFIG_TELEMETRY, null); + if (typeof oldTelemetrySetting === 'boolean') { + return oldTelemetrySetting; + } + + const oldAllowReportSetting = config.get(CONFIG_ALLOW_REPORT, null); + if (typeof oldAllowReportSetting === 'boolean') { + return oldAllowReportSetting; + } + + return null; +} diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js index f26ca0ca0e3c51..d78a4a3e923623 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js @@ -49,7 +49,7 @@ const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) => } }; - return new TelemetryOptInProvider($injector, chrome); + return new TelemetryOptInProvider($injector, chrome, false); }; describe('handle_old_settings', () => { diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js index 26f14fc87d937c..b0ebb9e7382f64 100644 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js @@ -48,7 +48,7 @@ describe('TelemetryOptInProvider', () => { } }; - const provider = new TelemetryOptInProvider(mockInjector, mockChrome); + const provider = new TelemetryOptInProvider(mockInjector, mockChrome, false); return { provider, mockHttp, diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts index 4d27bad352cd4d..9b32f88df1218a 100644 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts @@ -26,7 +26,36 @@ import { i18n } from '@kbn/i18n'; let bannerId: string | null = null; let currentOptInStatus = false; -export function TelemetryOptInProvider($injector: any, chrome: any) { +async function sendOptInStatus($injector: any, chrome: any, enabled: boolean) { + const telemetryOptInStatusUrl = npStart.core.injectedMetadata.getInjectedVar( + 'telemetryOptInStatusUrl' + ) as string; + const $http = $injector.get('$http'); + + try { + const optInStatus = await $http.post( + chrome.addBasePath('/api/telemetry/v2/clusters/_opt_in_stats'), + { + enabled, + unencrypted: false, + } + ); + + if (optInStatus.data && optInStatus.data.length) { + return await fetch(telemetryOptInStatusUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(optInStatus.data), + }); + } + } catch (err) { + // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. + // swallow any errors + } +} +export function TelemetryOptInProvider($injector: any, chrome: any, sendOptInStatusChange = true) { currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean; const allowChangingOptInStatus = npStart.core.injectedMetadata.getInjectedVar( 'allowChangingOptInStatus' @@ -41,11 +70,17 @@ export function TelemetryOptInProvider($injector: any, chrome: any) { bannerId = id; }, setOptIn: async (enabled: boolean) => { + if (!allowChangingOptInStatus) { + return; + } setCanTrackUiMetrics(enabled); const $http = $injector.get('$http'); try { await $http.post(chrome.addBasePath('/api/telemetry/v2/optIn'), { enabled }); + if (sendOptInStatusChange) { + await sendOptInStatus($injector, chrome, enabled); + } currentOptInStatus = enabled; } catch (error) { toastNotifications.addError(error, { diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts index fef0a9b0f9f40b..799d9f4ee9c8b6 100644 --- a/src/legacy/core_plugins/telemetry/server/collection_manager.ts +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -17,32 +17,187 @@ * under the License. */ -class TelemetryCollectionManager { - private getterMethod?: any; - private collectionTitle?: string; - private getterMethodPriority = 0; - - public setStatsGetter = (statsGetter: any, title: string, priority = 0) => { - if (priority >= this.getterMethodPriority) { - this.getterMethod = statsGetter; - this.collectionTitle = title; - this.getterMethodPriority = priority; +import { encryptTelemetry } from './collectors'; +import { CallCluster } from '../../elasticsearch'; + +export type EncryptedStatsGetterConfig = { unencrypted: false } & { + server: any; + start: string; + end: string; +}; + +export type UnencryptedStatsGetterConfig = { unencrypted: true } & { + req: any; + start: string; + end: string; +}; + +export interface ClusterDetails { + clusterUuid: string; +} + +export interface StatsCollectionConfig { + callCluster: CallCluster; + server: any; + start: string; + end: string; +} + +export type StatsGetterConfig = UnencryptedStatsGetterConfig | EncryptedStatsGetterConfig; +export type ClusterDetailsGetter = (config: StatsCollectionConfig) => Promise; +export type StatsGetter = ( + clustersDetails: ClusterDetails[], + config: StatsCollectionConfig +) => Promise; + +interface CollectionConfig { + title: string; + priority: number; + esCluster: string; + statsGetter: StatsGetter; + clusterDetailsGetter: ClusterDetailsGetter; +} +interface Collection { + statsGetter: StatsGetter; + clusterDetailsGetter: ClusterDetailsGetter; + esCluster: string; + title: string; +} + +export class TelemetryCollectionManager { + private usageGetterMethodPriority = -1; + private collections: Collection[] = []; + + public setCollection = (collectionConfig: CollectionConfig) => { + const { title, priority, esCluster, statsGetter, clusterDetailsGetter } = collectionConfig; + + if (typeof priority !== 'number') { + throw new Error('priority must be set.'); + } + if (priority === this.usageGetterMethodPriority) { + throw new Error(`A Usage Getter with the same priority is already set.`); } + + if (priority > this.usageGetterMethodPriority) { + if (!statsGetter) { + throw Error('Stats getter method not set.'); + } + if (!esCluster) { + throw Error('esCluster name must be set for the getCluster method.'); + } + if (!clusterDetailsGetter) { + throw Error('Cluser UUIds method is not set.'); + } + + this.collections.unshift({ + statsGetter, + clusterDetailsGetter, + esCluster, + title, + }); + this.usageGetterMethodPriority = priority; + } + }; + + private getStatsCollectionConfig = async ( + collection: Collection, + config: StatsGetterConfig + ): Promise => { + const { start, end } = config; + const server = config.unencrypted ? config.req.server : config.server; + const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster( + collection.esCluster + ); + const callCluster = config.unencrypted + ? (...args: any[]) => callWithRequest(config.req, ...args) + : callWithInternalUser; + + return { server, callCluster, start, end }; + }; + + private getOptInStatsForCollection = async ( + collection: Collection, + optInStatus: boolean, + statsCollectionConfig: StatsCollectionConfig + ) => { + const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig); + return clustersDetails.map(({ clusterUuid }) => ({ + cluster_uuid: clusterUuid, + opt_in_status: optInStatus, + })); }; - getCollectionTitle = () => { - return this.collectionTitle; + private getUsageForCollection = async ( + collection: Collection, + statsCollectionConfig: StatsCollectionConfig + ) => { + const clustersDetails = await collection.clusterDetailsGetter(statsCollectionConfig); + + if (clustersDetails.length === 0) { + // don't bother doing a further lookup, try next collection. + return; + } + + return await collection.statsGetter(clustersDetails, statsCollectionConfig); }; - public getStatsGetter = () => { - if (!this.getterMethod) { - throw Error('Stats getter method not set.'); + public getOptInStats = async (optInStatus: boolean, config: StatsGetterConfig) => { + for (const collection of this.collections) { + const statsCollectionConfig = await this.getStatsCollectionConfig(collection, config); + try { + const optInStats = await this.getOptInStatsForCollection( + collection, + optInStatus, + statsCollectionConfig + ); + if (optInStats && optInStats.length) { + statsCollectionConfig.server.log( + ['debug', 'telemetry', 'collection'], + `Got Opt In stats using ${collection.title} collection.` + ); + if (config.unencrypted) { + return optInStats; + } + const isDev = statsCollectionConfig.server.config().get('env.dev'); + return encryptTelemetry(optInStats, isDev); + } + } catch (err) { + statsCollectionConfig.server.log( + ['debu', 'telemetry', 'collection'], + `Failed to collect any opt in stats with registered collections.` + ); + // swallow error to try next collection; + } } - return { - getStats: this.getterMethod, - priority: this.getterMethodPriority, - title: this.collectionTitle, - }; + + return []; + }; + public getStats = async (config: StatsGetterConfig) => { + for (const collection of this.collections) { + const statsCollectionConfig = await this.getStatsCollectionConfig(collection, config); + try { + const usageData = await this.getUsageForCollection(collection, statsCollectionConfig); + if (usageData && usageData.length) { + statsCollectionConfig.server.log( + ['debug', 'telemetry', 'collection'], + `Got Usage using ${collection.title} collection.` + ); + if (config.unencrypted) { + return usageData; + } + const isDev = statsCollectionConfig.server.config().get('env.dev'); + return encryptTelemetry(usageData, isDev); + } + } catch (err) { + statsCollectionConfig.server.log( + ['debu', 'telemetry', 'collection'], + `Failed to collect any usage with registered collections.` + ); + // swallow error to try next collection; + } + } + + return []; }; } diff --git a/src/legacy/core_plugins/telemetry/server/collectors/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/index.ts index 0bc1d50fab1be6..f963ecec0477cf 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/index.ts @@ -21,3 +21,4 @@ export { encryptTelemetry } from './encryption'; export { createTelemetryUsageCollector } from './usage'; export { createUiMetricUsageCollector } from './ui_metric'; export { createLocalizationUsageCollector } from './localization'; +export { createTelemetryPluginUsageCollector } from './telemetry_plugin'; diff --git a/src/legacy/ui/public/registry/dev_tools.js b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts similarity index 83% rename from src/legacy/ui/public/registry/dev_tools.js rename to src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts index 1741f39f86375c..e96c47741f79c9 100644 --- a/src/legacy/ui/public/registry/dev_tools.js +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts @@ -17,11 +17,4 @@ * under the License. */ -import { uiRegistry } from './_registry'; - -export const DevToolsRegistryProvider = uiRegistry({ - name: 'devTools', - index: ['name'], - order: ['order'] -}); - +export { createTelemetryPluginUsageCollector } from './telemetry_plugin_collector'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts new file mode 100644 index 00000000000000..a172ba7dc69559 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/telemetry_plugin_collector.ts @@ -0,0 +1,75 @@ +/* + * 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 { TELEMETRY_STATS_TYPE } from '../../../common/constants'; +import { getTelemetrySavedObject, TelemetrySavedObject } from '../../telemetry_repository'; +import { getTelemetryOptIn, getTelemetrySendUsageFrom } from '../../telemetry_config'; +export interface TelemetryUsageStats { + opt_in_status?: boolean | null; + usage_fetcher?: 'browser' | 'server'; + last_reported?: number; +} + +export function createCollectorFetch(server: any) { + return async function fetchUsageStats(): Promise { + const config = server.config(); + const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom'); + const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); + const configTelemetryOptIn = config.get('telemetry.optIn'); + const currentKibanaVersion = config.get('pkg.version'); + + let telemetrySavedObject: TelemetrySavedObject = {}; + + try { + const { getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const internalRepository = getSavedObjectsRepository(callWithInternalUser); + telemetrySavedObject = await getTelemetrySavedObject(internalRepository); + } catch (err) { + // no-op + } + + return { + opt_in_status: getTelemetryOptIn({ + currentKibanaVersion, + telemetrySavedObject, + allowChangingOptInStatus, + configTelemetryOptIn, + }), + last_reported: telemetrySavedObject ? telemetrySavedObject.lastReported : undefined, + usage_fetcher: getTelemetrySendUsageFrom({ + telemetrySavedObject, + configTelemetrySendUsageFrom, + }), + }; + }; +} + +/* + * @param {Object} server + * @return {Object} kibana usage stats type collection object + */ +export function createTelemetryPluginUsageCollector(server: any) { + const { collectorSet } = server.usage; + return collectorSet.makeUsageCollector({ + type: TELEMETRY_STATS_TYPE, + isReady: () => true, + fetch: createCollectorFetch(server), + }); +} diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.ts index 6594c7f8e7a6f0..3b7a9355da746f 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.ts @@ -42,19 +42,16 @@ export function ensureDeepObject(obj: any): any { return obj.map(item => ensureDeepObject(item)); } - return Object.keys(obj).reduce( - (fullObject, propertyKey) => { - const propertyValue = obj[propertyKey]; - if (!propertyKey.includes(separator)) { - fullObject[propertyKey] = ensureDeepObject(propertyValue); - } else { - walk(fullObject, propertyKey.split(separator), propertyValue); - } + return Object.keys(obj).reduce((fullObject, propertyKey) => { + const propertyValue = obj[propertyKey]; + if (!propertyKey.includes(separator)) { + fullObject[propertyKey] = ensureDeepObject(propertyValue); + } else { + walk(fullObject, propertyKey.split(separator), propertyValue); + } - return fullObject; - }, - {} as any - ); + return fullObject; + }, {} as any); } function walk(obj: any, keys: string[], value: any) { diff --git a/src/legacy/core_plugins/telemetry/server/fetcher.ts b/src/legacy/core_plugins/telemetry/server/fetcher.ts new file mode 100644 index 00000000000000..9edd8457f2b89e --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/fetcher.ts @@ -0,0 +1,143 @@ +/* + * 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 moment from 'moment'; +// @ts-ignore +import fetch from 'node-fetch'; +import { telemetryCollectionManager } from './collection_manager'; +import { getTelemetryOptIn, getTelemetrySendUsageFrom } from './telemetry_config'; +import { getTelemetrySavedObject, updateTelemetrySavedObject } from './telemetry_repository'; +import { REPORT_INTERVAL_MS } from '../common/constants'; +import { getXpackConfigWithDeprecated } from '../common/get_xpack_config_with_deprecated'; + +export class FetcherTask { + private readonly checkDurationMs = 60 * 1000 * 5; + private intervalId?: NodeJS.Timeout; + private lastReported?: number; + private isSending = false; + private server: any; + + constructor(server: any) { + this.server = server; + } + + private getInternalRepository = () => { + const { getSavedObjectsRepository } = this.server.savedObjects; + const { callWithInternalUser } = this.server.plugins.elasticsearch.getCluster('admin'); + const internalRepository = getSavedObjectsRepository(callWithInternalUser); + return internalRepository; + }; + + private getCurrentConfigs = async () => { + const internalRepository = this.getInternalRepository(); + const telemetrySavedObject = await getTelemetrySavedObject(internalRepository); + const config = this.server.config(); + const currentKibanaVersion = config.get('pkg.version'); + const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom'); + const allowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); + const configTelemetryOptIn = config.get('telemetry.optIn'); + const telemetryUrl = getXpackConfigWithDeprecated(config, 'telemetry.url') as string; + + return { + telemetryOptIn: getTelemetryOptIn({ + currentKibanaVersion, + telemetrySavedObject, + allowChangingOptInStatus, + configTelemetryOptIn, + }), + telemetrySendUsageFrom: getTelemetrySendUsageFrom({ + telemetrySavedObject, + configTelemetrySendUsageFrom, + }), + telemetryUrl, + }; + }; + + private updateLastReported = async () => { + const internalRepository = this.getInternalRepository(); + this.lastReported = Date.now(); + updateTelemetrySavedObject(internalRepository, { + lastReported: this.lastReported, + }); + }; + + private shouldSendReport = ({ telemetryOptIn, telemetrySendUsageFrom }: any) => { + if (telemetryOptIn && telemetrySendUsageFrom === 'server') { + if (!this.lastReported || Date.now() - this.lastReported > REPORT_INTERVAL_MS) { + return true; + } + } + return false; + }; + + private fetchTelemetry = async () => { + return await telemetryCollectionManager.getStats({ + unencrypted: false, + server: this.server, + start: moment() + .subtract(20, 'minutes') + .toISOString(), + end: moment().toISOString(), + }); + }; + + private sendTelemetry = async (url: string, cluster: any): Promise => { + this.server.log(['debug', 'telemetry', 'fetcher'], `Sending usage stats.`); + await fetch(url, { + method: 'post', + body: cluster, + }); + }; + + private sendIfDue = async () => { + if (this.isSending) { + return; + } + try { + const telemetryConfig = await this.getCurrentConfigs(); + if (!this.shouldSendReport(telemetryConfig)) { + return; + } + + // mark that we are working so future requests are ignored until we're done + this.isSending = true; + const clusters = await this.fetchTelemetry(); + for (const cluster of clusters) { + await this.sendTelemetry(telemetryConfig.telemetryUrl, cluster); + } + + await this.updateLastReported(); + } catch (err) { + this.server.log( + ['warning', 'telemetry', 'fetcher'], + `Error sending telemetry usage data: ${err}` + ); + } + this.isSending = false; + }; + + public start = () => { + this.intervalId = setInterval(() => this.sendIfDue(), this.checkDurationMs); + }; + public stop = () => { + if (this.intervalId) { + clearInterval(this.intervalId); + } + }; +} diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts index aa13fab9a5f819..02752ca773488e 100644 --- a/src/legacy/core_plugins/telemetry/server/index.ts +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -21,7 +21,8 @@ import { PluginInitializerContext } from 'src/core/server'; import { TelemetryPlugin } from './plugin'; import * as constants from '../common/constants'; -export { getTelemetryOptIn } from './get_telemetry_opt_in'; +export { FetcherTask } from './fetcher'; +export { replaceTelemetryInjectedVars } from './telemetry_config'; export { telemetryCollectionManager } from './collection_manager'; export const telemetryPlugin = (initializerContext: PluginInitializerContext) => diff --git a/src/legacy/core_plugins/telemetry/server/plugin.ts b/src/legacy/core_plugins/telemetry/server/plugin.ts index 813aa0df09e8c8..f2628090c08afb 100644 --- a/src/legacy/core_plugins/telemetry/server/plugin.ts +++ b/src/legacy/core_plugins/telemetry/server/plugin.ts @@ -19,8 +19,7 @@ import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { registerRoutes } from './routes'; -import { telemetryCollectionManager } from './collection_manager'; -import { getStats } from './telemetry_collection'; +import { registerCollection } from './telemetry_collection'; export class TelemetryPlugin { private readonly currentKibanaVersion: string; @@ -29,9 +28,9 @@ export class TelemetryPlugin { this.currentKibanaVersion = initializerContext.env.packageInfo.version; } - public async setup(core: CoreSetup) { + public setup(core: CoreSetup) { const currentKibanaVersion = this.currentKibanaVersion; - telemetryCollectionManager.setStatsGetter(getStats, 'local'); + registerCollection(); registerRoutes({ core, currentKibanaVersion }); } } diff --git a/src/legacy/core_plugins/telemetry/server/routes/index.ts b/src/legacy/core_plugins/telemetry/server/routes/index.ts index 549b3ef6068ec4..66a7b2c97f3aeb 100644 --- a/src/legacy/core_plugins/telemetry/server/routes/index.ts +++ b/src/legacy/core_plugins/telemetry/server/routes/index.ts @@ -18,8 +18,9 @@ */ import { CoreSetup } from 'src/core/server'; -import { registerOptInRoutes } from './opt_in'; -import { registerTelemetryDataRoutes } from './telemetry_stats'; +import { registerTelemetryOptInRoutes } from './telemetry_opt_in'; +import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats'; +import { registerTelemetryOptInStatsRoutes } from './telemetry_opt_in_stats'; interface RegisterRoutesParams { core: CoreSetup; @@ -27,6 +28,7 @@ interface RegisterRoutesParams { } export function registerRoutes({ core, currentKibanaVersion }: RegisterRoutesParams) { - registerTelemetryDataRoutes(core); - registerOptInRoutes({ core, currentKibanaVersion }); + registerTelemetryOptInRoutes({ core, currentKibanaVersion }); + registerTelemetryUsageStatsRoutes(core); + registerTelemetryOptInStatsRoutes(core); } diff --git a/src/legacy/core_plugins/telemetry/server/routes/opt_in.ts b/src/legacy/core_plugins/telemetry/server/routes/opt_in.ts deleted file mode 100644 index 3a7194890b5700..00000000000000 --- a/src/legacy/core_plugins/telemetry/server/routes/opt_in.ts +++ /dev/null @@ -1,65 +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 Joi from 'joi'; -import { boomify } from 'boom'; -import { CoreSetup } from 'src/core/server'; - -interface RegisterOptInRoutesParams { - core: CoreSetup; - currentKibanaVersion: string; -} - -export interface SavedObjectAttributes { - enabled?: boolean; - lastVersionChecked: string; -} - -export function registerOptInRoutes({ core, currentKibanaVersion }: RegisterOptInRoutesParams) { - const { server } = core.http as any; - - server.route({ - method: 'POST', - path: '/api/telemetry/v2/optIn', - options: { - validate: { - payload: Joi.object({ - enabled: Joi.bool().required(), - }), - }, - }, - handler: async (req: any, h: any) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const savedObject: SavedObjectAttributes = { - enabled: req.payload.enabled, - lastVersionChecked: currentKibanaVersion, - }; - const options = { - id: 'telemetry', - overwrite: true, - }; - try { - await savedObjectsClient.create('telemetry', savedObject, options); - } catch (err) { - return boomify(err); - } - return h.response({}).code(200); - }, - }); -} diff --git a/src/legacy/core_plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/server/routes/telemetry_opt_in.ts new file mode 100644 index 00000000000000..596c5c17c353e9 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -0,0 +1,97 @@ +/* + * 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 Joi from 'joi'; +import moment from 'moment'; +import { boomify } from 'boom'; +import { CoreSetup } from 'src/core/server'; +import { getTelemetryAllowChangingOptInStatus } from '../telemetry_config'; +import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats'; + +import { + TelemetrySavedObjectAttributes, + updateTelemetrySavedObject, +} from '../telemetry_repository'; + +interface RegisterOptInRoutesParams { + core: CoreSetup; + currentKibanaVersion: string; +} + +export function registerTelemetryOptInRoutes({ + core, + currentKibanaVersion, +}: RegisterOptInRoutesParams) { + const { server } = core.http as any; + + server.route({ + method: 'POST', + path: '/api/telemetry/v2/optIn', + options: { + validate: { + payload: Joi.object({ + enabled: Joi.bool().required(), + }), + }, + }, + handler: async (req: any, h: any) => { + try { + const newOptInStatus = req.payload.enabled; + const attributes: TelemetrySavedObjectAttributes = { + enabled: newOptInStatus, + lastVersionChecked: currentKibanaVersion, + }; + const config = req.server.config(); + const savedObjectsClient = req.getSavedObjectsClient(); + const configTelemetryAllowChangingOptInStatus = config.get( + 'telemetry.allowChangingOptInStatus' + ); + + const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({ + telemetrySavedObject: savedObjectsClient, + configTelemetryAllowChangingOptInStatus, + }); + if (!allowChangingOptInStatus) { + return h.response({ error: 'Not allowed to change Opt-in Status.' }).code(400); + } + + const sendUsageFrom = config.get('telemetry.sendUsageFrom'); + if (sendUsageFrom === 'server') { + const optInStatusUrl = config.get('telemetry.optInStatusUrl'); + await sendTelemetryOptInStatus( + { optInStatusUrl, newOptInStatus }, + { + start: moment() + .subtract(20, 'minutes') + .toISOString(), + end: moment().toISOString(), + server: req.server, + unencrypted: false, + } + ); + } + + await updateTelemetrySavedObject(savedObjectsClient, attributes); + return h.response({}).code(200); + } catch (err) { + return boomify(err); + } + }, + }); +} diff --git a/src/legacy/core_plugins/telemetry/server/routes/telemetry_opt_in_stats.ts b/src/legacy/core_plugins/telemetry/server/routes/telemetry_opt_in_stats.ts new file mode 100644 index 00000000000000..d3bf6dbb77d7a1 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/routes/telemetry_opt_in_stats.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import fetch from 'node-fetch'; +import Joi from 'joi'; +import moment from 'moment'; +import { CoreSetup } from 'src/core/server'; +import { telemetryCollectionManager, StatsGetterConfig } from '../collection_manager'; + +interface SendTelemetryOptInStatusConfig { + optInStatusUrl: string; + newOptInStatus: boolean; +} + +export async function sendTelemetryOptInStatus( + config: SendTelemetryOptInStatusConfig, + statsGetterConfig: StatsGetterConfig +) { + const { optInStatusUrl, newOptInStatus } = config; + const optInStatus = await telemetryCollectionManager.getOptInStats( + newOptInStatus, + statsGetterConfig + ); + + await fetch(optInStatusUrl, { + method: 'post', + body: optInStatus, + }); +} + +export function registerTelemetryOptInStatsRoutes(core: CoreSetup) { + const { server } = core.http as any; + + server.route({ + method: 'POST', + path: '/api/telemetry/v2/clusters/_opt_in_stats', + options: { + validate: { + payload: Joi.object({ + enabled: Joi.bool().required(), + unencrypted: Joi.bool().default(true), + }), + }, + }, + handler: async (req: any, h: any) => { + try { + const newOptInStatus = req.payload.enabled; + const unencrypted = req.payload.unencrypted; + const statsGetterConfig = { + start: moment() + .subtract(20, 'minutes') + .toISOString(), + end: moment().toISOString(), + server: req.server, + req, + unencrypted, + }; + + const optInStatus = await telemetryCollectionManager.getOptInStats( + newOptInStatus, + statsGetterConfig + ); + + return h.response(optInStatus).code(200); + } catch (err) { + return h.response([]).code(200); + } + }, + }); +} diff --git a/src/legacy/core_plugins/telemetry/server/routes/telemetry_stats.ts b/src/legacy/core_plugins/telemetry/server/routes/telemetry_usage_stats.ts similarity index 80% rename from src/legacy/core_plugins/telemetry/server/routes/telemetry_stats.ts rename to src/legacy/core_plugins/telemetry/server/routes/telemetry_usage_stats.ts index 8a91d24b34ed21..c14314ca4da241 100644 --- a/src/legacy/core_plugins/telemetry/server/routes/telemetry_stats.ts +++ b/src/legacy/core_plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -20,10 +20,9 @@ import Joi from 'joi'; import { boomify } from 'boom'; import { CoreSetup } from 'src/core/server'; -import { encryptTelemetry } from '../collectors'; import { telemetryCollectionManager } from '../collection_manager'; -export function registerTelemetryDataRoutes(core: CoreSetup) { +export function registerTelemetryUsageStatsRoutes(core: CoreSetup) { const { server } = core.http as any; server.route({ @@ -45,17 +44,17 @@ export function registerTelemetryDataRoutes(core: CoreSetup) { const start = req.payload.timeRange.min; const end = req.payload.timeRange.max; const unencrypted = req.payload.unencrypted; - const isDev = config.get('env.dev'); try { - const { getStats, title } = telemetryCollectionManager.getStatsGetter(); - server.log(['debug', 'telemetry'], `Using Stats Getter: ${title}`); - - const usageData = await getStats(req, config, start, end, unencrypted); - - if (unencrypted) return usageData; - return encryptTelemetry(usageData, isDev); + return await telemetryCollectionManager.getStats({ + unencrypted, + server, + req, + start, + end, + }); } catch (err) { + const isDev = config.get('env.dev'); if (isDev) { // don't ignore errors when running in dev mode return boomify(err, { statusCode: err.status || 500 }); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js index 9ca609cd88778c..d60b330db7b5b5 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js @@ -35,9 +35,9 @@ export function mockGetClusterStats(callCluster, clusterStats, req) { .returns(clusterStats); } -describe('get_cluster_stats', () => { +describe.skip('get_cluster_stats', () => { - it('uses callCluster to get cluster.stats API', () => { + it('uses callCluster to get cluster.stats API', async () => { const callCluster = sinon.stub(); const response = Promise.resolve({}); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index d0de9cc365a712..4cbdf18df4a744 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -26,7 +26,6 @@ import { mockGetClusterStats } from './get_cluster_stats'; import { omit } from 'lodash'; import { getLocalStats, - getLocalStatsWithCaller, handleLocalStats, } from '../get_local_stats'; @@ -153,7 +152,7 @@ describe('get_local_stats', () => { }); }); - describe('getLocalStatsWithCaller', () => { + describe.skip('getLocalStats', () => { it('returns expected object without xpack data when X-Pack fails to respond', async () => { const callClusterUsageFailed = sinon.stub(); @@ -162,8 +161,10 @@ describe('get_local_stats', () => { Promise.resolve(clusterInfo), Promise.resolve(clusterStats), ); - - const result = await getLocalStatsWithCaller(getMockServer(), callClusterUsageFailed); + const result = await getLocalStats({ + server: getMockServer(), + callCluster: callClusterUsageFailed, + }); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); @@ -184,51 +185,13 @@ describe('get_local_stats', () => { Promise.resolve(clusterStats), ); - const result = await getLocalStatsWithCaller(getMockServer(callCluster, kibana), callCluster); + const result = await getLocalStats({ + server: getMockServer(callCluster, kibana), + callCluster, + }); + expect(result.stack_stats.xpack).to.eql(combinedStatsResult.stack_stats.xpack); expect(result.stack_stats.kibana).to.eql(combinedStatsResult.stack_stats.kibana); }); }); - - describe('getLocalStats', () => { - it('uses callWithInternalUser from data cluster', async () => { - const getCluster = sinon.stub(); - const req = { server: getMockServer(getCluster) }; - const callWithInternalUser = sinon.stub(); - - getCluster.withArgs('data').returns({ callWithInternalUser }); - - mockGetLocalStats( - callWithInternalUser, - Promise.resolve(clusterInfo), - Promise.resolve(clusterStats), - ); - - const result = await getLocalStats(req, { useInternalUser: true }); - expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); - expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); - expect(result.version).to.eql(combinedStatsResult.version); - expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); - }); - it('uses callWithRequest from data cluster', async () => { - const getCluster = sinon.stub(); - const req = { server: getMockServer(getCluster) }; - const callWithRequest = sinon.stub(); - - getCluster.withArgs('data').returns({ callWithRequest }); - - mockGetLocalStats( - callWithRequest, - Promise.resolve(clusterInfo), - Promise.resolve(clusterStats), - req - ); - - const result = await getLocalStats(req, { useInternalUser: false }); - expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); - expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); - expect(result.version).to.eql(combinedStatsResult.version); - expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); - }); - }); }); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.ts similarity index 65% rename from src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.js rename to src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.ts index a840c39812e2c8..4abd95f0cf66db 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.ts @@ -17,18 +17,24 @@ * under the License. */ +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { TIMEOUT } from './constants'; - +import { ClusterDetailsGetter } from '../collection_manager'; /** * Get the cluster stats from the connected cluster. * * This is the equivalent to GET /_cluster/stats?timeout=30s. - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch equivalent to GET /_cluster/stats. */ -export function getClusterStats(callCluster) { - return callCluster('cluster.stats', { - timeout: TIMEOUT +export async function getClusterStats(callCluster: CallCluster) { + return await callCluster('cluster.stats', { + timeout: TIMEOUT, }); } + +/** + * Get the cluster uuids from the connected cluster. + */ +export const getClusterUuids: ClusterDetailsGetter = async ({ callCluster }) => { + const result = await getClusterStats(callCluster); + return [{ clusterUuid: result.cluster_uuid }]; +}; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts similarity index 63% rename from src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.js rename to src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts index 67fc721306c213..e11c6b1277d5bf 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -18,9 +18,12 @@ */ import { get, omit } from 'lodash'; +// @ts-ignore import { getClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; +// @ts-ignore import { getKibana, handleKibanaStats } from './get_kibana'; +import { StatsGetter } from '../collection_manager'; /** * Handle the separate local calls by combining them into a single object response that looks like the @@ -30,9 +33,9 @@ import { getKibana, handleKibanaStats } from './get_kibana'; * @param {Object} clusterStats Cluster stats (GET /_cluster/stats) * @return {Object} A combined object containing the different responses. */ -export function handleLocalStats(server, clusterInfo, clusterStats, kibana) { +export function handleLocalStats(server: any, clusterInfo: any, clusterStats: any, kibana: any) { return { - timestamp: (new Date()).toISOString(), + timestamp: new Date().toISOString(), cluster_uuid: get(clusterInfo, 'cluster_uuid'), cluster_name: get(clusterInfo, 'cluster_name'), version: get(clusterInfo, 'version.number'), @@ -40,7 +43,7 @@ export function handleLocalStats(server, clusterInfo, clusterStats, kibana) { collection: 'local', stack_stats: { kibana: handleKibanaStats(server, kibana), - } + }, }; } @@ -51,28 +54,16 @@ export function handleLocalStats(server, clusterInfo, clusterStats, kibana) { * @param {function} callCluster The callWithInternalUser handler (exposed for testing) * @return {Promise} The object containing the current Elasticsearch cluster's telemetry. */ -export async function getLocalStatsWithCaller(server, callCluster) { - const [ clusterInfo, clusterStats, kibana ] = await Promise.all([ - getClusterInfo(callCluster), // cluster info - getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) - getKibana(server, callCluster), - ]); - - return handleLocalStats(server, clusterInfo, clusterStats, kibana); -} - - -/** - * Get statistics for the connected Elasticsearch cluster. - * - * @param {Object} req The incoming request - * @param {Boolean} useRequestUser callWithRequest, otherwise callWithInternalUser - * @return {Promise} The cluster object containing telemetry. - */ -export async function getLocalStats(req, { useInternalUser = false } = {}) { - const { server } = req; - const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster('data'); - const callCluster = useInternalUser ? callWithInternalUser : (...args) => callWithRequest(req, ...args); - - return await getLocalStatsWithCaller(server, callCluster); -} +export const getLocalStats: StatsGetter = async (clustersDetails, config) => { + const { server, callCluster } = config; + return await Promise.all( + clustersDetails.map(async clustersDetail => { + const [clusterInfo, clusterStats, kibana] = await Promise.all([ + getClusterInfo(callCluster), // cluster info + getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) + getKibana(server, callCluster), + ]); + return handleLocalStats(server, clusterInfo, clusterStats, kibana); + }) + ); +}; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_stats.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_stats.ts deleted file mode 100644 index 024272e0f805ca..00000000000000 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_stats.ts +++ /dev/null @@ -1,45 +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. - */ - -// @ts-ignore -import { getLocalStats } from './get_local_stats'; - -/** - * Get the telemetry data. - * - * @param {Object} req The incoming request. - * @param {Object} config Kibana config. - * @param {String} start The start time of the request (likely 20m ago). - * @param {String} end The end time of the request. - * @param {Boolean} unencrypted Is the request payload going to be unencrypted. - * @return {Promise} An array of telemetry objects. - */ -export async function getStats( - req: any, - config: any, - start: string, - end: string, - unencrypted: boolean -) { - return [ - await getLocalStats(req, { - useInternalUser: !unencrypted, - }), - ]; -} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts index f33727d82f44ce..7f228dbc5e6f69 100644 --- a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts @@ -19,6 +19,5 @@ // @ts-ignore export { getLocalStats } from './get_local_stats'; - -// @ts-ignore -export { getStats } from './get_stats'; +export { getClusterUuids } from './get_cluster_stats'; +export { registerCollection } from './register_collection'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts new file mode 100644 index 00000000000000..faf8e9de79194b --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/register_collection.ts @@ -0,0 +1,51 @@ +/* + * 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. + */ + +/* + * 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 { telemetryCollectionManager } from '../collection_manager'; +import { getLocalStats } from './get_local_stats'; +import { getClusterUuids } from './get_cluster_stats'; + +export function registerCollection() { + telemetryCollectionManager.setCollection({ + esCluster: 'data', + title: 'local', + priority: 0, + statsGetter: getLocalStats, + clusterDetailsGetter: getClusterUuids, + }); +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_allow_changing_opt_in_status.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_allow_changing_opt_in_status.ts new file mode 100644 index 00000000000000..9fa4fbc5e0227c --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_allow_changing_opt_in_status.ts @@ -0,0 +1,39 @@ +/* + * 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 { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object'; + +interface GetTelemetryAllowChangingOptInStatus { + configTelemetryAllowChangingOptInStatus: boolean; + telemetrySavedObject: TelemetrySavedObject; +} + +export function getTelemetryAllowChangingOptInStatus({ + telemetrySavedObject, + configTelemetryAllowChangingOptInStatus, +}: GetTelemetryAllowChangingOptInStatus) { + if (!telemetrySavedObject) { + return configTelemetryAllowChangingOptInStatus; + } + + if (typeof telemetrySavedObject.telemetryAllowChangingOptInStatus === 'undefined') { + return configTelemetryAllowChangingOptInStatus; + } + + return telemetrySavedObject.telemetryAllowChangingOptInStatus; +} diff --git a/src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.test.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_opt_in.test.ts similarity index 63% rename from src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.test.ts rename to src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_opt_in.test.ts index 67ad3aaae427d9..efc4a020e0ff05 100644 --- a/src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.test.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_opt_in.test.ts @@ -18,72 +18,47 @@ */ import { getTelemetryOptIn } from './get_telemetry_opt_in'; +import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object'; -describe('get_telemetry_opt_in', () => { - it('returns false when request path is not /app*', async () => { - const params = getCallGetTelemetryOptInParams({ - requestPath: '/foo/bar', - }); - - const result = await callGetTelemetryOptIn(params); - - expect(result).toBe(false); - }); - - it('returns null when saved object not found', async () => { +describe('getTelemetryOptIn', () => { + it('returns null when saved object not found', () => { const params = getCallGetTelemetryOptInParams({ savedObjectNotFound: true, }); - const result = await callGetTelemetryOptIn(params); + const result = callGetTelemetryOptIn(params); expect(result).toBe(null); }); - it('returns false when saved object forbidden', async () => { + it('returns false when saved object forbidden', () => { const params = getCallGetTelemetryOptInParams({ savedObjectForbidden: true, }); - const result = await callGetTelemetryOptIn(params); + const result = callGetTelemetryOptIn(params); expect(result).toBe(false); }); - it('throws an error on unexpected saved object error', async () => { - const params = getCallGetTelemetryOptInParams({ - savedObjectOtherError: true, - }); - - let threw = false; - try { - await callGetTelemetryOptIn(params); - } catch (err) { - threw = true; - expect(err.message).toBe(SavedObjectOtherErrorMessage); - } - - expect(threw).toBe(true); - }); - - it('returns null if enabled is null or undefined', async () => { + it('returns null if enabled is null or undefined', () => { for (const enabled of [null, undefined]) { const params = getCallGetTelemetryOptInParams({ enabled, }); - const result = await callGetTelemetryOptIn(params); + const result = callGetTelemetryOptIn(params); expect(result).toBe(null); } }); - it('returns true when enabled is true', async () => { + it('returns true when enabled is true', () => { const params = getCallGetTelemetryOptInParams({ enabled: true, }); - const result = await callGetTelemetryOptIn(params); + const result = callGetTelemetryOptIn(params); expect(result).toBe(true); }); @@ -146,24 +121,24 @@ describe('get_telemetry_opt_in', () => { }); interface CallGetTelemetryOptInParams { - requestPath: string; savedObjectNotFound: boolean; savedObjectForbidden: boolean; - savedObjectOtherError: boolean; - enabled: boolean | null | undefined; lastVersionChecked?: any; // should be a string, but test with non-strings currentKibanaVersion: string; result?: boolean | null; + enabled: boolean | null | undefined; + configTelemetryOptIn: boolean | null; + allowChangingOptInStatus: boolean; } const DefaultParams = { - requestPath: '/app/something', savedObjectNotFound: false, savedObjectForbidden: false, - savedObjectOtherError: false, enabled: true, lastVersionChecked: '8.0.0', currentKibanaVersion: '8.0.0', + configTelemetryOptIn: null, + allowChangingOptInStatus: true, }; function getCallGetTelemetryOptInParams( @@ -172,43 +147,28 @@ function getCallGetTelemetryOptInParams( return { ...DefaultParams, ...overrides }; } -async function callGetTelemetryOptIn(params: CallGetTelemetryOptInParams): Promise { - const { currentKibanaVersion } = params; - const request = getMockRequest(params); - return await getTelemetryOptIn({ request, currentKibanaVersion }); -} - -function getMockRequest(params: CallGetTelemetryOptInParams): any { - return { - path: params.requestPath, - getSavedObjectsClient() { - return getMockSavedObjectsClient(params); - }, - }; +function callGetTelemetryOptIn(params: CallGetTelemetryOptInParams) { + const { currentKibanaVersion, configTelemetryOptIn, allowChangingOptInStatus } = params; + const telemetrySavedObject = getMockTelemetrySavedObject(params); + return getTelemetryOptIn({ + currentKibanaVersion, + telemetrySavedObject, + allowChangingOptInStatus, + configTelemetryOptIn, + }); } -const SavedObjectNotFoundMessage = 'savedObjectNotFound'; -const SavedObjectForbiddenMessage = 'savedObjectForbidden'; -const SavedObjectOtherErrorMessage = 'savedObjectOtherError'; +function getMockTelemetrySavedObject(params: CallGetTelemetryOptInParams): TelemetrySavedObject { + const { savedObjectNotFound, savedObjectForbidden } = params; + if (savedObjectForbidden) { + return false; + } + if (savedObjectNotFound) { + return null; + } -function getMockSavedObjectsClient(params: CallGetTelemetryOptInParams) { return { - async get(type: string, id: string) { - if (params.savedObjectNotFound) throw new Error(SavedObjectNotFoundMessage); - if (params.savedObjectForbidden) throw new Error(SavedObjectForbiddenMessage); - if (params.savedObjectOtherError) throw new Error(SavedObjectOtherErrorMessage); - - const enabled = params.enabled; - const lastVersionChecked = params.lastVersionChecked; - return { attributes: { enabled, lastVersionChecked } }; - }, - errors: { - isNotFoundError(error: any) { - return error.message === SavedObjectNotFoundMessage; - }, - isForbiddenError(error: any) { - return error.message === SavedObjectForbiddenMessage; - }, - }, + enabled: params.enabled, + lastVersionChecked: params.lastVersionChecked, }; } diff --git a/src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_opt_in.ts similarity index 58% rename from src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.ts rename to src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_opt_in.ts index c8bd4a4b6dfbd4..d83ffdf69b5767 100644 --- a/src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.ts +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_opt_in.ts @@ -18,67 +18,51 @@ */ import semver from 'semver'; +import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object'; -import { SavedObjectAttributes } from './routes/opt_in'; - -interface GetTelemetryOptIn { - request: any; +interface GetTelemetryOptInConfig { + telemetrySavedObject: TelemetrySavedObject; currentKibanaVersion: string; + allowChangingOptInStatus: boolean; + configTelemetryOptIn: boolean | null; } -// Returns whether telemetry has been opt'ed into or not. -// Returns null not set, meaning Kibana should prompt in the UI. -export async function getTelemetryOptIn({ - request, +type GetTelemetryOptIn = (config: GetTelemetryOptInConfig) => null | boolean; + +export const getTelemetryOptIn: GetTelemetryOptIn = ({ + telemetrySavedObject, currentKibanaVersion, -}: GetTelemetryOptIn): Promise { - const isRequestingApplication = request.path.startsWith('/app'); + allowChangingOptInStatus, + configTelemetryOptIn, +}) => { + if (typeof configTelemetryOptIn === 'boolean' && !allowChangingOptInStatus) { + return configTelemetryOptIn; + } - // Prevent interstitial screens (such as the space selector) from prompting for telemetry - if (!isRequestingApplication) { + if (telemetrySavedObject === false) { return false; } - const savedObjectsClient = request.getSavedObjectsClient(); - - let savedObject; - try { - savedObject = await savedObjectsClient.get('telemetry', 'telemetry'); - } catch (error) { - if (savedObjectsClient.errors.isNotFoundError(error)) { - return null; - } - - // if we aren't allowed to get the telemetry document, we can assume that we won't - // be able to opt into telemetry either, so we're returning `false` here instead of null - if (savedObjectsClient.errors.isForbiddenError(error)) { - return false; - } - - throw error; + if (telemetrySavedObject === null || typeof telemetrySavedObject.enabled !== 'boolean') { + return configTelemetryOptIn; } - const { attributes }: { attributes: SavedObjectAttributes } = savedObject; - - // if enabled is already null, return null - if (attributes.enabled == null) return null; - - const enabled = !!attributes.enabled; + const savedOptIn = telemetrySavedObject.enabled; // if enabled is true, return it - if (enabled === true) return enabled; + if (savedOptIn === true) return savedOptIn; // Additional check if they've already opted out (enabled: false): // - if the Kibana version has changed by at least a minor version, // return null to re-prompt. - const lastKibanaVersion = attributes.lastVersionChecked; + const lastKibanaVersion = telemetrySavedObject.lastVersionChecked; // if the last kibana version isn't set, or is somehow not a string, return null if (typeof lastKibanaVersion !== 'string') return null; // if version hasn't changed, just return enabled value - if (lastKibanaVersion === currentKibanaVersion) return enabled; + if (lastKibanaVersion === currentKibanaVersion) return savedOptIn; const lastSemver = parseSemver(lastKibanaVersion); const currentSemver = parseSemver(currentKibanaVersion); @@ -93,8 +77,8 @@ export async function getTelemetryOptIn({ } // current version X.Y is not greater than last version X.Y, return enabled - return enabled; -} + return savedOptIn; +}; function parseSemver(version: string): semver.SemVer | null { // semver functions both return nulls AND throw exceptions: "it depends!" diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_send_usage_from.test.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_send_usage_from.test.ts new file mode 100644 index 00000000000000..69868a97a931d3 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_send_usage_from.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; +import { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object'; + +describe('getTelemetrySendUsageFrom', () => { + it('returns kibana.yml config when saved object not found', () => { + const params: CallGetTelemetryUsageFetcherParams = { + savedObjectNotFound: true, + configSendUsageFrom: 'browser', + }; + + const result = callGetTelemetryUsageFetcher(params); + + expect(result).toBe('browser'); + }); + + it('returns kibana.yml config when saved object forbidden', () => { + const params: CallGetTelemetryUsageFetcherParams = { + savedObjectForbidden: true, + configSendUsageFrom: 'browser', + }; + + const result = callGetTelemetryUsageFetcher(params); + + expect(result).toBe('browser'); + }); + + it('returns kibana.yml config when saved object sendUsageFrom is undefined', () => { + const params: CallGetTelemetryUsageFetcherParams = { + savedSendUsagefrom: undefined, + configSendUsageFrom: 'server', + }; + + const result = callGetTelemetryUsageFetcher(params); + + expect(result).toBe('server'); + }); +}); + +interface CallGetTelemetryUsageFetcherParams { + savedObjectNotFound?: boolean; + savedObjectForbidden?: boolean; + savedSendUsagefrom?: 'browser' | 'server'; + configSendUsageFrom: 'browser' | 'server'; +} + +function callGetTelemetryUsageFetcher(params: CallGetTelemetryUsageFetcherParams) { + const telemetrySavedObject = getMockTelemetrySavedObject(params); + const configTelemetrySendUsageFrom = params.configSendUsageFrom; + return getTelemetrySendUsageFrom({ configTelemetrySendUsageFrom, telemetrySavedObject }); +} + +function getMockTelemetrySavedObject( + params: CallGetTelemetryUsageFetcherParams +): TelemetrySavedObject { + const { savedObjectNotFound, savedObjectForbidden } = params; + if (savedObjectForbidden) { + return false; + } + if (savedObjectNotFound) { + return null; + } + + return { + sendUsageFrom: params.savedSendUsagefrom, + }; +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_send_usage_from.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_send_usage_from.ts new file mode 100644 index 00000000000000..9e4ae14b6097c2 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/get_telemetry_send_usage_from.ts @@ -0,0 +1,39 @@ +/* + * 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 { TelemetrySavedObject } from '../telemetry_repository/get_telemetry_saved_object'; + +interface GetTelemetryUsageFetcherConfig { + configTelemetrySendUsageFrom: 'browser' | 'server'; + telemetrySavedObject: TelemetrySavedObject; +} + +export function getTelemetrySendUsageFrom({ + telemetrySavedObject, + configTelemetrySendUsageFrom, +}: GetTelemetryUsageFetcherConfig) { + if (!telemetrySavedObject) { + return configTelemetrySendUsageFrom; + } + + if (typeof telemetrySavedObject.sendUsageFrom === 'undefined') { + return configTelemetrySendUsageFrom; + } + + return telemetrySavedObject.sendUsageFrom; +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts new file mode 100644 index 00000000000000..ab30dac1c36660 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { replaceTelemetryInjectedVars } from './replace_injected_vars'; +export { getTelemetryOptIn } from './get_telemetry_opt_in'; +export { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; +export { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_config/replace_injected_vars.ts b/src/legacy/core_plugins/telemetry/server/telemetry_config/replace_injected_vars.ts new file mode 100644 index 00000000000000..90d1f9cfdac65f --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_config/replace_injected_vars.ts @@ -0,0 +1,63 @@ +/* + * 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 { getTelemetrySavedObject } from '../telemetry_repository'; +import { getTelemetryOptIn } from './get_telemetry_opt_in'; +import { getTelemetrySendUsageFrom } from './get_telemetry_send_usage_from'; +import { getTelemetryAllowChangingOptInStatus } from './get_telemetry_allow_changing_opt_in_status'; + +export async function replaceTelemetryInjectedVars(request: any) { + const config = request.server.config(); + const configTelemetrySendUsageFrom = config.get('telemetry.sendUsageFrom'); + const configTelemetryOptIn = config.get('telemetry.optIn'); + const configTelemetryAllowChangingOptInStatus = config.get('telemetry.allowChangingOptInStatus'); + const isRequestingApplication = request.path.startsWith('/app'); + + // Prevent interstitial screens (such as the space selector) from prompting for telemetry + if (!isRequestingApplication) { + return { + telemetryOptedIn: false, + }; + } + + const currentKibanaVersion = config.get('pkg.version'); + const savedObjectsClient = request.getSavedObjectsClient(); + const telemetrySavedObject = await getTelemetrySavedObject(savedObjectsClient); + const allowChangingOptInStatus = getTelemetryAllowChangingOptInStatus({ + configTelemetryAllowChangingOptInStatus, + telemetrySavedObject, + }); + + const telemetryOptedIn = getTelemetryOptIn({ + configTelemetryOptIn, + allowChangingOptInStatus, + telemetrySavedObject, + currentKibanaVersion, + }); + + const telemetrySendUsageFrom = getTelemetrySendUsageFrom({ + configTelemetrySendUsageFrom, + telemetrySavedObject, + }); + + return { + telemetryOptedIn, + telemetrySendUsageFrom, + }; +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.test.ts b/src/legacy/core_plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.test.ts new file mode 100644 index 00000000000000..7cc177878de4d0 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { getTelemetrySavedObject } from './get_telemetry_saved_object'; +import { SavedObjectsErrorHelpers } from '../../../../../core/server'; + +describe('getTelemetrySavedObject', () => { + it('returns null when saved object not found', async () => { + const params = getCallGetTelemetrySavedObjectParams({ + savedObjectNotFound: true, + }); + + const result = await callGetTelemetrySavedObject(params); + + expect(result).toBe(null); + }); + + it('returns false when saved object forbidden', async () => { + const params = getCallGetTelemetrySavedObjectParams({ + savedObjectForbidden: true, + }); + + const result = await callGetTelemetrySavedObject(params); + + expect(result).toBe(false); + }); + + it('throws an error on unexpected saved object error', async () => { + const params = getCallGetTelemetrySavedObjectParams({ + savedObjectOtherError: true, + }); + + let threw = false; + try { + await callGetTelemetrySavedObject(params); + } catch (err) { + threw = true; + expect(err.message).toBe(SavedObjectOtherErrorMessage); + } + + expect(threw).toBe(true); + }); +}); + +interface CallGetTelemetrySavedObjectParams { + savedObjectNotFound: boolean; + savedObjectForbidden: boolean; + savedObjectOtherError: boolean; + result?: any; +} + +const DefaultParams = { + savedObjectNotFound: false, + savedObjectForbidden: false, + savedObjectOtherError: false, +}; + +function getCallGetTelemetrySavedObjectParams( + overrides: Partial +): CallGetTelemetrySavedObjectParams { + return { ...DefaultParams, ...overrides }; +} + +async function callGetTelemetrySavedObject(params: CallGetTelemetrySavedObjectParams) { + const savedObjectsClient = getMockSavedObjectsClient(params); + return await getTelemetrySavedObject(savedObjectsClient); +} + +const SavedObjectForbiddenMessage = 'savedObjectForbidden'; +const SavedObjectOtherErrorMessage = 'savedObjectOtherError'; + +function getMockSavedObjectsClient(params: CallGetTelemetrySavedObjectParams) { + return { + async get(type: string, id: string) { + if (params.savedObjectNotFound) throw SavedObjectsErrorHelpers.createGenericNotFoundError(); + if (params.savedObjectForbidden) + throw SavedObjectsErrorHelpers.decorateForbiddenError( + new Error(SavedObjectForbiddenMessage) + ); + if (params.savedObjectOtherError) + throw SavedObjectsErrorHelpers.decorateGeneralError( + new Error(SavedObjectOtherErrorMessage) + ); + + return { attributes: { enabled: null } }; + }, + }; +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.ts b/src/legacy/core_plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.ts new file mode 100644 index 00000000000000..91965ef201ecb5 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.ts @@ -0,0 +1,43 @@ +/* + * 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 { TelemetrySavedObjectAttributes } from './'; +import { SavedObjectsErrorHelpers } from '../../../../../core/server'; + +export type TelemetrySavedObject = TelemetrySavedObjectAttributes | null | false; +type GetTelemetrySavedObject = (repository: any) => Promise; + +export const getTelemetrySavedObject: GetTelemetrySavedObject = async (repository: any) => { + try { + const { attributes } = await repository.get('telemetry', 'telemetry'); + return attributes; + } catch (error) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + return null; + } + + // if we aren't allowed to get the telemetry document, we can assume that we won't + // be able to opt into telemetry either, so we're returning `false` here instead of null + if (SavedObjectsErrorHelpers.isForbiddenError(error)) { + return false; + } + + throw error; + } +}; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts new file mode 100644 index 00000000000000..f3629abc1620cc --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_repository/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { getTelemetrySavedObject, TelemetrySavedObject } from './get_telemetry_saved_object'; +export { updateTelemetrySavedObject } from './update_telemetry_saved_object'; + +export interface TelemetrySavedObjectAttributes { + enabled?: boolean | null; + lastVersionChecked?: string; + sendUsageFrom?: 'browser' | 'server'; + lastReported?: number; + telemetryAllowChangingOptInStatus?: boolean; +} diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js b/src/legacy/core_plugins/telemetry/server/telemetry_repository/update_telemetry_saved_object.ts similarity index 57% rename from src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js rename to src/legacy/core_plugins/telemetry/server/telemetry_repository/update_telemetry_saved_object.ts index 4dc55194562e5a..b66e01faaa6bc7 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/hacks/hide_empty_tools.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_repository/update_telemetry_saved_object.ts @@ -17,17 +17,22 @@ * under the License. */ -import { uiModules } from 'ui/modules'; -import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; -import { npStart } from 'ui/new_platform'; +import { TelemetrySavedObjectAttributes } from './'; +import { SavedObjectsErrorHelpers } from '../../../../../core/server'; -export function hideEmptyDevTools(Private) { - const hasTools = !!Private(DevToolsRegistryProvider).length; - if (!hasTools) { - npStart.core.chrome.navLinks.update('kibana:dev_tools', { - hidden: true - }); +export async function updateTelemetrySavedObject( + savedObjectsClient: any, + savedObjectAttributes: TelemetrySavedObjectAttributes +) { + try { + return await savedObjectsClient.update('telemetry', 'telemetry', savedObjectAttributes); + } catch (err) { + if (SavedObjectsErrorHelpers.isNotFoundError(err)) { + return await savedObjectsClient.create('telemetry', savedObjectAttributes, { + id: 'telemetry', + overwrite: true, + }); + } + throw err; } } - -uiModules.get('kibana').run(hideEmptyDevTools); diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 751f92fe88215b..0e3c4fdd9d3557 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -20,7 +20,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern'; -import * as visModule from 'ui/vis'; +import { Vis } from 'ui/vis'; import { ImageComparator } from 'test_utils/image_comparator'; import dummyESResponse from './dummy_es_response.json'; import initial from './initial.png'; @@ -65,7 +65,6 @@ let visRegComplete = false; describe('CoordinateMapsVisualizationTest', function () { let domNode; let CoordinateMapsVisualization; - let Vis; let indexPattern; let vis; let dependencies; @@ -91,7 +90,6 @@ describe('CoordinateMapsVisualizationTest', function () { } - Vis = Private(visModule.VisProvider); CoordinateMapsVisualization = createTileMapVisualization(dependencies); indexPattern = Private(LogstashIndexPatternStubProvider); diff --git a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts index 6239e4027c392a..35dea4a0deb9b2 100644 --- a/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts +++ b/src/legacy/core_plugins/timelion/public/vis/timelion_request_handler.ts @@ -22,8 +22,7 @@ import { buildEsQuery, getEsQueryConfig } from '@kbn/es-query'; // @ts-ignore import { timezoneProvider } from 'ui/vis/lib/timezone'; import { KIBANA_CONTEXT_NAME } from 'src/plugins/expressions/public'; -import { Query } from 'src/legacy/core_plugins/data/public'; -import { TimeRange, esFilters } from 'src/plugins/data/public'; +import { Query, TimeRange, esFilters } from 'src/plugins/data/public'; import { VisParams } from 'ui/vis'; import { i18n } from '@kbn/i18n'; import { TimelionVisualizationDependencies } from '../plugin'; diff --git a/src/legacy/core_plugins/ui_metric/index.ts b/src/legacy/core_plugins/ui_metric/index.ts index 6c957f23b5c40d..964e3497accba7 100644 --- a/src/legacy/core_plugins/ui_metric/index.ts +++ b/src/legacy/core_plugins/ui_metric/index.ts @@ -39,13 +39,13 @@ export default function(kibana: any) { injectDefaultVars(server: Server) { const config = server.config(); return { + uiMetricEnabled: config.get('ui_metric.enabled'), debugUiMetric: config.get('ui_metric.debug'), }; }, mappings: require('./mappings.json'), hacks: ['plugins/ui_metric/hacks/ui_metric_init'], }, - init(server: Legacy.Server) { registerUiMetricRoute(server); }, diff --git a/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts b/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts index 7aafc82cfe4c63..983434f09922b3 100644 --- a/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts +++ b/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts @@ -20,15 +20,26 @@ // @ts-ignore import { uiModules } from 'ui/modules'; import chrome from 'ui/chrome'; -import { createAnalyticsReporter, setTelemetryReporter } from '../services/telemetry_analytics'; +import { kfetch } from 'ui/kfetch'; +import { + createAnalyticsReporter, + setTelemetryReporter, + trackUserAgent, +} from '../services/telemetry_analytics'; +import { isUnauthenticated } from '../../../telemetry/public/services'; function telemetryInit($injector: any) { - const localStorage = $injector.get('localStorage'); + const uiMetricEnabled = chrome.getInjected('uiMetricEnabled'); const debug = chrome.getInjected('debugUiMetric'); - const $http = $injector.get('$http'); - const basePath = chrome.getBasePath(); - const uiReporter = createAnalyticsReporter({ localStorage, $http, basePath, debug }); + if (!uiMetricEnabled || isUnauthenticated()) { + return; + } + const localStorage = $injector.get('localStorage'); + + const uiReporter = createAnalyticsReporter({ localStorage, debug, kfetch }); setTelemetryReporter(uiReporter); + uiReporter.start(); + trackUserAgent('kibana'); } uiModules.get('kibana').run(telemetryInit); diff --git a/src/legacy/core_plugins/ui_metric/public/index.ts b/src/legacy/core_plugins/ui_metric/public/index.ts index b1e78b56d05d0a..5c327234b1e7cd 100644 --- a/src/legacy/core_plugins/ui_metric/public/index.ts +++ b/src/legacy/core_plugins/ui_metric/public/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { createUiStatsReporter } from './services/telemetry_analytics'; -export { METRIC_TYPE } from '@kbn/analytics'; +export { createUiStatsReporter, trackUserAgent } from './services/telemetry_analytics'; +export { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; diff --git a/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts index 7310ee5b5f1720..ee928b8a1d9ee8 100644 --- a/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts +++ b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts @@ -17,7 +17,9 @@ * under the License. */ -import { createReporter, Reporter, UiStatsMetricType } from '@kbn/analytics'; +import { Reporter, UiStatsMetricType } from '@kbn/analytics'; +// @ts-ignore +import { addSystemApiHeader } from 'ui/system_api'; let telemetryReporter: Reporter; @@ -39,28 +41,36 @@ export const createUiStatsReporter = (appName: string) => ( } }; +export const trackUserAgent = (appName: string) => { + if (telemetryReporter) { + return telemetryReporter.reportUserAgent(appName); + } +}; + interface AnalyicsReporterConfig { localStorage: any; - basePath: string; debug: boolean; - $http: ng.IHttpService; + kfetch: any; } export function createAnalyticsReporter(config: AnalyicsReporterConfig) { - const { localStorage, basePath, debug } = config; + const { localStorage, debug, kfetch } = config; - return createReporter({ + return new Reporter({ debug, storage: localStorage, async http(report) { - const url = `${basePath}/api/telemetry/report`; - await fetch(url, { + const response = await kfetch({ method: 'POST', - headers: { - 'kbn-xsrf': 'true', - }, - body: JSON.stringify({ report }), + pathname: '/api/telemetry/report', + body: JSON.stringify(report), + headers: addSystemApiHeader({}), }); + + if (response.status !== 'ok') { + throw Error('Unable to store report.'); + } + return response; }, }); } diff --git a/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts b/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts index 8a7950c46fa317..e2de23ea806e44 100644 --- a/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts +++ b/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts @@ -18,7 +18,6 @@ */ import Joi from 'joi'; -import Boom from 'boom'; import { Report } from '@kbn/analytics'; import { Server } from 'hapi'; @@ -27,15 +26,27 @@ export async function storeReport(server: any, report: Report) { const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); const internalRepository = getSavedObjectsRepository(callWithInternalUser); - const metricKeys = Object.keys(report.uiStatsMetrics); - return Promise.all( - metricKeys.map(async key => { - const metric = report.uiStatsMetrics[key]; + const uiStatsMetrics = report.uiStatsMetrics ? Object.entries(report.uiStatsMetrics) : []; + const userAgents = report.userAgent ? Object.entries(report.userAgent) : []; + return Promise.all([ + ...userAgents.map(async ([key, metric]) => { + const { userAgent } = metric; + const savedObjectId = `${key}:${userAgent}`; + return await internalRepository.create( + 'ui-metric', + { count: 1 }, + { + id: savedObjectId, + overwrite: true, + } + ); + }), + ...uiStatsMetrics.map(async ([key, metric]) => { const { appName, eventName } = metric; const savedObjectId = `${appName}:${eventName}`; - return internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'); - }) - ); + return await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'); + }), + ]); } export function registerUiMetricRoute(server: Server) { @@ -45,36 +56,46 @@ export function registerUiMetricRoute(server: Server) { options: { validate: { payload: Joi.object({ - report: Joi.object({ - uiStatsMetrics: Joi.object() - .pattern( - /.*/, - Joi.object({ - key: Joi.string().required(), - type: Joi.string().required(), - appName: Joi.string().required(), - eventName: Joi.string().required(), - stats: Joi.object({ - min: Joi.number(), - sum: Joi.number(), - max: Joi.number(), - avg: Joi.number(), - }).allow(null), - }) - ) - .allow(null), - }), + reportVersion: Joi.number().optional(), + userAgent: Joi.object() + .pattern( + /.*/, + Joi.object({ + key: Joi.string().required(), + type: Joi.string().required(), + appName: Joi.string().required(), + userAgent: Joi.string().required(), + }) + ) + .allow(null) + .optional(), + uiStatsMetrics: Joi.object() + .pattern( + /.*/, + Joi.object({ + key: Joi.string().required(), + type: Joi.string().required(), + appName: Joi.string().required(), + eventName: Joi.string().required(), + stats: Joi.object({ + min: Joi.number(), + sum: Joi.number(), + max: Joi.number(), + avg: Joi.number(), + }).allow(null), + }) + ) + .allow(null), }), }, }, handler: async (req: any, h: any) => { - const { report } = req.payload; - try { + const report = req.payload; await storeReport(server, report); - return {}; + return { status: 'ok' }; } catch (error) { - return new Boom('Something went wrong', { statusCode: error.status }); + return { status: 'fail' }; } }, }); diff --git a/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis.js b/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis.js index 7f626df6a4ea3d..384beb3764e2e0 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis.js +++ b/src/legacy/core_plugins/vis_type_metric/public/__tests__/metric_vis.js @@ -21,7 +21,7 @@ import $ from 'jquery'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { VisProvider } from 'ui/vis'; +import { Vis } from 'ui/vis'; import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern'; import { createMetricVisTypeDefinition } from '../metric_vis_type'; @@ -34,7 +34,6 @@ describe('metric_vis - createMetricVisTypeDefinition', () => { beforeEach( ngMock.inject(Private => { setup = () => { - const Vis = Private(VisProvider); const metricVisType = createMetricVisTypeDefinition(); const indexPattern = Private(LogstashIndexPatternStubProvider); diff --git a/src/legacy/core_plugins/vis_type_table/public/__tests__/table_vis_controller.js b/src/legacy/core_plugins/vis_type_table/public/__tests__/table_vis_controller.js index abebf8190dc9fc..4153ce2da36a73 100644 --- a/src/legacy/core_plugins/vis_type_table/public/__tests__/table_vis_controller.js +++ b/src/legacy/core_plugins/vis_type_table/public/__tests__/table_vis_controller.js @@ -21,7 +21,7 @@ import $ from 'jquery'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; -import { VisProvider } from 'ui/vis'; +import { Vis } from 'ui/vis'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { AppStateProvider } from 'ui/state_management/app_state'; @@ -36,7 +36,6 @@ describe('Table Vis - Controller', async function () { let Private; let $scope; let $el; - let Vis; let fixtures; let AppState; let tableAggResponse; @@ -63,7 +62,6 @@ describe('Table Vis - Controller', async function () { $compile = $injector.get('$compile'); fixtures = require('fixtures/fake_hierarchical_data'); AppState = Private(AppStateProvider); - Vis = Private(VisProvider); tableAggResponse = legacyResponseHandlerProvider().handler; }) ); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js index d22ff92c4d3f61..13e8a4fd9535a1 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js @@ -25,7 +25,7 @@ import fixtures from 'fixtures/fake_hierarchical_data'; import sinon from 'sinon'; import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { VisProvider } from 'ui/vis'; +import { Vis } from 'ui/vis'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { round } from 'lodash'; @@ -36,7 +36,6 @@ import { setup as visualizationsSetup } from '../../../../visualizations/public/ describe('Table Vis - AggTable Directive', function () { let $rootScope; let $compile; - let Vis; let indexPattern; let settings; let tableAggResponse; @@ -113,7 +112,6 @@ describe('Table Vis - AggTable Directive', function () { ngMock.inject(function ($injector, Private, config) { tableAggResponse = legacyResponseHandlerProvider().handler; indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - Vis = Private(VisProvider); settings = config; $rootScope = $injector.get('$rootScope'); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js index e8359e37d61860..f4e3a8e36605cf 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js +++ b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js @@ -23,13 +23,12 @@ import expect from '@kbn/expect'; import fixtures from 'fixtures/fake_hierarchical_data'; import { legacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { VisProvider } from 'ui/vis'; +import { Vis } from 'ui/vis'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; describe('Table Vis - AggTableGroup Directive', function () { let $rootScope; let $compile; - let Vis; let indexPattern; let tableAggResponse; const tabifiedData = {}; @@ -69,7 +68,6 @@ describe('Table Vis - AggTableGroup Directive', function () { tableAggResponse = legacyResponseHandlerProvider().handler; indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - Vis = Private(VisProvider); $rootScope = $injector.get('$rootScope'); $compile = $injector.get('$compile'); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js b/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js index 69d08d8cb3f74f..0cb903faac47c5 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js @@ -20,7 +20,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern'; -import * as visModule from 'ui/vis'; +import { Vis } from 'ui/vis'; import { ImageComparator } from 'test_utils/image_comparator'; import { TagCloudVisualization } from '../tag_cloud_visualization'; import basicdrawPng from './basicdraw.png'; @@ -33,7 +33,6 @@ const PIXEL_DIFF = 64; describe('TagCloudVisualizationTest', function () { let domNode; - let Vis; let indexPattern; let vis; let imageComparator; @@ -57,7 +56,6 @@ describe('TagCloudVisualizationTest', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject((Private) => { - Vis = Private(visModule.VisProvider); indexPattern = Private(LogstashIndexPatternStubProvider); })); diff --git a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js index 842d3aa6c4ad75..2b42c22ad7c435 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor.js @@ -44,8 +44,7 @@ const APP_NAME = 'VisEditor'; export class VisEditor extends Component { constructor(props) { super(props); - const { vis } = props; - this.appState = vis.API.getAppState(); + this.appState = props.appState; this.localStorage = new Storage(window.localStorage); this.state = { model: props.visParams, @@ -183,7 +182,6 @@ export class VisEditor extends Component { dirty={this.state.dirty} autoApply={this.state.autoApply} model={model} - appState={this.appState} savedObj={this.props.savedObj} timeRange={this.props.timeRange} uiState={this.uiState} @@ -239,4 +237,5 @@ VisEditor.propTypes = { isEditorMode: PropTypes.bool, savedObj: PropTypes.object, timeRange: PropTypes.object, + appState: PropTypes.object, }; diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js index 46725a2c5d01fc..191f35d2e03ea3 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js @@ -23,7 +23,7 @@ import ngMock from 'ng_mock'; import $ from 'jquery'; import { createVegaVisualization } from '../vega_visualization'; import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern'; -import * as visModule from 'ui/vis'; +import { Vis } from 'ui/vis'; import { ImageComparator } from 'test_utils/image_comparator'; import vegaliteGraph from '!!raw-loader!./vegalite_graph.hjson'; @@ -50,7 +50,6 @@ const PIXEL_DIFF = 30; describe('VegaVisualizations', () => { let domNode; let VegaVisualization; - let Vis; let indexPattern; let vis; let imageComparator; @@ -73,7 +72,6 @@ describe('VegaVisualizations', () => { ); } - Vis = Private(visModule.VisProvider); VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); indexPattern = Private(LogstashIndexPatternStubProvider); diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts index b4c32f37eb90c1..accc52c1e5a147 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts @@ -18,11 +18,9 @@ */ import { timefilter } from 'ui/timefilter'; -import { TimeRange } from 'src/plugins/data/public'; -import { Query } from 'src/legacy/core_plugins/data/public'; -import { buildEsQuery, getEsQueryConfig } from '@kbn/es-query'; -import { esFilters } from '../../../../plugins/data/public'; +import { buildEsQuery, getEsQueryConfig } from '@kbn/es-query'; +import { esFilters, TimeRange, Query } from '../../../../plugins/data/public'; // @ts-ignore import { VegaParser } from './data_model/vega_parser'; diff --git a/src/legacy/core_plugins/visualizations/public/index.ts b/src/legacy/core_plugins/visualizations/public/index.ts index ad86c9ddb14c59..ca79f547890f9c 100644 --- a/src/legacy/core_plugins/visualizations/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/index.ts @@ -39,7 +39,6 @@ export { DefaultEditorSize } from 'ui/vis/editor_size'; import * as types from 'ui/vis/vis'; export type Vis = types.Vis; export type VisParams = types.VisParams; -export type VisProvider = types.VisProvider; export type VisState = types.VisState; export { VisualizationController } from 'ui/vis/vis_types/vis_type'; export { Status } from 'ui/vis/update_status'; diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts index c14daa37f57060..dfd36f2aa7b2b2 100644 --- a/src/legacy/plugin_discovery/types.ts +++ b/src/legacy/plugin_discovery/types.ts @@ -62,7 +62,6 @@ export interface LegacyPluginOptions { }>; apps: any; hacks: string[]; - devTools: string[]; styleSheetPaths: string; injectDefaultVars: (server: Server) => Record; noParse: string[]; diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js index 27b390ed7e4711..39303e94adc6f1 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js +++ b/src/legacy/ui/public/agg_response/tabify/__tests__/_get_columns.js @@ -20,15 +20,13 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { tabifyGetColumns } from '../_get_columns'; -import { VisProvider } from '../../../vis'; +import { Vis } from '../../../vis'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('get columns', function () { - let Vis; let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(VisProvider); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js index fd8df904b600f0..dabd66a22fcd28 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js +++ b/src/legacy/ui/public/agg_response/tabify/__tests__/_integration.js @@ -22,16 +22,14 @@ import fixtures from 'fixtures/fake_hierarchical_data'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { tabifyAggResponse } from '../tabify'; -import { VisProvider } from '../../../vis'; +import { Vis } from '../../../vis'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('tabifyAggResponse Integration', function () { - let Vis; let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(VisProvider); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); diff --git a/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js b/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js index 09668c638d6952..001132f97c95a6 100644 --- a/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js +++ b/src/legacy/ui/public/agg_response/tabify/__tests__/_response_writer.js @@ -20,11 +20,10 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { TabbedAggResponseWriter } from '../_response_writer'; -import { VisProvider } from '../../../vis'; +import { Vis } from '../../../vis'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('TabbedAggResponseWriter class', function () { - let Vis; let Private; let indexPattern; @@ -32,7 +31,6 @@ describe('TabbedAggResponseWriter class', function () { beforeEach(ngMock.inject(function ($injector) { Private = $injector.get('Private'); - Vis = Private(VisProvider); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); diff --git a/src/legacy/ui/public/agg_types/__tests__/buckets/_terms_other_bucket_helper.js b/src/legacy/ui/public/agg_types/__tests__/buckets/_terms_other_bucket_helper.js index dcadeacb3d1fea..759a6b80d8faf9 100644 --- a/src/legacy/ui/public/agg_types/__tests__/buckets/_terms_other_bucket_helper.js +++ b/src/legacy/ui/public/agg_types/__tests__/buckets/_terms_other_bucket_helper.js @@ -20,7 +20,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { buildOtherBucketAgg, mergeOtherBucketAggResponse, updateMissingBucket } from '../../buckets/_terms_other_bucket_helper'; -import { VisProvider } from '../../../vis'; +import { Vis } from '../../../vis'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; const visConfigSingleTerm = { @@ -158,7 +158,6 @@ describe('Terms Agg Other bucket helper', () => { function init(aggConfig) { ngMock.module('kibana'); ngMock.inject((Private) => { - const Vis = Private(VisProvider); const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); vis = new Vis(indexPattern, aggConfig); diff --git a/src/legacy/ui/public/agg_types/agg_configs.ts b/src/legacy/ui/public/agg_types/agg_configs.ts index 675d37d05c33c1..7c0245f30a1fda 100644 --- a/src/legacy/ui/public/agg_types/agg_configs.ts +++ b/src/legacy/ui/public/agg_types/agg_configs.ts @@ -253,13 +253,10 @@ export class AggConfigs { // collect all the aggregations const aggregations = this.aggs .filter(agg => agg.enabled && agg.type) - .reduce( - (requestValuesAggs, agg: AggConfig) => { - const aggs = agg.getRequestAggs(); - return aggs ? requestValuesAggs.concat(aggs) : requestValuesAggs; - }, - [] as AggConfig[] - ); + .reduce((requestValuesAggs, agg: AggConfig) => { + const aggs = agg.getRequestAggs(); + return aggs ? requestValuesAggs.concat(aggs) : requestValuesAggs; + }, [] as AggConfig[]); // move metrics to the end return _.sortBy(aggregations, (agg: AggConfig) => agg.type.type === AggGroupNames.Metrics ? 1 : 0 @@ -282,13 +279,10 @@ export class AggConfigs { * @return {array[AggConfig]} */ getResponseAggs(): AggConfig[] { - return this.getRequestAggs().reduce( - function(responseValuesAggs, agg: AggConfig) { - const aggs = agg.getResponseAggs(); - return aggs ? responseValuesAggs.concat(aggs) : responseValuesAggs; - }, - [] as AggConfig[] - ); + return this.getRequestAggs().reduce(function(responseValuesAggs, agg: AggConfig) { + const aggs = agg.getResponseAggs(); + return aggs ? responseValuesAggs.concat(aggs) : responseValuesAggs; + }, [] as AggConfig[]); } /** diff --git a/src/legacy/ui/public/agg_types/buckets/filters.ts b/src/legacy/ui/public/agg_types/buckets/filters.ts index 44a97abb7a1d77..a8d509d507c6b9 100644 --- a/src/legacy/ui/public/agg_types/buckets/filters.ts +++ b/src/legacy/ui/public/agg_types/buckets/filters.ts @@ -27,10 +27,9 @@ import { buildEsQuery } from '@kbn/es-query'; import { FiltersParamEditor, FilterValue } from '../../vis/editors/default/controls/filters'; import { createFilterFilters } from './create_filter/filters'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; -import { setup as data } from '../../../../core_plugins/data/public/legacy'; import { Storage } from '../../../../../plugins/kibana_utils/public'; +import { getQueryLog } from '../../../../../plugins/data/public'; -const { getQueryLog } = data.query.helpers; const config = chrome.getUiSettingsClient(); const storage = new Storage(window.localStorage); diff --git a/src/legacy/ui/public/agg_types/buckets/geo_hash.test.ts b/src/legacy/ui/public/agg_types/buckets/geo_hash.test.ts index 5c599f16e09c2e..effa49f0ade6b2 100644 --- a/src/legacy/ui/public/agg_types/buckets/geo_hash.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/geo_hash.test.ts @@ -156,8 +156,9 @@ describe('Geohash Agg', () => { describe('aggregation options', () => { it('should only create geohash_grid and geo_centroid aggregations when isFilteredByCollar is false', () => { const aggConfigs = getAggConfigs({ isFilteredByCollar: false }); - const requestAggs = geoHashBucketAgg.getRequestAggs(aggConfigs - .aggs[0] as IBucketGeoHashGridAggConfig) as IBucketGeoHashGridAggConfig[]; + const requestAggs = geoHashBucketAgg.getRequestAggs( + aggConfigs.aggs[0] as IBucketGeoHashGridAggConfig + ) as IBucketGeoHashGridAggConfig[]; expect(requestAggs.length).toEqual(2); expect(requestAggs[0].type.name).toEqual('geohash_grid'); @@ -166,8 +167,9 @@ describe('Geohash Agg', () => { it('should only create filter and geohash_grid aggregations when useGeocentroid is false', () => { const aggConfigs = getAggConfigs({ useGeocentroid: false }); - const requestAggs = geoHashBucketAgg.getRequestAggs(aggConfigs - .aggs[0] as IBucketGeoHashGridAggConfig) as IBucketGeoHashGridAggConfig[]; + const requestAggs = geoHashBucketAgg.getRequestAggs( + aggConfigs.aggs[0] as IBucketGeoHashGridAggConfig + ) as IBucketGeoHashGridAggConfig[]; expect(requestAggs.length).toEqual(2); expect(requestAggs[0].type.name).toEqual('filter'); @@ -179,36 +181,43 @@ describe('Geohash Agg', () => { let originalRequestAggs: IBucketGeoHashGridAggConfig[]; beforeEach(() => { - originalRequestAggs = geoHashBucketAgg.getRequestAggs(getAggConfigs() - .aggs[0] as IBucketGeoHashGridAggConfig) as IBucketGeoHashGridAggConfig[]; + originalRequestAggs = geoHashBucketAgg.getRequestAggs( + getAggConfigs().aggs[0] as IBucketGeoHashGridAggConfig + ) as IBucketGeoHashGridAggConfig[]; }); it('should change geo_bounding_box filter aggregation and vis session state when map movement is outside map collar', () => { - const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs(getAggConfigs({ - mapBounds: { - top_left: { lat: 10.0, lon: -10.0 }, - bottom_right: { lat: 9.0, lon: -9.0 }, - }, - }).aggs[0] as IBucketGeoHashGridAggConfig) as IBucketGeoHashGridAggConfig[]; + const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( + getAggConfigs({ + mapBounds: { + top_left: { lat: 10.0, lon: -10.0 }, + bottom_right: { lat: 9.0, lon: -9.0 }, + }, + }).aggs[0] as IBucketGeoHashGridAggConfig + ) as IBucketGeoHashGridAggConfig[]; expect(originalRequestAggs[1].params).not.toEqual(geoBoxingBox.params); }); it('should not change geo_bounding_box filter aggregation and vis session state when map movement is within map collar', () => { - const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs(getAggConfigs({ - mapBounds: { - top_left: { lat: 1, lon: -1 }, - bottom_right: { lat: -1, lon: 1 }, - }, - }).aggs[0] as IBucketGeoHashGridAggConfig) as IBucketGeoHashGridAggConfig[]; + const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( + getAggConfigs({ + mapBounds: { + top_left: { lat: 1, lon: -1 }, + bottom_right: { lat: -1, lon: 1 }, + }, + }).aggs[0] as IBucketGeoHashGridAggConfig + ) as IBucketGeoHashGridAggConfig[]; expect(originalRequestAggs[1].params).toEqual(geoBoxingBox.params); }); it('should change geo_bounding_box filter aggregation and vis session state when map zoom level changes', () => { - const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs(getAggConfigs({ - mapZoom: -1, - }).aggs[0] as IBucketGeoHashGridAggConfig) as IBucketGeoHashGridAggConfig[]; + const [, geoBoxingBox] = geoHashBucketAgg.getRequestAggs( + getAggConfigs({ + mapZoom: -1, + }).aggs[0] as IBucketGeoHashGridAggConfig + ) as IBucketGeoHashGridAggConfig[]; expect(originalRequestAggs[1].lastMapCollar).not.toEqual(geoBoxingBox.lastMapCollar); }); diff --git a/src/legacy/ui/public/agg_types/buckets/range.test.ts b/src/legacy/ui/public/agg_types/buckets/range.test.ts index f7cae60cce773b..1b423e64c48aef 100644 --- a/src/legacy/ui/public/agg_types/buckets/range.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/range.test.ts @@ -72,7 +72,10 @@ describe('Range Agg', () => { schema: 'segment', params: { field: 'bytes', - ranges: [{ from: 0, to: 1000 }, { from: 1000, to: 2000 }], + ranges: [ + { from: 0, to: 1000 }, + { from: 1000, to: 2000 }, + ], }, }, ], diff --git a/src/legacy/ui/public/agg_types/buckets/range.ts b/src/legacy/ui/public/agg_types/buckets/range.ts index 348fccdab3fe37..230675ab487ade 100644 --- a/src/legacy/ui/public/agg_types/buckets/range.ts +++ b/src/legacy/ui/public/agg_types/buckets/range.ts @@ -98,7 +98,10 @@ export const rangeBucketAgg = new BucketAggType({ }, { name: 'ranges', - default: [{ from: 0, to: 1000 }, { from: 1000, to: 2000 }], + default: [ + { from: 0, to: 1000 }, + { from: 1000, to: 2000 }, + ], editorComponent: RangesEditor, write(aggConfig: IBucketAggConfig, output: Record) { output.params.ranges = aggConfig.params.ranges; diff --git a/src/legacy/ui/public/agg_types/filter/prop_filter.ts b/src/legacy/ui/public/agg_types/filter/prop_filter.ts index 45f350ea6adc81..e6b5f3831e65d9 100644 --- a/src/legacy/ui/public/agg_types/filter/prop_filter.ts +++ b/src/legacy/ui/public/agg_types/filter/prop_filter.ts @@ -59,24 +59,21 @@ function propFilter

(prop: P) { return list; } - const options = filters.reduce( - (acc, filter) => { - let type = 'include'; - let value = filter; + const options = filters.reduce((acc, filter) => { + let type = 'include'; + let value = filter; - if (filter.charAt(0) === '!') { - type = 'exclude'; - value = filter.substr(1); - } + if (filter.charAt(0) === '!') { + type = 'exclude'; + value = filter.substr(1); + } - if (!acc[type]) { - acc[type] = []; - } - acc[type].push(value); - return acc; - }, - {} as { [type: string]: string[] } - ); + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(value); + return acc; + }, {} as { [type: string]: string[] }); return list.filter(item => { const value = item[prop]; diff --git a/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts b/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts index 66bc205cead132..c1f5528825bcc4 100644 --- a/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts +++ b/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts @@ -58,8 +58,9 @@ export class MetricAggType< config.getValue || ((agg, bucket) => { // Metric types where an empty set equals `zero` - const isSettableToZero = [METRIC_TYPES.CARDINALITY, METRIC_TYPES.SUM].includes(agg.type - .name as METRIC_TYPES); + const isSettableToZero = [METRIC_TYPES.CARDINALITY, METRIC_TYPES.SUM].includes( + agg.type.name as METRIC_TYPES + ); // Return proper values when no buckets are present // `Count` handles empty sets properly diff --git a/src/legacy/ui/public/agg_types/metrics/percentile_ranks.test.ts b/src/legacy/ui/public/agg_types/metrics/percentile_ranks.test.ts index f3882ca57161f1..7461b5cf07ee7c 100644 --- a/src/legacy/ui/public/agg_types/metrics/percentile_ranks.test.ts +++ b/src/legacy/ui/public/agg_types/metrics/percentile_ranks.test.ts @@ -63,8 +63,9 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() { }); it('uses the custom label if it is set', function() { - const responseAggs: any = percentileRanksMetricAgg.getResponseAggs(aggConfigs - .aggs[0] as IPercentileRanksAggConfig); + const responseAggs: any = percentileRanksMetricAgg.getResponseAggs( + aggConfigs.aggs[0] as IPercentileRanksAggConfig + ); const percentileRankLabelFor5kBytes = responseAggs[0].makeLabel(); const percentileRankLabelFor10kBytes = responseAggs[1].makeLabel(); diff --git a/src/legacy/ui/public/agg_types/metrics/percentiles.test.ts b/src/legacy/ui/public/agg_types/metrics/percentiles.test.ts index 1503f43b22dc30..c9f4bcc3862a03 100644 --- a/src/legacy/ui/public/agg_types/metrics/percentiles.test.ts +++ b/src/legacy/ui/public/agg_types/metrics/percentiles.test.ts @@ -63,8 +63,9 @@ describe('AggTypesMetricsPercentilesProvider class', () => { }); it('uses the custom label if it is set', () => { - const responseAggs: any = percentilesMetricAgg.getResponseAggs(aggConfigs - .aggs[0] as IPercentileAggConfig); + const responseAggs: any = percentilesMetricAgg.getResponseAggs( + aggConfigs.aggs[0] as IPercentileAggConfig + ); const ninetyFifthPercentileLabel = responseAggs[0].makeLabel(); diff --git a/src/legacy/ui/public/agg_types/metrics/std_deviation.test.ts b/src/legacy/ui/public/agg_types/metrics/std_deviation.test.ts index ae09b5cd789774..3125026a521854 100644 --- a/src/legacy/ui/public/agg_types/metrics/std_deviation.test.ts +++ b/src/legacy/ui/public/agg_types/metrics/std_deviation.test.ts @@ -58,8 +58,9 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { it('uses the custom label if it is set', () => { const aggConfigs = getAggConfigs('custom label'); - const responseAggs: any = stdDeviationMetricAgg.getResponseAggs(aggConfigs - .aggs[0] as IStdDevAggConfig); + const responseAggs: any = stdDeviationMetricAgg.getResponseAggs( + aggConfigs.aggs[0] as IStdDevAggConfig + ); const lowerStdDevLabel = responseAggs[0].makeLabel(); const upperStdDevLabel = responseAggs[1].makeLabel(); @@ -71,8 +72,9 @@ describe('AggTypeMetricStandardDeviationProvider class', () => { it('uses the default labels if custom label is not set', () => { const aggConfigs = getAggConfigs(); - const responseAggs: any = stdDeviationMetricAgg.getResponseAggs(aggConfigs - .aggs[0] as IStdDevAggConfig); + const responseAggs: any = stdDeviationMetricAgg.getResponseAggs( + aggConfigs.aggs[0] as IStdDevAggConfig + ); const lowerStdDevLabel = responseAggs[0].makeLabel(); const upperStdDevLabel = responseAggs[1].makeLabel(); diff --git a/src/legacy/ui/public/filter_manager/__tests__/filter_generator.js b/src/legacy/ui/public/filter_manager/__tests__/filter_generator.js deleted file mode 100644 index 5b6455bf20847e..00000000000000 --- a/src/legacy/ui/public/filter_manager/__tests__/filter_generator.js +++ /dev/null @@ -1,166 +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 _ from 'lodash'; -import sinon from 'sinon'; -import MockState from 'fixtures/mock_state'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { getFilterGenerator } from '..'; -import { FilterBarQueryFilterProvider } from '../../filter_manager/query_filter'; -import { uniqFilters, esFilters } from '../../../../../plugins/data/public'; - -let queryFilter; -let filterGen; -let appState; - -function checkAddFilters(length, comps, idx) { - idx = idx || 0; - const filters = queryFilter.addFilters.getCall(idx).args[0]; - - expect(filters.length).to.be(length); - if (!Array.isArray(comps)) return; - comps.forEach(function (comp, i) { - expect(filters[i]).to.eql(comp); - }); -} - -describe('Filter Manager', function () { - beforeEach(ngMock.module( - 'kibana', - 'kibana/global_state', - function ($provide) { - $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); - - appState = new MockState({ filters: [] }); - $provide.service('getAppState', function () { - return function () { return appState; }; - }); - } - )); - - beforeEach(ngMock.inject(function (_$rootScope_, Private) { - - // mock required queryFilter methods, used in the manager - queryFilter = Private(FilterBarQueryFilterProvider); - filterGen = getFilterGenerator(queryFilter); - sinon.stub(queryFilter, 'getAppFilters').callsFake(() => appState.filters); - sinon.stub(queryFilter, 'addFilters').callsFake((filters) => { - if (!Array.isArray(filters)) filters = [filters]; - appState.filters = uniqFilters(appState.filters.concat(filters)); - }); - })); - - it('should have an `add` function', function () { - expect(filterGen.add).to.be.a(Function); - }); - - it('should add a filter', function () { - filterGen.add('myField', 1, '+', 'myIndex'); - expect(queryFilter.addFilters.callCount).to.be(1); - checkAddFilters(1, [{ - meta: { index: 'myIndex', negate: false }, - query: { match_phrase: { myField: 1 } } - }]); - }); - - it('should add multiple filters if passed an array of values', function () { - filterGen.add('myField', [1, 2, 3], '+', 'myIndex'); - expect(queryFilter.addFilters.callCount).to.be(1); - checkAddFilters(3, [{ - meta: { index: 'myIndex', negate: false }, - query: { match_phrase: { myField: 1 } } - }, { - meta: { index: 'myIndex', negate: false }, - query: { match_phrase: { myField: 2 } } - }, { - meta: { index: 'myIndex', negate: false }, - query: { match_phrase: { myField: 3 } } - }]); - }); - - it('should add an exists filter if _exists_ is used as the field', function () { - filterGen.add('_exists_', 'myField', '+', 'myIndex'); - checkAddFilters(1, [{ - meta: { index: 'myIndex', negate: false }, - exists: { field: 'myField' } - }]); - }); - - it('should negate existing filter instead of added a conflicting filter', function () { - filterGen.add('myField', 1, '+', 'myIndex'); - checkAddFilters(1, [{ - meta: { index: 'myIndex', negate: false }, - query: { match_phrase: { myField: 1 } } - }], 0); - expect(appState.filters).to.have.length(1); - - // NOTE: negating exists filters also forces disabled to false - filterGen.add('myField', 1, '-', 'myIndex'); - checkAddFilters(1, [{ - meta: { index: 'myIndex', negate: true, disabled: false }, - query: { match_phrase: { myField: 1 } } - }], 1); - expect(appState.filters).to.have.length(1); - - filterGen.add('_exists_', 'myField', '+', 'myIndex'); - checkAddFilters(1, [{ - meta: { index: 'myIndex', negate: false }, - exists: { field: 'myField' } - }], 2); - expect(appState.filters).to.have.length(2); - - filterGen.add('_exists_', 'myField', '-', 'myIndex'); - checkAddFilters(1, [{ - meta: { index: 'myIndex', negate: true, disabled: false }, - exists: { field: 'myField' } - }], 3); - expect(appState.filters).to.have.length(2); - - const scriptedField = { name: 'scriptedField', scripted: true, script: 1, lang: 'painless' }; - filterGen.add(scriptedField, 1, '+', 'myIndex'); - checkAddFilters(1, [{ - meta: { index: 'myIndex', negate: false, field: 'scriptedField' }, - script: esFilters.getPhraseScript(scriptedField, 1) - }], 4); - expect(appState.filters).to.have.length(3); - - filterGen.add(scriptedField, 1, '-', 'myIndex'); - checkAddFilters(1, [{ - meta: { index: 'myIndex', negate: true, disabled: false, field: 'scriptedField' }, - script: esFilters.getPhraseScript(scriptedField, 1) - }], 5); - expect(appState.filters).to.have.length(3); - }); - - it('should enable matching filters being changed', function () { - _.each([true, false], function (negate) { - appState.filters = [{ - query: { match_phrase: { myField: 1 } }, - meta: { disabled: true, negate: negate } - }]; - expect(appState.filters.length).to.be(1); - expect(appState.filters[0].meta.disabled).to.be(true); - - filterGen.add('myField', 1, '+', 'myIndex'); - expect(appState.filters.length).to.be(1); - expect(appState.filters[0].meta.disabled).to.be(false); - }); - }); -}); diff --git a/src/legacy/ui/public/filter_manager/filter_generator.js b/src/legacy/ui/public/filter_manager/filter_generator.js deleted file mode 100644 index e11e0ff6653a7e..00000000000000 --- a/src/legacy/ui/public/filter_manager/filter_generator.js +++ /dev/null @@ -1,98 +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 _ from 'lodash'; -import { esFilters } from '../../../../plugins/data/public'; - -// Adds a filter to a passed state -export function getFilterGenerator(queryFilter) { - const filterGen = {}; - - filterGen.generate = (field, values, operation, index) => { - values = Array.isArray(values) ? values : [values]; - const fieldName = _.isObject(field) ? field.name : field; - const filters = _.flatten([queryFilter.getAppFilters()]); - const newFilters = []; - - const negate = (operation === '-'); - - // TODO: On array fields, negating does not negate the combination, rather all terms - _.each(values, function (value) { - let filter; - const existing = _.find(filters, function (filter) { - if (!filter) return; - - if (fieldName === '_exists_' && filter.exists) { - return filter.exists.field === value; - } - - if (esFilters.isPhraseFilter(filter)) { - return esFilters.getPhraseFilterField(filter) === fieldName && esFilters.getPhraseFilterValue(filter) === value; - } - - if (filter.script) { - return filter.meta.field === fieldName && filter.script.script.params.value === value; - } - }); - - if (existing) { - existing.meta.disabled = false; - if (existing.meta.negate !== negate) { - existing.meta.negate = !existing.meta.negate; - } - newFilters.push(existing); - return; - } - - switch (fieldName) { - case '_exists_': - filter = { - meta: { negate, index }, - exists: { - field: value - } - }; - break; - default: - if (field.scripted) { - filter = { - meta: { negate, index, field: fieldName }, - script: esFilters.getPhraseScript(field, value) - }; - } else { - filter = { meta: { negate, index }, query: { match_phrase: {} } }; - filter.query.match_phrase[fieldName] = value; - } - - break; - } - - newFilters.push(filter); - }); - - return newFilters; - }; - - filterGen.add = function (field, values, operation, index) { - const newFilters = this.generate(field, values, operation, index); - return queryFilter.addFilters(newFilters); - }; - - return filterGen; -} diff --git a/src/legacy/ui/public/filter_manager/index.js b/src/legacy/ui/public/filter_manager/index.js index 6adc4e0965ccde..ce99d4cac3017c 100644 --- a/src/legacy/ui/public/filter_manager/index.js +++ b/src/legacy/ui/public/filter_manager/index.js @@ -17,4 +17,3 @@ * under the License. */ -export { getFilterGenerator } from './filter_generator'; diff --git a/src/legacy/ui/public/index_patterns/__mocks__/index.ts b/src/legacy/ui/public/index_patterns/__mocks__/index.ts index 85c07cb3b1df17..2dd3f370c6d6aa 100644 --- a/src/legacy/ui/public/index_patterns/__mocks__/index.ts +++ b/src/legacy/ui/public/index_patterns/__mocks__/index.ts @@ -45,6 +45,4 @@ export { IndexPatternMissingIndices, NoDefaultIndexPattern, NoDefinedIndexPatterns, - mockFields, - mockIndexPattern, } from '../../../../core_plugins/data/public'; diff --git a/src/legacy/ui/public/index_patterns/index.ts b/src/legacy/ui/public/index_patterns/index.ts index 67c370cad82a50..3b4952ac815192 100644 --- a/src/legacy/ui/public/index_patterns/index.ts +++ b/src/legacy/ui/public/index_patterns/index.ts @@ -47,8 +47,6 @@ export { IndexPatternMissingIndices, NoDefaultIndexPattern, NoDefinedIndexPatterns, - mockFields, - mockIndexPattern, } from '../../../core_plugins/data/public'; // types diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 58b8422cb2f8a9..788718e8484308 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -40,6 +40,7 @@ import { fatalError } from 'ui/notify'; import { capabilities } from 'ui/capabilities'; // @ts-ignore import { modifyUrl } from 'ui/url'; +import { toMountPoint } from '../../../../plugins/kibana_react/public'; // @ts-ignore import { UrlOverflowService } from '../error_url_overflow'; import { npStart } from '../new_platform'; @@ -329,7 +330,7 @@ const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( title: i18n.translate('common.ui.chrome.bigUrlWarningNotificationTitle', { defaultMessage: 'The URL is big and Kibana might stop working', }), - text: ( + text: toMountPoint( {}, + }, inspector: { registerView: () => undefined, __LEGACY: { @@ -97,6 +100,9 @@ export const npStart = { registerRenderer: sinon.fake(), registerType: sinon.fake(), }, + devTools: { + getSortedDevTools: () => [], + }, data: { autocomplete: { getProvider: sinon.fake(), diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 0c7b28e7da3df2..9ee5d8580a90b9 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -28,6 +28,7 @@ import { Start as InspectorStart, } from '../../../../plugins/inspector/public'; import { EuiUtilsStart } from '../../../../plugins/eui_utils/public'; +import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/public'; import { FeatureCatalogueSetup, FeatureCatalogueStart, @@ -40,6 +41,7 @@ export interface PluginsSetup { feature_catalogue: FeatureCatalogueSetup; inspector: InspectorSetup; uiActions: IUiActionsSetup; + devTools: DevToolsSetup; } export interface PluginsStart { @@ -50,6 +52,7 @@ export interface PluginsStart { feature_catalogue: FeatureCatalogueStart; inspector: InspectorStart; uiActions: IUiActionsStart; + devTools: DevToolsStart; } export const npSetup = { diff --git a/src/legacy/ui/public/utils/migrate_legacy_query.ts b/src/legacy/ui/public/utils/migrate_legacy_query.ts index 06e819d6a64a27..8d9b50d5a66b2a 100644 --- a/src/legacy/ui/public/utils/migrate_legacy_query.ts +++ b/src/legacy/ui/public/utils/migrate_legacy_query.ts @@ -18,7 +18,7 @@ */ import { has } from 'lodash'; -import { Query } from 'plugins/data'; +import { Query } from 'src/plugins/data/public'; /** * Creates a standardized query object from old queries that were either strings or pure ES query DSL diff --git a/src/legacy/ui/public/vis/__tests__/_agg_config.js b/src/legacy/ui/public/vis/__tests__/_agg_config.js index 46d7ed4601f29b..2e2e0c31bdb8af 100644 --- a/src/legacy/ui/public/vis/__tests__/_agg_config.js +++ b/src/legacy/ui/public/vis/__tests__/_agg_config.js @@ -20,7 +20,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { VisProvider } from '..'; +import { Vis } from '..'; import { AggType } from '../../agg_types/agg_type'; import { AggConfig } from '../../agg_types/agg_config'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; @@ -28,12 +28,10 @@ import { fieldFormats } from '../../registry/field_formats'; describe('AggConfig', function () { - let Vis; let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(VisProvider); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); diff --git a/src/legacy/ui/public/vis/__tests__/_agg_configs.js b/src/legacy/ui/public/vis/__tests__/_agg_configs.js index 96e4aa943ed1da..3240bd56f1502e 100644 --- a/src/legacy/ui/public/vis/__tests__/_agg_configs.js +++ b/src/legacy/ui/public/vis/__tests__/_agg_configs.js @@ -22,7 +22,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { AggConfig } from '../../agg_types/agg_config'; -import { VisProvider } from '..'; +import { Vis } from '..'; import { AggConfigs } from '../../agg_types/agg_configs'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { Schemas } from '../editors/default/schemas'; @@ -30,13 +30,11 @@ import { AggGroupNames } from '../editors/default/agg_groups'; describe('AggConfigs', function () { - let Vis; let indexPattern; beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { // load main deps - Vis = Private(VisProvider); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); diff --git a/src/legacy/ui/public/vis/__tests__/_vis.js b/src/legacy/ui/public/vis/__tests__/_vis.js index 5a2f93ab35b500..1d5e2de6dafe37 100644 --- a/src/legacy/ui/public/vis/__tests__/_vis.js +++ b/src/legacy/ui/public/vis/__tests__/_vis.js @@ -20,13 +20,12 @@ import _ from 'lodash'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { VisProvider } from '..'; +import { Vis } from '..'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { start as visualizations } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy'; describe('Vis Class', function () { let indexPattern; - let Vis; let visTypes; let vis; @@ -43,7 +42,6 @@ describe('Vis Class', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private) { - Vis = Private(VisProvider); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); visTypes = visualizations.types; })); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx index 7806b1c0f78fbc..661ece5944fa34 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx @@ -248,10 +248,9 @@ describe('DefaultEditorAgg component', () => { expect(compHistogram.find(DefaultEditorAggParams).props()).toHaveProperty('disabledParams', [ 'min_doc_count', ]); - expect(compDateHistogram.find(DefaultEditorAggParams).props()).toHaveProperty( - 'disabledParams', - ['min_doc_count'] - ); + expect( + compDateHistogram.find(DefaultEditorAggParams).props() + ).toHaveProperty('disabledParams', ['min_doc_count']); }); it('should set error when agg is not histogram or date_histogram', () => { diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx b/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx index 8e8926f027cad0..980889743c20d1 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg_group_state.tsx @@ -53,13 +53,10 @@ function aggGroupReducer(state: AggsState, action: AggsAction): AggsState { } function initAggsState(group: AggConfig[]): AggsState { - return group.reduce( - (state, agg) => { - state[agg.id] = { touched: false, valid: true }; - return state; - }, - {} as AggsState - ); + return group.reduce((state, agg) => { + state[agg.id] = { touched: false, valid: true }; + return state; + }, {} as AggsState); } export { aggGroupReducer, initAggsState }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts index 5fb241714b2e8c..eb6bef48876420 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts +++ b/src/legacy/ui/public/vis/editors/default/components/agg_params_helper.test.ts @@ -121,7 +121,10 @@ describe('DefaultEditorAggParams helpers', () => { }, schema: {}, getIndexPattern: jest.fn(() => ({ - fields: [{ name: '@timestamp', type: 'date' }, { name: 'geo_desc', type: 'string' }], + fields: [ + { name: '@timestamp', type: 'date' }, + { name: 'geo_desc', type: 'string' }, + ], })), params: { orderBy: 'orderBy', diff --git a/src/legacy/ui/public/vis/editors/default/controls/auto_precision.tsx b/src/legacy/ui/public/vis/editors/default/controls/auto_precision.tsx index 3b6aebe8c2b0c5..53f74465e90a5e 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/auto_precision.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/auto_precision.tsx @@ -23,7 +23,7 @@ import { EuiSwitch, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AggParamEditorProps } from '..'; -function AutoPrecisionParamEditor({ value, setValue }: AggParamEditorProps) { +function AutoPrecisionParamEditor({ value = false, setValue }: AggParamEditorProps) { const label = i18n.translate('common.ui.aggTypes.changePrecisionLabel', { defaultMessage: 'Change precision on map zoom', }); diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts index 3928c0a62eeaa7..c6772cc1087627 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts @@ -35,7 +35,10 @@ describe('NumberList utils', () => { let range: Range; beforeEach(() => { - modelList = [{ value: 1, id: '1', isInvalid: false }, { value: 2, id: '2', isInvalid: false }]; + modelList = [ + { value: 1, id: '1', isInvalid: false }, + { value: 2, id: '2', isInvalid: false }, + ]; range = { min: 1, max: 10, diff --git a/src/legacy/ui/public/vis/editors/default/controls/filter.tsx b/src/legacy/ui/public/vis/editors/default/controls/filter.tsx index 4ebe7b0d835d7c..e847a95ead4780 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/filter.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/filter.tsx @@ -20,7 +20,8 @@ import React, { useState } from 'react'; import { EuiForm, EuiButtonIcon, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Query, QueryBarInput } from 'plugins/data'; +import { QueryBarInput } from 'plugins/data'; +import { Query } from 'src/plugins/data/public'; import { AggConfig } from '../../..'; import { npStart } from '../../../../new_platform'; import { Storage } from '../../../../../../../plugins/kibana_utils/public'; diff --git a/src/legacy/ui/public/vis/editors/default/controls/filters.tsx b/src/legacy/ui/public/vis/editors/default/controls/filters.tsx index fe72e8dddd68d4..aa654d26a23fd6 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/filters.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/filters.tsx @@ -21,7 +21,7 @@ import React, { useState, useEffect } from 'react'; import { omit, isEqual } from 'lodash'; import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Query } from 'plugins/data'; +import { Query } from 'src/plugins/data/public'; import chrome from '../../../../chrome'; import { FilterRow } from './filter'; import { AggParamEditorProps } from '..'; diff --git a/src/legacy/ui/public/vis/editors/default/controls/switch.tsx b/src/legacy/ui/public/vis/editors/default/controls/switch.tsx index a5fc9682bd9543..de675386d91003 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/switch.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/switch.tsx @@ -30,7 +30,7 @@ interface SwitchParamEditorProps extends AggParamEditorProps { } function SwitchParamEditor({ - value, + value = false, setValue, dataTestSubj, displayToolTip, diff --git a/src/legacy/ui/public/vis/editors/default/controls/use_geocentroid.tsx b/src/legacy/ui/public/vis/editors/default/controls/use_geocentroid.tsx index 6da32690912e7e..932a4d19b495c0 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/use_geocentroid.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/use_geocentroid.tsx @@ -23,7 +23,7 @@ import { EuiSwitch, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AggParamEditorProps } from '..'; -function UseGeocentroidParamEditor({ value, setValue }: AggParamEditorProps) { +function UseGeocentroidParamEditor({ value = false, setValue }: AggParamEditorProps) { const label = i18n.translate('common.ui.aggTypes.placeMarkersOffGridLabel', { defaultMessage: 'Place markers off grid (use geocentroid)', }); diff --git a/src/legacy/ui/public/vis/editors/default/utils.tsx b/src/legacy/ui/public/vis/editors/default/utils.tsx index efc424488ec410..4f82298aaca41f 100644 --- a/src/legacy/ui/public/vis/editors/default/utils.tsx +++ b/src/legacy/ui/public/vis/editors/default/utils.tsx @@ -44,24 +44,21 @@ function groupAndSortBy< TGroupBy extends string = 'type', TLabelName extends string = 'title' >(objects: T[], groupBy: TGroupBy, labelName: TLabelName): ComboBoxGroupedOptions { - const groupedOptions = objects.reduce( - (array, obj) => { - const group = array.find(element => element.label === obj[groupBy]); - const option = { - label: obj[labelName], - target: obj, - }; + const groupedOptions = objects.reduce((array, obj) => { + const group = array.find(element => element.label === obj[groupBy]); + const option = { + label: obj[labelName], + target: obj, + }; - if (group && group.options) { - group.options.push(option); - } else { - array.push({ label: obj[groupBy], options: [option] }); - } + if (group && group.options) { + group.options.push(option); + } else { + array.push({ label: obj[groupBy], options: [option] }); + } - return array; - }, - [] as Array> - ); + return array; + }, [] as Array>); groupedOptions.sort(sortByLabel); diff --git a/src/legacy/ui/public/vis/index.d.ts b/src/legacy/ui/public/vis/index.d.ts index 5a2a7edb6329a0..791ce2563e0f17 100644 --- a/src/legacy/ui/public/vis/index.d.ts +++ b/src/legacy/ui/public/vis/index.d.ts @@ -18,5 +18,5 @@ */ export { AggConfig } from '../agg_types/agg_config'; -export { Vis, VisProvider, VisParams, VisState } from './vis'; +export { Vis, VisParams, VisState } from './vis'; export { VisualizationController, VisType } from './vis_types/vis_type'; diff --git a/src/legacy/ui/public/vis/index.js b/src/legacy/ui/public/vis/index.js index 711b465a1e8a53..05cd030f7d1006 100644 --- a/src/legacy/ui/public/vis/index.js +++ b/src/legacy/ui/public/vis/index.js @@ -17,4 +17,4 @@ * under the License. */ -export { VisProvider } from './vis'; +export { Vis } from './vis'; diff --git a/src/legacy/ui/public/vis/map/map_messages.js b/src/legacy/ui/public/vis/map/map_messages.js index 7571547ecf0d48..a9a636ab9e4cfc 100644 --- a/src/legacy/ui/public/vis/map/map_messages.js +++ b/src/legacy/ui/public/vis/map/map_messages.js @@ -24,6 +24,7 @@ import { EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; export const createZoomWarningMsg = (function () { let disableZoomMsg = false; @@ -107,7 +108,7 @@ export const createZoomWarningMsg = (function () { const zoomToast = ({ title: 'No additional zoom levels', - text: , + text: toMountPoint(), 'data-test-subj': 'maxZoomWarning', }); diff --git a/src/legacy/ui/public/vis/vis.d.ts b/src/legacy/ui/public/vis/vis.d.ts index be73cccf06a599..e16562641801ea 100644 --- a/src/legacy/ui/public/vis/vis.d.ts +++ b/src/legacy/ui/public/vis/vis.d.ts @@ -30,8 +30,6 @@ export interface Vis { [key: string]: any; } -export type VisProvider = (...dependencies: any[]) => Vis; - export interface VisParams { [key: string]: any; } diff --git a/src/legacy/ui/public/vis/vis.js b/src/legacy/ui/public/vis/vis.js index c1fff1556e3ade..304289a5cfa073 100644 --- a/src/legacy/ui/public/vis/vis.js +++ b/src/legacy/ui/public/vis/vis.js @@ -38,181 +38,178 @@ import { start as visualizations } from '../../../core_plugins/visualizations/pu import '../directives/bind'; -export function VisProvider(Private, getAppState) { - const visTypes = visualizations.types; - - class Vis extends EventEmitter { - constructor(indexPattern, visState) { - super(); - visState = visState || {}; - - if (_.isString(visState)) { - visState = { - type: visState - }; - } - this.indexPattern = indexPattern; - this._setUiState(new PersistedState()); - this.setCurrentState(visState); - this.setState(this.getCurrentState(), false); - - // Session state is for storing information that is transitory, and will not be saved with the visualization. - // For instance, map bounds, which depends on the view port, browser window size, etc. - this.sessionState = {}; - - this.API = { - SearchSource: SearchSource, - events: { - filter: data => this.eventsSubject.next({ name: 'filterBucket', data }), - brush: data => this.eventsSubject.next({ name: 'brush', data }), - }, - getAppState, +const visTypes = visualizations.types; + +class Vis extends EventEmitter { + constructor(indexPattern, visState) { + super(); + visState = visState || {}; + + if (_.isString(visState)) { + visState = { + type: visState }; } + this.indexPattern = indexPattern; + this._setUiState(new PersistedState()); + this.setCurrentState(visState); + this.setState(this.getCurrentState(), false); + + // Session state is for storing information that is transitory, and will not be saved with the visualization. + // For instance, map bounds, which depends on the view port, browser window size, etc. + this.sessionState = {}; + + this.API = { + SearchSource: SearchSource, + events: { + filter: data => this.eventsSubject.next({ name: 'filterBucket', data }), + brush: data => this.eventsSubject.next({ name: 'brush', data }), + }, + }; + } - setCurrentState(state) { - this.title = state.title || ''; - const type = state.type || this.type; - if (_.isString(type)) { - this.type = visTypes.get(type); - if (!this.type) { - throw new Error(`Invalid type "${type}"`); - } - } else { - this.type = type; + setCurrentState(state) { + this.title = state.title || ''; + const type = state.type || this.type; + if (_.isString(type)) { + this.type = visTypes.get(type); + if (!this.type) { + throw new Error(`Invalid type "${type}"`); } + } else { + this.type = type; + } - this.params = _.defaults({}, - _.cloneDeep(state.params || {}), - _.cloneDeep(this.type.visConfig.defaults || {}) - ); + this.params = _.defaults({}, + _.cloneDeep(state.params || {}), + _.cloneDeep(this.type.visConfig.defaults || {}) + ); - updateVisualizationConfig(state.params, this.params); + updateVisualizationConfig(state.params, this.params); - if (state.aggs || !this.aggs) { - this.aggs = new AggConfigs(this.indexPattern, state.aggs ? state.aggs.aggs || state.aggs : [], this.type.schemas.all); - } + if (state.aggs || !this.aggs) { + this.aggs = new AggConfigs(this.indexPattern, state.aggs ? state.aggs.aggs || state.aggs : [], this.type.schemas.all); } + } - setState(state, updateCurrentState = true) { - this._state = _.cloneDeep(state); - if (updateCurrentState) { - this.setCurrentState(this._state); - } + setState(state, updateCurrentState = true) { + this._state = _.cloneDeep(state); + if (updateCurrentState) { + this.setCurrentState(this._state); } + } - updateState() { - this.setState(this.getCurrentState(true)); - this.emit('update'); - } + updateState() { + this.setState(this.getCurrentState(true)); + this.emit('update'); + } - forceReload() { - this.emit('reload'); - } + forceReload() { + this.emit('reload'); + } - getCurrentState(includeDisabled) { - return { - title: this.title, - type: this.type.name, - params: _.cloneDeep(this.params), - aggs: this.aggs.aggs - .map(agg => agg.toJSON()) - .filter(agg => includeDisabled || agg.enabled) - .filter(Boolean) - }; - } + getCurrentState(includeDisabled) { + return { + title: this.title, + type: this.type.name, + params: _.cloneDeep(this.params), + aggs: this.aggs.aggs + .map(agg => agg.toJSON()) + .filter(agg => includeDisabled || agg.enabled) + .filter(Boolean) + }; + } - getSerializableState(state) { - return { - title: state.title, - type: state.type, - params: _.cloneDeep(state.params), - aggs: state.aggs.aggs - .map(agg => agg.toJSON()) - .filter(agg => agg.enabled) - .filter(Boolean) - }; - } + getSerializableState(state) { + return { + title: state.title, + type: state.type, + params: _.cloneDeep(state.params), + aggs: state.aggs.aggs + .map(agg => agg.toJSON()) + .filter(agg => agg.enabled) + .filter(Boolean) + }; + } - copyCurrentState(includeDisabled = false) { - const state = this.getCurrentState(includeDisabled); - state.aggs = new AggConfigs(this.indexPattern, state.aggs.aggs || state.aggs, this.type.schemas.all); - return state; - } + copyCurrentState(includeDisabled = false) { + const state = this.getCurrentState(includeDisabled); + state.aggs = new AggConfigs(this.indexPattern, state.aggs.aggs || state.aggs, this.type.schemas.all); + return state; + } - getStateInternal(includeDisabled) { - return { - title: this._state.title, - type: this._state.type, - params: this._state.params, - aggs: this._state.aggs - .filter(agg => includeDisabled || agg.enabled) - }; - } + getStateInternal(includeDisabled) { + return { + title: this._state.title, + type: this._state.type, + params: this._state.params, + aggs: this._state.aggs + .filter(agg => includeDisabled || agg.enabled) + }; + } - getEnabledState() { - return this.getStateInternal(false); - } + getEnabledState() { + return this.getStateInternal(false); + } - getAggConfig() { - return this.aggs.clone({ enabledOnly: true }); - } + getAggConfig() { + return this.aggs.clone({ enabledOnly: true }); + } - getState() { - return this.getStateInternal(true); - } + getState() { + return this.getStateInternal(true); + } - isHierarchical() { - if (_.isFunction(this.type.hierarchicalData)) { - return !!this.type.hierarchicalData(this); - } else { - return !!this.type.hierarchicalData; - } + isHierarchical() { + if (_.isFunction(this.type.hierarchicalData)) { + return !!this.type.hierarchicalData(this); + } else { + return !!this.type.hierarchicalData; } + } - hasSchemaAgg(schemaName, aggTypeName) { - const aggs = this.aggs.bySchemaName(schemaName) || []; - return aggs.some(function (agg) { - if (!agg.type || !agg.type.name) return false; - return agg.type.name === aggTypeName; - }); - } + hasSchemaAgg(schemaName, aggTypeName) { + const aggs = this.aggs.bySchemaName(schemaName) || []; + return aggs.some(function (agg) { + if (!agg.type || !agg.type.name) return false; + return agg.type.name === aggTypeName; + }); + } - hasUiState() { - return !!this.__uiState; - } + hasUiState() { + return !!this.__uiState; + } - /*** - * this should not be used outside of visualize - * @param uiState - * @private - */ - _setUiState(uiState) { - if (uiState instanceof PersistedState) { - this.__uiState = uiState; - } + /*** + * this should not be used outside of visualize + * @param uiState + * @private + */ + _setUiState(uiState) { + if (uiState instanceof PersistedState) { + this.__uiState = uiState; } + } - getUiState() { - return this.__uiState; - } + getUiState() { + return this.__uiState; + } - /** - * Currently this is only used to extract map-specific information - * (e.g. mapZoom, mapCenter). - */ - uiStateVal(key, val) { - if (this.hasUiState()) { - if (_.isUndefined(val)) { - return this.__uiState.get(key); - } - return this.__uiState.set(key, val); + /** + * Currently this is only used to extract map-specific information + * (e.g. mapZoom, mapCenter). + */ + uiStateVal(key, val) { + if (this.hasUiState()) { + if (_.isUndefined(val)) { + return this.__uiState.get(key); } - return val; + return this.__uiState.set(key, val); } + return val; } +} - Vis.prototype.type = 'histogram'; +Vis.prototype.type = 'histogram'; - return Vis; -} +export { Vis }; diff --git a/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js b/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js index 18eef99f05a753..afb3fea15a4307 100644 --- a/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js +++ b/src/legacy/ui/public/vis/vis_types/__tests__/vislib_vis_legend.js @@ -21,7 +21,7 @@ import $ from 'jquery'; import _ from 'lodash'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { VisProvider } from '../../vis'; +import { Vis } from '../../vis'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('visualize_legend directive', function () { @@ -29,7 +29,6 @@ describe('visualize_legend directive', function () { let $compile; let $timeout; let $el; - let Vis; let indexPattern; let fixtures; @@ -39,7 +38,6 @@ describe('visualize_legend directive', function () { $compile = $injector.get('$compile'); $timeout = $injector.get('$timeout'); fixtures = require('fixtures/fake_hierarchical_data'); - Vis = Private(VisProvider); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); })); diff --git a/src/legacy/ui/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/ui/public/vislib/__tests__/visualizations/pie_chart.js index a4f62e6ceb4310..5692ae65af2816 100644 --- a/src/legacy/ui/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/legacy/ui/public/vislib/__tests__/visualizations/pie_chart.js @@ -24,7 +24,7 @@ import _ from 'lodash'; import fixtures from 'fixtures/fake_hierarchical_data'; import $ from 'jquery'; import FixturesVislibVisFixtureProvider from 'fixtures/vislib/_vis_fixture'; -import { VisProvider } from '../../../vis'; +import { Vis } from '../../../vis'; import '../../../persisted_state'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { vislibSlicesResponseHandlerProvider } from '../../../vis/response_handlers/vislib'; @@ -113,7 +113,6 @@ describe('No global chart settings', function () { addTooltip: true }; let chart1; - let Vis; let persistedState; let indexPattern; let responseHandler; @@ -123,7 +122,6 @@ describe('No global chart settings', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { chart1 = Private(FixturesVislibVisFixtureProvider)(visLibParams1); - Vis = Private(VisProvider); persistedState = new ($injector.get('PersistedState'))(); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); responseHandler = vislibSlicesResponseHandlerProvider().handler; @@ -203,7 +201,6 @@ describe('Vislib PieChart Class Test Suite', function () { addTooltip: true }; let vis; - let Vis; let persistedState; let indexPattern; let data; @@ -213,7 +210,6 @@ describe('Vislib PieChart Class Test Suite', function () { beforeEach(ngMock.module('kibana')); beforeEach(ngMock.inject(function (Private, $injector) { vis = Private(FixturesVislibVisFixtureProvider)(visLibParams); - Vis = Private(VisProvider); persistedState = new ($injector.get('PersistedState'))(); indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); responseHandler = vislibSlicesResponseHandlerProvider().handler; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts index 056f3e8cfc586a..70e0c1f1382fad 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts @@ -172,7 +172,10 @@ describe('visualize loader pipeline helpers: build pipeline', () => { const visState = { ...visStateDef, params: { foo: 'bar' } }; const schemas = { ...schemasDef, - metric: [{ ...schemaConfig, accessor: 0 }, { ...schemaConfig, accessor: 1 }], + metric: [ + { ...schemaConfig, accessor: 0 }, + { ...schemaConfig, accessor: 1 }, + ], }; const actual = buildPipelineVisFunction.table(visState, schemas, uiState); expect(actual).toMatchSnapshot(); @@ -192,7 +195,10 @@ describe('visualize loader pipeline helpers: build pipeline', () => { const visState = { ...visStateDef, params: { foo: 'bar' } }; const schemas = { ...schemasDef, - metric: [{ ...schemaConfig, accessor: 0 }, { ...schemaConfig, accessor: 1 }], + metric: [ + { ...schemaConfig, accessor: 0 }, + { ...schemaConfig, accessor: 1 }, + ], split_row: [2, 4], bucket: [3], }; @@ -250,7 +256,10 @@ describe('visualize loader pipeline helpers: build pipeline', () => { const visState = { ...visStateDef, params: { metric: {} } }; const schemas = { ...schemasDef, - metric: [{ ...schemaConfig, accessor: 0 }, { ...schemaConfig, accessor: 1 }], + metric: [ + { ...schemaConfig, accessor: 0 }, + { ...schemaConfig, accessor: 1 }, + ], }; const actual = buildPipelineVisFunction.metric(visState, schemas, uiState); expect(actual).toMatchSnapshot(); @@ -260,7 +269,10 @@ describe('visualize loader pipeline helpers: build pipeline', () => { const visState = { ...visStateDef, params: { metric: {} } }; const schemas = { ...schemasDef, - metric: [{ ...schemaConfig, accessor: 0 }, { ...schemaConfig, accessor: 1 }], + metric: [ + { ...schemaConfig, accessor: 0 }, + { ...schemaConfig, accessor: 1 }, + ], group: [{ accessor: 2 }], }; const actual = buildPipelineVisFunction.metric(visState, schemas, uiState); diff --git a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts index 912afab74bef45..36759551a17236 100644 --- a/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts +++ b/src/legacy/ui/public/visualize/loader/utils/query_geohash_bounds.ts @@ -22,11 +22,10 @@ import { get } from 'lodash'; import { toastNotifications } from 'ui/notify'; import { AggConfig } from 'ui/vis'; -import { Query } from 'src/legacy/core_plugins/data/public'; import { timefilter } from 'ui/timefilter'; import { Vis } from '../../../vis'; +import { esFilters, Query } from '../../../../../../plugins/data/public'; import { SearchSource } from '../../../courier'; -import { esFilters } from '../../../../../../plugins/data/public'; interface QueryGeohashBoundsParams { filters?: esFilters.Filter[]; diff --git a/src/legacy/ui/public/visualize/loader/vis.js b/src/legacy/ui/public/visualize/loader/vis.js index 1942fd58afebbd..29208563055d00 100644 --- a/src/legacy/ui/public/visualize/loader/vis.js +++ b/src/legacy/ui/public/visualize/loader/vis.js @@ -33,114 +33,109 @@ import { PersistedState } from '../../persisted_state'; import { start as visualizations } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy'; -export function VisProvider(getAppState) { - const visTypes = visualizations.types; - - class Vis extends EventEmitter { - constructor(visState = { type: 'histogram' }) { - super(); - - this._setUiState(new PersistedState()); - this.setState(visState); - - // Session state is for storing information that is transitory, and will not be saved with the visualization. - // For instance, map bounds, which depends on the view port, browser window size, etc. - this.sessionState = {}; - - this.API = { - events: { - filter: data => { - if (!this.eventsSubject) return; - this.eventsSubject.next({ name: 'filterBucket', data }); - }, - brush: data => { - if (!this.eventsSubject) return; - this.eventsSubject.next({ name: 'brush', data }); - }, +const visTypes = visualizations.types; + +export class Vis extends EventEmitter { + constructor(visState = { type: 'histogram' }) { + super(); + + this._setUiState(new PersistedState()); + this.setState(visState); + + // Session state is for storing information that is transitory, and will not be saved with the visualization. + // For instance, map bounds, which depends on the view port, browser window size, etc. + this.sessionState = {}; + + this.API = { + events: { + filter: data => { + if (!this.eventsSubject) return; + this.eventsSubject.next({ name: 'filterBucket', data }); }, - getAppState, - }; - } + brush: data => { + if (!this.eventsSubject) return; + this.eventsSubject.next({ name: 'brush', data }); + }, + }, + }; + } - setState(state) { - this.title = state.title || ''; - const type = state.type || this.type; - if (_.isString(type)) { - this.type = visTypes.get(type); - if (!this.type) { - throw new Error(`Invalid type "${type}"`); - } - } else { - this.type = type; + setState(state) { + this.title = state.title || ''; + const type = state.type || this.type; + if (_.isString(type)) { + this.type = visTypes.get(type); + if (!this.type) { + throw new Error(`Invalid type "${type}"`); } - - this.params = _.defaultsDeep({}, - _.cloneDeep(state.params || {}), - _.cloneDeep(this.type.visConfig.defaults || {}) - ); + } else { + this.type = type; } - setCurrentState(state) { - this.setState(state); - } + this.params = _.defaultsDeep({}, + _.cloneDeep(state.params || {}), + _.cloneDeep(this.type.visConfig.defaults || {}) + ); + } - getState() { - return { - title: this.title, - type: this.type.name, - params: _.cloneDeep(this.params), - }; - } + setCurrentState(state) { + this.setState(state); + } - updateState() { - this.emit('update'); - } + getState() { + return { + title: this.title, + type: this.type.name, + params: _.cloneDeep(this.params), + }; + } - forceReload() { - this.emit('reload'); - } + updateState() { + this.emit('update'); + } - isHierarchical() { - if (_.isFunction(this.type.hierarchicalData)) { - return !!this.type.hierarchicalData(this); - } else { - return !!this.type.hierarchicalData; - } - } + forceReload() { + this.emit('reload'); + } - hasUiState() { - return !!this.__uiState; + isHierarchical() { + if (_.isFunction(this.type.hierarchicalData)) { + return !!this.type.hierarchicalData(this); + } else { + return !!this.type.hierarchicalData; } + } - /*** - * this should not be used outside of visualize - * @param uiState - * @private - */ - _setUiState(uiState) { - if (uiState instanceof PersistedState) { - this.__uiState = uiState; - } - } + hasUiState() { + return !!this.__uiState; + } - getUiState() { - return this.__uiState; + /*** + * this should not be used outside of visualize + * @param uiState + * @private + */ + _setUiState(uiState) { + if (uiState instanceof PersistedState) { + this.__uiState = uiState; } + } - /** - * Currently this is only used to extract map-specific information - * (e.g. mapZoom, mapCenter). - */ - uiStateVal(key, val) { - if (this.hasUiState()) { - if (_.isUndefined(val)) { - return this.__uiState.get(key); - } - return this.__uiState.set(key, val); + getUiState() { + return this.__uiState; + } + + /** + * Currently this is only used to extract map-specific information + * (e.g. mapZoom, mapCenter). + */ + uiStateVal(key, val) { + if (this.hasUiState()) { + if (_.isUndefined(val)) { + return this.__uiState.get(key); } - return val; + return this.__uiState.set(key, val); } + return val; } - - return Vis; } diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx index 02e5f45fae3bd6..36efd0bcba676a 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_flyout.tsx @@ -19,15 +19,9 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiGlobalToastListToast as Toast, -} from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import { DashboardPanelState } from '../embeddable'; -import { NotificationsStart } from '../../../../core/public'; +import { NotificationsStart, Toast } from '../../../../core/public'; import { IContainer, IEmbeddable, diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx index 8b258f35584388..6cefd11c912f19 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/dashboard_container.tsx @@ -20,7 +20,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { RefreshInterval, TimeRange, Query } from '../../../data/public'; +import { RefreshInterval, TimeRange, Query, esFilters } from '../../../data/public'; import { CoreStart } from '../../../../core/public'; import { IUiActionsStart } from '../ui_actions_plugin'; import { @@ -37,7 +37,6 @@ import { createPanelState } from './panel'; import { DashboardPanelState } from './types'; import { DashboardViewport } from './viewport/dashboard_viewport'; import { Start as InspectorStartContract } from '../../../inspector/public'; -import { esFilters } from '../../../../plugins/data/public'; import { KibanaContextProvider, KibanaReactContext, diff --git a/src/plugins/dashboard_embeddable_container/public/plugin.tsx b/src/plugins/dashboard_embeddable_container/public/plugin.tsx index eadf70a36416a6..dbb5a06da9cd94 100644 --- a/src/plugins/dashboard_embeddable_container/public/plugin.tsx +++ b/src/plugins/dashboard_embeddable_container/public/plugin.tsx @@ -61,9 +61,10 @@ export class DashboardEmbeddableContainerPublicPlugin const { application, notifications, overlays } = core; const { embeddable, inspector, uiActions } = plugins; - const SavedObjectFinder: React.FC< - Exclude - > = props => ( + const SavedObjectFinder: React.FC> = props => ( string); params?: any; + value?: string | ((formatter?: FilterValueFormatter) => string); } export interface Filter { diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.test.ts b/src/plugins/data/common/es_query/filters/phrase_filter.test.ts index ec13e28c583d18..250ec792fbb573 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/phrase_filter.test.ts @@ -18,7 +18,7 @@ */ import { buildInlineScriptForPhraseFilter, buildPhraseFilter } from './phrase_filter'; -import { IndexPattern } from './types'; +import { IndexPattern } from '../../types'; import { getField } from '../__tests__/fields_mock'; describe('Phrase filter builder', () => { diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.ts b/src/plugins/data/common/es_query/filters/phrase_filter.ts index 15c5c9d4ad2e6d..35110c924fe611 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrase_filter.ts @@ -19,7 +19,7 @@ import { get, isPlainObject } from 'lodash'; import { Filter, FilterMeta } from './meta_filter'; -import { IndexPattern, Field } from './types'; +import { IndexPattern, Field } from '../../types'; export type PhraseFilterMeta = FilterMeta & { params?: { diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/src/plugins/data/common/es_query/filters/phrases_filter.ts index e4606695c0f6a5..e207a3ff5961be 100644 --- a/src/plugins/data/common/es_query/filters/phrases_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrases_filter.ts @@ -18,7 +18,7 @@ */ import { Filter, FilterMeta } from './meta_filter'; -import { Field, IndexPattern } from './types'; +import { Field, IndexPattern } from '../../types'; import { getPhraseScript } from './phrase_filter'; export type PhrasesFilterMeta = FilterMeta & { diff --git a/src/plugins/data/common/es_query/filters/query_string_filter.test.ts b/src/plugins/data/common/es_query/filters/query_string_filter.test.ts index 839e4f6359257e..5a580db0c57b82 100644 --- a/src/plugins/data/common/es_query/filters/query_string_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/query_string_filter.test.ts @@ -18,7 +18,7 @@ */ import { buildQueryFilter } from './query_string_filter'; -import { IndexPattern } from './types'; +import { IndexPattern } from '../../types'; describe('Phrase filter builder', () => { let indexPattern: IndexPattern; diff --git a/src/plugins/data/common/es_query/filters/query_string_filter.ts b/src/plugins/data/common/es_query/filters/query_string_filter.ts index 901dc724aa4e49..d2374162b195f4 100644 --- a/src/plugins/data/common/es_query/filters/query_string_filter.ts +++ b/src/plugins/data/common/es_query/filters/query_string_filter.ts @@ -18,7 +18,7 @@ */ import { Filter, FilterMeta } from './meta_filter'; -import { IndexPattern } from './types'; +import { IndexPattern } from '../../types'; export type QueryStringFilterMeta = FilterMeta; diff --git a/src/plugins/data/common/es_query/filters/range_filter.test.ts b/src/plugins/data/common/es_query/filters/range_filter.test.ts index 9008dc2a672944..017bb8e9cb7c5b 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.test.ts @@ -19,7 +19,7 @@ import { each } from 'lodash'; import { buildRangeFilter, RangeFilter } from './range_filter'; -import { IndexPattern, Field } from './types'; +import { IndexPattern, Field } from '../../types'; import { getField } from '../__tests__/fields_mock'; describe('Range filter builder', () => { diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index d7931f191e52b1..c2513a9dc0c5e1 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -18,7 +18,7 @@ */ import { map, reduce, mapValues, get, keys, pick } from 'lodash'; import { Filter, FilterMeta } from './meta_filter'; -import { Field, IndexPattern } from './types'; +import { Field, IndexPattern } from '../../types'; const OPERANDS_IN_RANGE = 2; diff --git a/src/plugins/data/common/es_query/filters/types.ts b/src/plugins/data/common/es_query/filters/types.ts index 28147350619995..a242df4811c059 100644 --- a/src/plugins/data/common/es_query/filters/types.ts +++ b/src/plugins/data/common/es_query/filters/types.ts @@ -49,9 +49,3 @@ export enum FILTERS { GEO_BOUNDING_BOX = 'geo_bounding_box', GEO_POLYGON = 'geo_polygon', } - -// We can't import the real types from the data plugin, so need to either duplicate -// them here or figure out another solution, perhaps housing them in this package -// will be replaces after Fieds / IndexPattern will be moved into new platform -export type Field = any; -export type IndexPattern = any; diff --git a/src/plugins/data/common/types.ts b/src/plugins/data/common/types.ts index 9eda75d8abd0b6..ec8d8b006317fd 100644 --- a/src/plugins/data/common/types.ts +++ b/src/plugins/data/common/types.ts @@ -21,3 +21,10 @@ export * from './field_formats/types'; export * from './timefilter/types'; export * from './query/types'; export * from './kbn_field_types/types'; + +// We can't import the real types from the data plugin, so need to either duplicate +// them here or figure out another solution, perhaps housing them in this package +// will be replaces after Fieds / IndexPattern will be moved into new platform +export type Field = any; +export type IndexPattern = any; +export type StaticIndexPattern = any; diff --git a/src/plugins/data/public/autocomplete_provider/types.ts b/src/plugins/data/public/autocomplete_provider/types.ts index 1f2d8f914dde3e..d838e54e9ead49 100644 --- a/src/plugins/data/public/autocomplete_provider/types.ts +++ b/src/plugins/data/public/autocomplete_provider/types.ts @@ -17,8 +17,8 @@ * under the License. */ -import { StaticIndexPattern, Field } from 'ui/index_patterns'; import { AutocompleteProviderRegister } from '.'; +import { Field, StaticIndexPattern } from '..'; export type AutocompletePublicPluginSetup = Pick< AutocompleteProviderRegister, diff --git a/src/plugins/data/public/index_patterns/field.stub.ts b/src/plugins/data/public/index_patterns/field.stub.ts new file mode 100644 index 00000000000000..315894cd212c40 --- /dev/null +++ b/src/plugins/data/public/index_patterns/field.stub.ts @@ -0,0 +1,79 @@ +/* + * 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 { Field } from '../../common'; + +export const stubFields: Field[] = [ + { + name: 'machine.os', + esTypes: ['text'], + type: 'string', + aggregatable: false, + searchable: false, + filterable: true, + }, + { + name: 'machine.os.raw', + type: 'string', + esTypes: ['keyword'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'not.filterable', + type: 'string', + esTypes: ['text'], + aggregatable: true, + searchable: false, + filterable: false, + }, + { + name: 'bytes', + type: 'number', + esTypes: ['long'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: '@timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'clientip', + type: 'ip', + esTypes: ['ip'], + aggregatable: true, + searchable: true, + filterable: true, + }, + { + name: 'bool.field', + type: 'boolean', + esTypes: ['boolean'], + aggregatable: true, + searchable: true, + filterable: true, + }, +]; diff --git a/src/legacy/core_plugins/console/public/quarantined/hacks/register.js b/src/plugins/data/public/index_patterns/index_pattern.stub.ts similarity index 72% rename from src/legacy/core_plugins/console/public/quarantined/hacks/register.js rename to src/plugins/data/public/index_patterns/index_pattern.stub.ts index b5df1c1af99c5b..444e65cd0cd4b9 100644 --- a/src/legacy/core_plugins/console/public/quarantined/hacks/register.js +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -17,14 +17,12 @@ * under the License. */ -import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; -import { i18n } from '@kbn/i18n'; +import { IndexPattern } from '../../common'; +import { stubFields } from './field.stub'; -DevToolsRegistryProvider.register(() => ({ - order: 1, - name: 'console', - display: i18n.translate('console.consoleDisplayName', { - defaultMessage: 'Console', - }), - url: '#/dev_tools/console', -})); +export const stubIndexPattern: IndexPattern = { + id: 'logstash-*', + fields: stubFields, + title: 'logstash-*', + timeFieldName: '@timestamp', +}; diff --git a/src/plugins/data/public/query/filter_manager/index.ts b/src/plugins/data/public/query/filter_manager/index.ts index 7955cdd825ee6c..ce7a479151797c 100644 --- a/src/plugins/data/public/query/filter_manager/index.ts +++ b/src/plugins/data/public/query/filter_manager/index.ts @@ -22,3 +22,4 @@ export { FilterManager } from './filter_manager'; export { uniqFilters } from './lib/uniq_filters'; export { mapAndFlattenFilters } from './lib/map_and_flatten_filters'; export { onlyDisabledFiltersChanged } from './lib/only_disabled'; +export { generateFilters } from './lib/generate_filters'; diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts new file mode 100644 index 00000000000000..46cf0fd9c111ec --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filter.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { generateFilters } from './generate_filters'; +import { FilterManager } from '../filter_manager'; +import { esFilters } from '../../..'; + +const INDEX_NAME = 'my-index'; +const EXISTS_FIELD_NAME = '_exists_'; +const FIELD = { + name: 'my-field', +}; +const PHRASE_VALUE = 'my-value'; + +describe('Generate filters', () => { + let mockFilterManager: FilterManager; + let filtersArray: esFilters.Filter[]; + + beforeEach(() => { + filtersArray = []; + mockFilterManager = { + getAppFilters: () => { + return filtersArray; + }, + } as FilterManager; + }); + + it('should create exists filter', () => { + const filters = generateFilters( + mockFilterManager, + EXISTS_FIELD_NAME, + FIELD.name, + '', + INDEX_NAME + ); + expect(filters).toHaveLength(1); + expect(filters[0].meta.index === INDEX_NAME); + expect(filters[0].meta.negate).toBeFalsy(); + expect(esFilters.isExistsFilter(filters[0])).toBeTruthy(); + }); + + it('should create negated exists filter', () => { + const filters = generateFilters( + mockFilterManager, + EXISTS_FIELD_NAME, + FIELD.name, + '-', + INDEX_NAME + ); + expect(filters).toHaveLength(1); + expect(filters[0].meta.index === INDEX_NAME); + expect(filters[0].meta.negate).toBeTruthy(); + expect(esFilters.isExistsFilter(filters[0])).toBeTruthy(); + }); + + it('should update and re-enable EXISTING exists filter', () => { + const filter = esFilters.buildExistsFilter(FIELD, { id: INDEX_NAME }); + filter.meta.disabled = true; + filtersArray.push(filter); + + const filters = generateFilters(mockFilterManager, '_exists_', FIELD.name, '-', INDEX_NAME); + expect(filters).toHaveLength(1); + expect(filters[0].meta.index === INDEX_NAME); + expect(filters[0].meta.negate).toBeTruthy(); + expect(filters[0].meta.disabled).toBeFalsy(); + expect(esFilters.isExistsFilter(filters[0])).toBeTruthy(); + }); + + it('should create phrase filter', () => { + const filters = generateFilters(mockFilterManager, FIELD, PHRASE_VALUE, '', INDEX_NAME); + expect(filters).toHaveLength(1); + expect(filters[0].meta.index === INDEX_NAME); + expect(filters[0].meta.negate).toBeFalsy(); + expect(esFilters.isPhraseFilter(filters[0])).toBeTruthy(); + expect((filters[0] as esFilters.PhraseFilter).query.match_phrase).toEqual({ + [FIELD.name]: PHRASE_VALUE, + }); + }); + + it('should create negated phrase filter', () => { + const filters = generateFilters(mockFilterManager, FIELD, PHRASE_VALUE, '-', INDEX_NAME); + expect(filters).toHaveLength(1); + expect(filters[0].meta.index === INDEX_NAME); + expect(filters[0].meta.negate).toBeTruthy(); + expect(esFilters.isPhraseFilter(filters[0])).toBeTruthy(); + expect((filters[0] as esFilters.PhraseFilter).query.match_phrase).toEqual({ + [FIELD.name]: PHRASE_VALUE, + }); + }); + + it('should create multiple phrase filters', () => { + const ANOTHER_PHRASE = 'another-value'; + const filters = generateFilters( + mockFilterManager, + FIELD, + [PHRASE_VALUE, ANOTHER_PHRASE], + '', + INDEX_NAME + ); + expect(filters).toHaveLength(2); + expect(filters[0].meta.index === INDEX_NAME); + expect(filters[0].meta.negate).toBeFalsy(); + expect(filters[1].meta.index === INDEX_NAME); + expect(filters[1].meta.negate).toBeFalsy(); + expect(esFilters.isPhraseFilter(filters[0])).toBeTruthy(); + expect(esFilters.isPhraseFilter(filters[1])).toBeTruthy(); + expect((filters[0] as esFilters.PhraseFilter).query.match_phrase).toEqual({ + [FIELD.name]: PHRASE_VALUE, + }); + expect((filters[1] as esFilters.PhraseFilter).query.match_phrase).toEqual({ + [FIELD.name]: ANOTHER_PHRASE, + }); + }); +}); diff --git a/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts new file mode 100644 index 00000000000000..5c4cdc2717338f --- /dev/null +++ b/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts @@ -0,0 +1,112 @@ +/* + * 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 _ from 'lodash'; +import { FilterManager, esFilters, Field } from '../../..'; + +function getExistingFilter( + appFilters: esFilters.Filter[], + fieldName: string, + value: any +): esFilters.Filter | undefined { + // TODO: On array fields, negating does not negate the combination, rather all terms + return _.find(appFilters, function(filter) { + if (!filter) return; + + if (fieldName === '_exists_' && esFilters.isExistsFilter(filter)) { + return filter.exists!.field === value; + } + + if (esFilters.isPhraseFilter(filter)) { + return ( + esFilters.getPhraseFilterField(filter) === fieldName && + esFilters.getPhraseFilterValue(filter) === value + ); + } + + if (esFilters.isScriptedPhraseFilter(filter)) { + return filter.meta.field === fieldName && filter.meta.script!.script.params.value === value; + } + }); +} + +function updateExistingFilter(existingFilter: esFilters.Filter, negate: boolean) { + existingFilter.meta.disabled = false; + if (existingFilter.meta.negate !== negate) { + existingFilter.meta.negate = !existingFilter.meta.negate; + } +} + +/** + * Generate filter objects, as a result of triggering a filter action on a + * specific index pattern field. + * + * @param {FilterManager} filterManager - The active filter manager to lookup for existing filters + * @param {Field | string} field - The field for which filters should be generated + * @param {any} values - One or more values to filter for. + * @param {string} operation - "-" to create a negated filter + * @param {string} index - Index string to generate filters for + * + * @returns {object} An array of filters to be added back to filterManager + */ +export function generateFilters( + filterManager: FilterManager, + field: Field | string, + values: any, + operation: string, + index: string +): esFilters.Filter[] { + values = Array.isArray(values) ? values : [values]; + const fieldObj = _.isObject(field) + ? field + : { + name: field, + }; + const fieldName = fieldObj.name; + const newFilters: esFilters.Filter[] = []; + const appFilters = filterManager.getAppFilters(); + + const negate = operation === '-'; + let filter; + + _.each(values, function(value) { + const existing = getExistingFilter(appFilters, fieldName, value); + + if (existing) { + updateExistingFilter(existing, negate); + filter = existing; + } else { + const tmpIndexPattern = { id: index }; + switch (fieldName) { + case '_exists_': + filter = esFilters.buildExistsFilter(fieldObj, tmpIndexPattern); + break; + default: + filter = esFilters.buildPhraseFilter(fieldObj, value, tmpIndexPattern); + break; + } + + filter.meta.negate = negate; + } + + newFilters.push(filter); + }); + + return newFilters; +} diff --git a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.test.ts b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.test.ts index 3afa3891a24bb8..1847296016c732 100644 --- a/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.test.ts +++ b/src/plugins/data/public/query/filter_manager/lib/mappers/map_geo_polygon.test.ts @@ -30,7 +30,10 @@ describe('filter manager utilities', () => { }, geo_polygon: { point: { - points: [{ lat: 5, lon: 10 }, { lat: 15, lon: 20 }], + points: [ + { lat: 5, lon: 10 }, + { lat: 15, lon: 20 }, + ], }, }, } as esFilters.GeoPolygonFilter; diff --git a/src/plugins/data/public/query/index.tsx b/src/plugins/data/public/query/index.tsx index 9d7c2ffc56f70b..224c2f3a04076b 100644 --- a/src/plugins/data/public/query/index.tsx +++ b/src/plugins/data/public/query/index.tsx @@ -17,6 +17,8 @@ * under the License. */ +export * from './lib'; + export * from './query_service'; export * from './filter_manager'; diff --git a/src/plugins/data/public/query/lib/from_user.test.ts b/src/plugins/data/public/query/lib/from_user.test.ts new file mode 100644 index 00000000000000..b74a1a07dc356d --- /dev/null +++ b/src/plugins/data/public/query/lib/from_user.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { fromUser } from '../'; + +describe('user input helpers', function() { + describe('user input parser', function() { + it('should return the input if passed an object', function() { + expect(fromUser({ foo: 'bar' })).toEqual({ foo: 'bar' }); + }); + + it('unless the object is empty, then convert it to an empty string', function() { + expect(fromUser({})).toEqual(''); + }); + + it('should pass through input strings that not start with {', function() { + expect(fromUser('foo')).toEqual('foo'); + expect(fromUser('400')).toEqual('400'); + expect(fromUser('true')).toEqual('true'); + }); + + it('should parse valid JSON and return the object instead of a string', function() { + expect(fromUser('{}')).toEqual({}); + + // invalid json remains a string + expect(fromUser('{a:b}')).toEqual('{a:b}'); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/lib/from_user.ts b/src/plugins/data/public/query/lib/from_user.ts similarity index 96% rename from src/legacy/core_plugins/data/public/query/query_bar/lib/from_user.ts rename to src/plugins/data/public/query/lib/from_user.ts index 15eebaa0b9fd63..fbb1726fc99ea2 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/lib/from_user.ts +++ b/src/plugins/data/public/query/lib/from_user.ts @@ -28,6 +28,10 @@ import _ from 'lodash'; export function fromUser(userInput: object | string) { const matchAll = ''; + if (_.isEmpty(userInput)) { + return ''; + } + if (_.isObject(userInput)) { return userInput; } diff --git a/src/legacy/core_plugins/data/public/query/query_bar/lib/get_query_log.ts b/src/plugins/data/public/query/lib/get_query_log.ts similarity index 94% rename from src/legacy/core_plugins/data/public/query/query_bar/lib/get_query_log.ts rename to src/plugins/data/public/query/lib/get_query_log.ts index 66424d9a1d6a35..67073a9078046c 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/lib/get_query_log.ts +++ b/src/plugins/data/public/query/lib/get_query_log.ts @@ -19,7 +19,7 @@ import { UiSettingsClientContract } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { PersistedLog } from '../../../../../../../plugins/data/public'; +import { PersistedLog } from '../persisted_log'; export function getQueryLog( uiSettings: UiSettingsClientContract, diff --git a/src/legacy/core_plugins/data/public/query/query_bar/lib/index.ts b/src/plugins/data/public/query/lib/index.ts similarity index 93% rename from src/legacy/core_plugins/data/public/query/query_bar/lib/index.ts rename to src/plugins/data/public/query/lib/index.ts index 852f9fd269b32b..19ac37fb59ae75 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/lib/index.ts +++ b/src/plugins/data/public/query/lib/index.ts @@ -20,3 +20,5 @@ export * from './match_pairs'; export * from './from_user'; export * from './to_user'; +export * from './to_user'; +export * from './get_query_log'; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/lib/match_pairs.ts b/src/plugins/data/public/query/lib/match_pairs.ts similarity index 100% rename from src/legacy/core_plugins/data/public/query/query_bar/lib/match_pairs.ts rename to src/plugins/data/public/query/lib/match_pairs.ts diff --git a/src/plugins/data/public/query/lib/to_user.test.ts b/src/plugins/data/public/query/lib/to_user.test.ts new file mode 100644 index 00000000000000..d13afa251ecb1d --- /dev/null +++ b/src/plugins/data/public/query/lib/to_user.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { toUser } from '../'; + +describe('user input helpers', function() { + describe('model presentation formatter', function() { + it('should present objects as strings', function() { + expect(toUser({ foo: 'bar' })).toBe('{"foo":"bar"}'); + }); + + it('should present query_string queries as strings', function() { + expect(toUser({ query_string: { query: 'lucene query string' } })).toBe( + 'lucene query string' + ); + }); + + it('should present query_string queries without a query as an empty string', function() { + expect(toUser({ query_string: {} })).toBe(''); + }); + + it('should present string as strings', function() { + expect(toUser('foo')).toBe('foo'); + }); + + it('should present numbers as strings', function() { + expect(toUser(400)).toBe('400'); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/query/query_bar/lib/to_user.ts b/src/plugins/data/public/query/lib/to_user.ts similarity index 89% rename from src/legacy/core_plugins/data/public/query/query_bar/lib/to_user.ts rename to src/plugins/data/public/query/lib/to_user.ts index 1eb054ba405142..1fdb2d8ed03df1 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/lib/to_user.ts +++ b/src/plugins/data/public/query/lib/to_user.ts @@ -17,14 +17,12 @@ * under the License. */ -import angular from 'angular'; - /** * Take text from the model and present it to the user as a string * @param text model value * @returns {string} */ -export function toUser(text: { [key: string]: any } | string): string { +export function toUser(text: { [key: string]: any } | string | number): string { if (text == null) { return ''; } @@ -35,7 +33,7 @@ export function toUser(text: { [key: string]: any } | string): string { if (text.query_string) { return toUser(text.query_string.query); } - return angular.toJson(text); + return JSON.stringify(text); } return '' + text; } diff --git a/src/plugins/data/public/query/persisted_log/persisted_log.test.ts b/src/plugins/data/public/query/persisted_log/persisted_log.test.ts index e0bc8f2c3525f5..87c1ec29c1aee1 100644 --- a/src/plugins/data/public/query/persisted_log/persisted_log.test.ts +++ b/src/plugins/data/public/query/persisted_log/persisted_log.test.ts @@ -36,12 +36,6 @@ const createMockStorage = () => ({ clear: jest.fn(), }); -jest.mock('ui/chrome', () => { - return { - getBasePath: () => `/some/base/path`, - }; -}); - const historyName = 'testHistory'; const historyLimit = 10; const payload = [ diff --git a/src/plugins/data/public/search/create_app_mount_context_search.test.ts b/src/plugins/data/public/search/create_app_mount_context_search.test.ts index deab9af4e3a018..fa7cdbcda3082b 100644 --- a/src/plugins/data/public/search/create_app_mount_context_search.test.ts +++ b/src/plugins/data/public/search/create_app_mount_context_search.test.ts @@ -62,8 +62,10 @@ describe('Create app mount search context', () => { }); }, }); - context - .search({ greeting: 'hi' } as any, {}, 'mysearch') - .subscribe(response => {}, () => {}, done); + context.search({ greeting: 'hi' } as any, {}, 'mysearch').subscribe( + response => {}, + () => {}, + done + ); }); }); diff --git a/src/plugins/data/public/search/es_search/es_search_strategy.ts b/src/plugins/data/public/search/es_search/es_search_strategy.ts index 643ded120799e0..d29f3b6882b26b 100644 --- a/src/plugins/data/public/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/public/search/es_search/es_search_strategy.ts @@ -20,6 +20,7 @@ import { Observable } from 'rxjs'; import { ES_SEARCH_STRATEGY, IEsSearchResponse } from '../../../common/search'; import { SYNC_SEARCH_STRATEGY } from '../sync_search_strategy'; +import { getEsPreference } from './get_es_preference'; import { TSearchStrategyProvider, ISearchStrategy, ISearchGeneric, ISearchContext } from '..'; export const esSearchStrategyProvider: TSearchStrategyProvider = ( @@ -27,11 +28,17 @@ export const esSearchStrategyProvider: TSearchStrategyProvider => { return { - search: (request, options) => - search( + search: (request, options) => { + if (typeof request.params.preference === 'undefined') { + const setPreference = context.core.uiSettings.get('courier:setRequestPreference'); + const customPreference = context.core.uiSettings.get('courier:customRequestPreference'); + request.params.preference = getEsPreference(setPreference, customPreference); + } + return search( { ...request, serverStrategy: ES_SEARCH_STRATEGY }, options, SYNC_SEARCH_STRATEGY - ) as Observable, + ) as Observable; + }, }; }; diff --git a/src/plugins/data/public/search/es_search/get_es_preference.test.ts b/src/plugins/data/public/search/es_search/get_es_preference.test.ts new file mode 100644 index 00000000000000..27e6f9b48bbdd5 --- /dev/null +++ b/src/plugins/data/public/search/es_search/get_es_preference.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { getEsPreference } from './get_es_preference'; + +jest.useFakeTimers(); + +describe('Get ES preference', () => { + test('returns the session ID if set to sessionId', () => { + const setPreference = 'sessionId'; + const customPreference = 'foobar'; + const sessionId = 'my_session_id'; + const preference = getEsPreference(setPreference, customPreference, sessionId); + expect(preference).toBe(sessionId); + }); + + test('returns the custom preference if set to custom', () => { + const setPreference = 'custom'; + const customPreference = 'foobar'; + const preference = getEsPreference(setPreference, customPreference); + expect(preference).toBe(customPreference); + }); + + test('returns undefined if set to none', () => { + const setPreference = 'none'; + const customPreference = 'foobar'; + const preference = getEsPreference(setPreference, customPreference); + expect(preference).toBe(undefined); + }); +}); diff --git a/src/plugins/data/public/search/es_search/get_es_preference.ts b/src/plugins/data/public/search/es_search/get_es_preference.ts new file mode 100644 index 00000000000000..200e5bacb7f182 --- /dev/null +++ b/src/plugins/data/public/search/es_search/get_es_preference.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +const defaultSessionId = `${Date.now()}`; + +export function getEsPreference( + setRequestPreference: string, + customRequestPreference?: string, + sessionId: string = defaultSessionId +) { + if (setRequestPreference === 'sessionId') return `${sessionId}`; + return setRequestPreference === 'custom' ? customRequestPreference : undefined; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/filter.js b/src/plugins/data/public/stubs.ts similarity index 80% rename from src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/filter.js rename to src/plugins/data/public/stubs.ts index 1a2854ec154120..40a5e7d18f8d9f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/filter.js +++ b/src/plugins/data/public/stubs.ts @@ -17,10 +17,5 @@ * under the License. */ -export function addFilter(field, values = [], operation, index, state, filterGen) { - if (!Array.isArray(values)) { - values = [values]; - } - - filterGen.add(field, values, operation, index); -} +export { stubIndexPattern } from './index_patterns/index_pattern.stub'; +export { stubFields } from './index_patterns/field.stub'; diff --git a/src/plugins/data/public/suggestions_provider/types.ts b/src/plugins/data/public/suggestions_provider/types.ts index eac380dde6a62f..988b5fcd43fa89 100644 --- a/src/plugins/data/public/suggestions_provider/types.ts +++ b/src/plugins/data/public/suggestions_provider/types.ts @@ -16,8 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - -// Should be import { Field } from './index_patterns'; -export type Field = any; +import { Field } from '..'; export type IGetSuggestions = (index: string, field: Field, query: string, boolFilter?: any) => any; diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts b/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts index 13ccbbd9f3ddea..7dc8ff0fe133d1 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts +++ b/src/plugins/data/public/suggestions_provider/value_suggestions.test.ts @@ -19,9 +19,8 @@ // TODO: remove when index patterns are moved here. jest.mock('ui/new_platform'); -jest.mock('ui/index_patterns'); -import { mockFields, mockIndexPattern } from 'ui/index_patterns'; +import { stubIndexPattern, stubFields } from '../stubs'; import { getSuggestionsProvider } from './value_suggestions'; import { UiSettingsClientContract } from 'kibana/public'; @@ -37,8 +36,8 @@ describe('getSuggestions', () => { }); it('should return an empty array', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields; + const index = stubIndexPattern.id; + const [field] = stubFields; const query = ''; const suggestions = await getSuggestions(index, field, query); expect(suggestions).toEqual([]); @@ -54,8 +53,8 @@ describe('getSuggestions', () => { }); it('should return true/false for boolean fields', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter(({ type }) => type === 'boolean'); + const index = stubIndexPattern.id; + const [field] = stubFields.filter(({ type }) => type === 'boolean'); const query = ''; const suggestions = await getSuggestions(index, field, query); expect(suggestions).toEqual([true, false]); @@ -63,8 +62,8 @@ describe('getSuggestions', () => { }); it('should return an empty array if the field type is not a string or boolean', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter(({ type }) => type !== 'string' && type !== 'boolean'); + const index = stubIndexPattern.id; + const [field] = stubFields.filter(({ type }) => type !== 'string' && type !== 'boolean'); const query = ''; const suggestions = await getSuggestions(index, field, query); expect(suggestions).toEqual([]); @@ -72,8 +71,8 @@ describe('getSuggestions', () => { }); it('should return an empty array if the field is not aggregatable', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter(({ aggregatable }) => !aggregatable); + const index = stubIndexPattern.id; + const [field] = stubFields.filter(({ aggregatable }) => !aggregatable); const query = ''; const suggestions = await getSuggestions(index, field, query); expect(suggestions).toEqual([]); @@ -81,8 +80,8 @@ describe('getSuggestions', () => { }); it('should otherwise request suggestions', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter( + const index = stubIndexPattern.id; + const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); const query = ''; @@ -91,8 +90,8 @@ describe('getSuggestions', () => { }); it('should cache results if using the same index/field/query/filter', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter( + const index = stubIndexPattern.id; + const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); const query = ''; @@ -102,8 +101,8 @@ describe('getSuggestions', () => { }); it('should cache results for only one minute', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter( + const index = stubIndexPattern.id; + const [field] = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); const query = ''; @@ -119,7 +118,7 @@ describe('getSuggestions', () => { }); it('should not cache results if using a different index/field/query', async () => { - const fields = mockFields.filter( + const fields = stubFields.filter( ({ type, aggregatable }) => type === 'string' && aggregatable ); await getSuggestions('index', fields[0], ''); diff --git a/src/plugins/data/public/suggestions_provider/value_suggestions.ts b/src/plugins/data/public/suggestions_provider/value_suggestions.ts index 03eaa5d9594d27..c769f64025b0ed 100644 --- a/src/plugins/data/public/suggestions_provider/value_suggestions.ts +++ b/src/plugins/data/public/suggestions_provider/value_suggestions.ts @@ -20,7 +20,8 @@ import { memoize } from 'lodash'; import { UiSettingsClientContract, HttpServiceBase } from 'src/core/public'; -import { IGetSuggestions, Field } from './types'; +import { IGetSuggestions } from './types'; +import { Field } from '..'; export function getSuggestionsProvider( uiSettings: UiSettingsClientContract, diff --git a/src/plugins/dev_tools/README.md b/src/plugins/dev_tools/README.md new file mode 100644 index 00000000000000..1610411b9c98e0 --- /dev/null +++ b/src/plugins/dev_tools/README.md @@ -0,0 +1,29 @@ +# Dev tools plugin + +The ui/registry/dev_tools is removed in favor of the `dev_tools` plugin which exposes a register method in the setup contract. +Registering app works mostly the same as registering apps in core.application.register. +Routing will be handled by the id of the dev tool - your dev tool will be mounted when the URL matches `/app/kibana#/dev_tools/`. +This API doesn't support angular, for registering angular dev tools, bootstrap a local module on mount into the given HTML element. + +During the migration this plugin exposes the registered dev tools in the start contract. This is necessary to keep the dev tools app +which is still living in the legacy platform working and will be removed once everything is moved over to the new platform. It should +not be used by other plugins. + +## Example registration + +```ts +// For legacy plugins +import { npSetup } from 'ui/new_platform'; +npSetup.plugins.dev_tools.register(/* same details here */); + +// For new plugins: first add 'dev_tools' to the list of `optionalPlugins` +// in your kibana.json file. Then access the plugin directly in `setup`: + +class MyPlugin { + setup(core, plugins) { + if (plugins.dev_tools) { + plugins.dev_tools.register(/* same details here. */); + } + } +} +``` diff --git a/src/plugins/dev_tools/kibana.json b/src/plugins/dev_tools/kibana.json new file mode 100644 index 00000000000000..307035c7ec6645 --- /dev/null +++ b/src/plugins/dev_tools/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "devTools", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/dev_tools/public/index.ts b/src/plugins/dev_tools/public/index.ts new file mode 100644 index 00000000000000..3a0d1455e2168f --- /dev/null +++ b/src/plugins/dev_tools/public/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { DevToolsPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new DevToolsPlugin(); +} + +export * from './plugin'; diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts new file mode 100644 index 00000000000000..8098308c0882bc --- /dev/null +++ b/src/plugins/dev_tools/public/plugin.ts @@ -0,0 +1,115 @@ +/* + * 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 { App, CoreSetup, Plugin } from 'kibana/public'; +import { sortBy } from 'lodash'; + +export interface DevToolsSetup { + /** + * Register a developer tool. It will be available + * in the dev tools app under a separate tab. + * + * Registering dev tools works almost similar to registering + * applications in the core application service, + * but they will be rendered with a frame containing tabs + * to switch between the tools. + * @param devTool The dev tools descriptor + */ + register: (devTool: DevTool) => void; +} + +export interface DevToolsStart { + /** + * Returns all registered dev tools in an ordered array. + * This function is only exposed because the dev tools app + * actually rendering the tool has to stay in the legacy platform + * for now. Once it is moved into this plugin, this function + * becomes an implementation detail. + * @deprecated + */ + getSortedDevTools: () => readonly DevTool[]; +} + +/** + * Descriptor for a dev tool. A dev tool works similar to an application + * registered in the core application service. + */ +export interface DevTool { + /** + * The id of the dev tools. This will become part of the URL path + * (`dev_tools/${devTool.id}`. It has to be unique among registered + * dev tools. + */ + id: string; + /** + * The human readable name of the dev tool. Should be internationalized. + * This will be used as a label in the tab above the actual tool. + */ + title: string; + mount: App['mount']; + /** + * Flag indicating to disable the tab of this dev tool. Navigating to a + * disabled dev tool will be treated as the navigation to an unknown route + * (redirect to the console). + */ + disabled?: boolean; + /** + * Optional tooltip content of the tab. + */ + tooltipContent?: string; + /** + * Flag indicating whether the dev tool will do routing within the `dev_tools/${devTool.id}/` + * prefix. If it is set to true, the dev tool is responsible to redirect + * the user when navigating to unknown URLs within the prefix. If set + * to false only the root URL of the dev tool will be recognized as valid. + */ + enableRouting: boolean; + /** + * Number used to order the tabs. + */ + order: number; +} + +export class DevToolsPlugin implements Plugin { + private readonly devTools = new Map(); + + private getSortedDevTools(): readonly DevTool[] { + return sortBy([...this.devTools.values()], 'order'); + } + + public setup(core: CoreSetup) { + return { + register: (devTool: DevTool) => { + if (this.devTools.has(devTool.id)) { + throw new Error( + `Dev tool with id [${devTool.id}] has already been registered. Use a unique id.` + ); + } + + this.devTools.set(devTool.id, devTool); + }, + }; + } + + public start() { + return { + getSortedDevTools: this.getSortedDevTools.bind(this), + }; + } +} 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 b11bd167e15f2e..70d7c99d3fb9d0 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -173,7 +173,7 @@ test('Can set title to an empty string', async () => { ); const inputField = findTestSubject(component, 'customizePanelHideTitle'); - inputField.simulate('change'); + inputField.simulate('click'); findTestSubject(component, 'saveNewTitleButton').simulate('click'); expect(inputField.props().value).toBeUndefined(); diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx index 417f3436a2c63c..0c075c497a4d07 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/toggle_field.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { FieldHook } from '../../hook_form_lib'; import { getFieldValidityAndErrorMessage } from '../helpers'; @@ -33,6 +33,14 @@ interface Props { export const ToggleField = ({ field, euiFieldProps = {}, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + // Shim for sufficient overlap between EuiSwitchEvent and FieldHook[onChange] event + const onChange = (e: EuiSwitchEvent) => { + const event = ({ ...e, value: `${e.target.checked}` } as unknown) as React.ChangeEvent<{ + value: string; + }>; + field.onChange(event); + }; + return ( { diff --git a/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts b/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts index e288f61de8681d..0bb89cc1af593b 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/serializers.ts @@ -69,24 +69,21 @@ export const stripEmptyFields = ( ): { [key: string]: any } => { const { types = ['string', 'object'], recursive = false } = options || {}; - return Object.entries(object).reduce( - (acc, [key, value]) => { - const type = typeof value; - const shouldStrip = types.includes(type as 'string'); + return Object.entries(object).reduce((acc, [key, value]) => { + const type = typeof value; + const shouldStrip = types.includes(type as 'string'); - if (shouldStrip && type === 'string' && value.trim() === '') { + if (shouldStrip && type === 'string' && value.trim() === '') { + return acc; + } else if (type === 'object' && !Array.isArray(value) && value !== null) { + if (Object.keys(value).length === 0 && shouldStrip) { return acc; - } else if (type === 'object' && !Array.isArray(value) && value !== null) { - if (Object.keys(value).length === 0 && shouldStrip) { - return acc; - } else if (recursive) { - value = stripEmptyFields({ ...value }, options); - } + } else if (recursive) { + value = stripEmptyFields({ ...value }, options); } + } - acc[key] = value; - return acc; - }, - {} as { [key: string]: any } - ); + acc[key] = value; + return acc; + }, {} as { [key: string]: any }); }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 360182368ae638..3902b0615a33d8 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -65,15 +65,12 @@ export function useForm( const stripEmptyFields = (fields: FieldsMap): FieldsMap => { if (formOptions.stripEmptyFields) { - return Object.entries(fields).reduce( - (acc, [key, field]) => { - if (typeof field.value !== 'string' || field.value.trim() !== '') { - acc[key] = field; - } - return acc; - }, - {} as FieldsMap - ); + return Object.entries(fields).reduce((acc, [key, field]) => { + if (typeof field.value !== 'string' || field.value.trim() !== '') { + acc[key] = field; + } + return acc; + }, {} as FieldsMap); } return fields; }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts index 66c3e8d983f983..62867a0c07a6b0 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -50,10 +50,7 @@ export const mapFormFields = ( formFields: Record, fn: (field: FieldHook) => any ) => - Object.entries(formFields).reduce( - (acc, [key, field]) => { - acc[key] = fn(field); - return acc; - }, - {} as Record - ); + Object.entries(formFields).reduce((acc, [key, field]) => { + acc[key] = fn(field); + return acc; + }, {} as Record); diff --git a/src/plugins/expressions/public/interpreter_provider.ts b/src/plugins/expressions/public/interpreter_provider.ts index cb84370ad69c52..15d6b1c025f549 100644 --- a/src/plugins/expressions/public/interpreter_provider.ts +++ b/src/plugins/expressions/public/interpreter_provider.ts @@ -163,9 +163,9 @@ export function interpreterProvider(config: InterpreterConfig): ExpressionInterp // Check for missing required arguments each(argDefs, argDef => { - const { aliases, default: argDefault, name: argName, required } = argDef as (ArgumentType< + const { aliases, default: argDefault, name: argName, required } = argDef as ArgumentType< any - > & { name: string }); + > & { name: string }; if ( typeof argDefault === 'undefined' && required && diff --git a/src/plugins/kibana_react/public/context/context.tsx b/src/plugins/kibana_react/public/context/context.tsx index cbae5c4638ca2d..cbf2ad07b463e7 100644 --- a/src/plugins/kibana_react/public/context/context.tsx +++ b/src/plugins/kibana_react/public/context/context.tsx @@ -32,12 +32,11 @@ const defaultContextValue = { export const context = createContext>(defaultContextValue); -export const useKibana = (): KibanaReactContextValue< - KibanaServices & Extra -> => - useContext((context as unknown) as React.Context< - KibanaReactContextValue - >); +export const useKibana = (): KibanaReactContextValue => + useContext( + (context as unknown) as React.Context> + ); export const withKibana = }>( type: React.ComponentType diff --git a/src/plugins/kibana_react/public/index.ts b/src/plugins/kibana_react/public/index.ts index cf025ec2e88d46..2d82f646c827b9 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -24,3 +24,4 @@ export * from './overlays'; export * from './ui_settings'; export * from './field_icon'; export * from './table_list_view'; +export { toMountPoint } from './util'; diff --git a/src/plugins/kibana_react/public/notifications/create_notifications.test.tsx b/src/plugins/kibana_react/public/notifications/create_notifications.test.tsx index 35c503d590b2cd..4f64a2b95f512f 100644 --- a/src/plugins/kibana_react/public/notifications/create_notifications.test.tsx +++ b/src/plugins/kibana_react/public/notifications/create_notifications.test.tsx @@ -52,9 +52,20 @@ test('can display string element as title', () => { wrapper.toasts.show({ title: 'foo' }); expect(notifications.toasts.add).toHaveBeenCalledTimes(1); - expect(notifications.toasts.add.mock.calls[0][0]).toMatchObject({ - title: 'foo', - }); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "color": undefined, + "iconType": undefined, + "onClose": undefined, + "text": MountPoint { + "reactNode": , + }, + "title": MountPoint { + "reactNode": "foo", + }, + "toastLifeTimeMs": undefined, + } + `); }); test('can display React element as title', () => { @@ -67,10 +78,12 @@ test('can display React element as title', () => { expect(notifications.toasts.add).toHaveBeenCalledTimes(1); expect((notifications.toasts.add.mock.calls[0][0] as any).title).toMatchInlineSnapshot(` -

- bar -
- `); + MountPoint { + "reactNode":
+ bar +
, + } + `); }); test('can display React element as toast body', () => { @@ -81,12 +94,14 @@ test('can display React element as toast body', () => { expect(notifications.toasts.add).toHaveBeenCalledTimes(1); expect((notifications.toasts.add.mock.calls[0][0] as any).text).toMatchInlineSnapshot(` - -
- baz -
-
- `); + MountPoint { + "reactNode": +
+ baz +
+
, + } + `); }); test('can set toast properties', () => { @@ -102,17 +117,21 @@ test('can set toast properties', () => { }); expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot(` - Object { - "color": "danger", - "iconType": "foo", - "onClose": undefined, - "text": - 1 - , - "title": "2", - "toastLifeTimeMs": 3, - } - `); + Object { + "color": "danger", + "iconType": "foo", + "onClose": undefined, + "text": MountPoint { + "reactNode": + 1 + , + }, + "title": MountPoint { + "reactNode": "2", + }, + "toastLifeTimeMs": 3, + } + `); }); test('can display success, warning and danger toasts', () => { @@ -124,21 +143,48 @@ test('can display success, warning and danger toasts', () => { wrapper.toasts.danger({ title: '3' }); expect(notifications.toasts.add).toHaveBeenCalledTimes(3); - expect(notifications.toasts.add.mock.calls[0][0]).toMatchObject({ - title: '1', - color: 'success', - iconType: 'check', - }); - expect(notifications.toasts.add.mock.calls[1][0]).toMatchObject({ - title: '2', - color: 'warning', - iconType: 'help', - }); - expect(notifications.toasts.add.mock.calls[2][0]).toMatchObject({ - title: '3', - color: 'danger', - iconType: 'alert', - }); + expect(notifications.toasts.add.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "color": "success", + "iconType": "check", + "onClose": undefined, + "text": MountPoint { + "reactNode": , + }, + "title": MountPoint { + "reactNode": "1", + }, + "toastLifeTimeMs": undefined, + } + `); + expect(notifications.toasts.add.mock.calls[1][0]).toMatchInlineSnapshot(` + Object { + "color": "warning", + "iconType": "help", + "onClose": undefined, + "text": MountPoint { + "reactNode": , + }, + "title": MountPoint { + "reactNode": "2", + }, + "toastLifeTimeMs": undefined, + } + `); + expect(notifications.toasts.add.mock.calls[2][0]).toMatchInlineSnapshot(` + Object { + "color": "danger", + "iconType": "alert", + "onClose": undefined, + "text": MountPoint { + "reactNode": , + }, + "title": MountPoint { + "reactNode": "3", + }, + "toastLifeTimeMs": undefined, + } + `); }); test('if body is not set, renders it empty', () => { @@ -147,7 +193,9 @@ test('if body is not set, renders it empty', () => { wrapper.toasts.success({ title: '1' }); - expect((notifications.toasts.add.mock.calls[0][0] as any).text).toMatchInlineSnapshot( - `` - ); + expect((notifications.toasts.add.mock.calls[0][0] as any).text).toMatchInlineSnapshot(` + MountPoint { + "reactNode": , + } + `); }); diff --git a/src/plugins/kibana_react/public/notifications/create_notifications.tsx b/src/plugins/kibana_react/public/notifications/create_notifications.tsx index 28c1d5391d1601..774f74863ee6f0 100644 --- a/src/plugins/kibana_react/public/notifications/create_notifications.tsx +++ b/src/plugins/kibana_react/public/notifications/create_notifications.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { KibanaServices } from '../context/types'; import { KibanaReactNotifications } from './types'; +import { toMountPoint } from '../util'; export const createNotifications = (services: KibanaServices): KibanaReactNotifications => { const show: KibanaReactNotifications['toasts']['show'] = ({ @@ -34,8 +35,8 @@ export const createNotifications = (services: KibanaServices): KibanaReactNotifi throw new TypeError('Could not show notification as notifications service is not available.'); } services.notifications!.toasts.add({ - title, - text: <>{body || null}, + title: toMountPoint(title), + text: toMountPoint(<>{body || null}), color, iconType, toastLifeTimeMs, diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx index e1e7f1c536342d..bab710cdca5958 100644 --- a/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.tsx @@ -32,6 +32,7 @@ import { EuiOverlayMask, EuiSpacer, EuiSwitch, + EuiSwitchEvent, EuiTextArea, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -227,7 +228,7 @@ export class SavedObjectSaveModal extends React.Component { }); }; - private onCopyOnSaveChange = (event: React.ChangeEvent) => { + private onCopyOnSaveChange = (event: EuiSwitchEvent) => { this.setState({ copyOnSave: event.target.checked, }); diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 7d95c00e764190..dde8efa7e11066 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -38,6 +38,7 @@ import { EuiCallOut, } from '@elastic/eui'; import { ToastsStart, UiSettingsClientContract } from 'kibana/public'; +import { toMountPoint } from '../util'; export const EMPTY_FILTER = ''; @@ -166,7 +167,7 @@ class TableListView extends React.Component itemsById[id])); } catch (error) { this.props.toastNotifications.addDanger({ - title: ( + title: toMountPoint( { + const mount = (element: HTMLElement) => { + ReactDOM.render({node}, element); + return () => ReactDOM.unmountComponentAtNode(element); + }; + // only used for tests and snapshots serialization + if (process.env.NODE_ENV !== 'production') { + mount.__reactMount__ = node; + } + return mount; +}; diff --git a/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts b/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts new file mode 100644 index 00000000000000..45ad4cb407175f --- /dev/null +++ b/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function test(value: any) { + return value && value.__reactMount__; +} + +export function print(value: any, serialize: any) { + // there is no proper way to correctly indent multiline values + // so the trick here is to use the Object representation and rewriting the root object name + return serialize({ + reactNode: value.__reactMount__, + }).replace('Object', 'MountPoint'); +} diff --git a/src/plugins/newsfeed/constants.ts b/src/plugins/newsfeed/constants.ts new file mode 100644 index 00000000000000..ddcbbb6cb1dbe6 --- /dev/null +++ b/src/plugins/newsfeed/constants.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const NEWSFEED_FALLBACK_LANGUAGE = 'en'; +export const NEWSFEED_LAST_FETCH_STORAGE_KEY = 'newsfeed.lastfetchtime'; +export const NEWSFEED_HASH_SET_STORAGE_KEY = 'newsfeed.hashes'; diff --git a/src/plugins/newsfeed/kibana.json b/src/plugins/newsfeed/kibana.json new file mode 100644 index 00000000000000..9d49b42424a06c --- /dev/null +++ b/src/plugins/newsfeed/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "newsfeed", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/newsfeed/public/components/__snapshots__/empty_news.test.tsx.snap b/src/plugins/newsfeed/public/components/__snapshots__/empty_news.test.tsx.snap new file mode 100644 index 00000000000000..8764b7664d4496 --- /dev/null +++ b/src/plugins/newsfeed/public/components/__snapshots__/empty_news.test.tsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`empty_news rendering renders the default Empty News 1`] = ` + + +

+ } + data-test-subj="emptyNewsfeed" + iconType="documents" + title={ +

+ +

+ } + titleSize="s" +/> +`; diff --git a/src/plugins/newsfeed/public/components/__snapshots__/loading_news.test.tsx.snap b/src/plugins/newsfeed/public/components/__snapshots__/loading_news.test.tsx.snap new file mode 100644 index 00000000000000..2e88b0053535e6 --- /dev/null +++ b/src/plugins/newsfeed/public/components/__snapshots__/loading_news.test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`news_loading rendering renders the default News Loading 1`] = ` + + +

+ } + title={ + + } +/> +`; diff --git a/src/plugins/newsfeed/public/components/empty_news.test.tsx b/src/plugins/newsfeed/public/components/empty_news.test.tsx new file mode 100644 index 00000000000000..33702df00a583a --- /dev/null +++ b/src/plugins/newsfeed/public/components/empty_news.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import { NewsEmptyPrompt } from './empty_news'; + +describe('empty_news', () => { + describe('rendering', () => { + it('renders the default Empty News', () => { + const wrapper = shallow(); + expect(toJson(wrapper)).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/newsfeed/public/components/empty_news.tsx b/src/plugins/newsfeed/public/components/empty_news.tsx new file mode 100644 index 00000000000000..cec18e0bdec433 --- /dev/null +++ b/src/plugins/newsfeed/public/components/empty_news.tsx @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *    http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied.  See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +export const NewsEmptyPrompt = () => { + return ( + + + + } + body={ +

+ +

+ } + /> + ); +}; diff --git a/src/plugins/newsfeed/public/components/flyout_list.tsx b/src/plugins/newsfeed/public/components/flyout_list.tsx new file mode 100644 index 00000000000000..8a99abe18a75f3 --- /dev/null +++ b/src/plugins/newsfeed/public/components/flyout_list.tsx @@ -0,0 +1,110 @@ +/* + * 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, { useCallback, useContext } from 'react'; +import { + EuiIcon, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiLink, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiText, + EuiBadge, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiHeaderAlert } from '../../../../legacy/core_plugins/newsfeed/public/np_ready/components/header_alert/header_alert'; +import { NewsfeedContext } from './newsfeed_header_nav_button'; +import { NewsfeedItem } from '../../types'; +import { NewsEmptyPrompt } from './empty_news'; +import { NewsLoadingPrompt } from './loading_news'; + +export const NewsfeedFlyout = () => { + const { newsFetchResult, setFlyoutVisible } = useContext(NewsfeedContext); + const closeFlyout = useCallback(() => setFlyoutVisible(false), [setFlyoutVisible]); + + return ( + + + +

+ +

+
+
+ + {!newsFetchResult ? ( + + ) : newsFetchResult.feedItems.length > 0 ? ( + newsFetchResult.feedItems.map((item: NewsfeedItem) => { + return ( + + {item.linkText} + + + } + date={item.publishOn.format('DD MMMM YYYY')} + badge={{item.badge}} + /> + ); + }) + ) : ( + + )} + + + + + + + + + + {newsFetchResult ? ( + +

+ +

+
+ ) : null} +
+
+
+
+ ); +}; diff --git a/src/plugins/newsfeed/public/components/loading_news.test.tsx b/src/plugins/newsfeed/public/components/loading_news.test.tsx new file mode 100644 index 00000000000000..ca449b8ee879e2 --- /dev/null +++ b/src/plugins/newsfeed/public/components/loading_news.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import { NewsLoadingPrompt } from './loading_news'; + +describe('news_loading', () => { + describe('rendering', () => { + it('renders the default News Loading', () => { + const wrapper = shallow(); + expect(toJson(wrapper)).toMatchSnapshot(); + }); + }); +}); diff --git a/src/plugins/newsfeed/public/components/loading_news.tsx b/src/plugins/newsfeed/public/components/loading_news.tsx new file mode 100644 index 00000000000000..fcbc7970377d4e --- /dev/null +++ b/src/plugins/newsfeed/public/components/loading_news.tsx @@ -0,0 +1,39 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiLoadingKibana } from '@elastic/eui'; + +export const NewsLoadingPrompt = () => { + return ( + } + body={ +

+ +

+ } + /> + ); +}; diff --git a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx new file mode 100644 index 00000000000000..da042f0fce7b6e --- /dev/null +++ b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx @@ -0,0 +1,82 @@ +/* + * 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, { useState, Fragment, useEffect } from 'react'; +import * as Rx from 'rxjs'; +import { EuiHeaderSectionItemButton, EuiIcon, EuiNotificationBadge } from '@elastic/eui'; +import { NewsfeedFlyout } from './flyout_list'; +import { FetchResult } from '../../types'; + +export interface INewsfeedContext { + setFlyoutVisible: React.Dispatch>; + newsFetchResult: FetchResult | void | null; +} +export const NewsfeedContext = React.createContext({} as INewsfeedContext); + +export type NewsfeedApiFetchResult = Rx.Observable; + +export interface Props { + apiFetchResult: NewsfeedApiFetchResult; +} + +export const NewsfeedNavButton = ({ apiFetchResult }: Props) => { + const [showBadge, setShowBadge] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + const [newsFetchResult, setNewsFetchResult] = useState(null); + + useEffect(() => { + function handleStatusChange(fetchResult: FetchResult | void | null) { + if (fetchResult) { + setShowBadge(fetchResult.hasNew); + } + setNewsFetchResult(fetchResult); + } + + const subscription = apiFetchResult.subscribe(res => handleStatusChange(res)); + return () => subscription.unsubscribe(); + }, [apiFetchResult]); + + function showFlyout() { + setShowBadge(false); + setFlyoutVisible(!flyoutVisible); + } + + return ( + + + + + {showBadge ? ( + + ▪ + + ) : null} + + {flyoutVisible ? : null} + + + ); +}; diff --git a/src/plugins/newsfeed/public/index.ts b/src/plugins/newsfeed/public/index.ts new file mode 100644 index 00000000000000..1217de60d9638c --- /dev/null +++ b/src/plugins/newsfeed/public/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'src/core/public'; +import { NewsfeedPublicPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new NewsfeedPublicPlugin(initializerContext); +} diff --git a/src/plugins/newsfeed/public/lib/api.test.ts b/src/plugins/newsfeed/public/lib/api.test.ts new file mode 100644 index 00000000000000..4383b9e0f7dabe --- /dev/null +++ b/src/plugins/newsfeed/public/lib/api.test.ts @@ -0,0 +1,698 @@ +/* + * 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 { take, tap, toArray } from 'rxjs/operators'; +import { interval, race } from 'rxjs'; +import sinon, { stub } from 'sinon'; +import moment from 'moment'; +import { HttpServiceBase } from 'src/core/public'; +import { NEWSFEED_HASH_SET_STORAGE_KEY, NEWSFEED_LAST_FETCH_STORAGE_KEY } from '../../constants'; +import { ApiItem, NewsfeedItem, NewsfeedPluginInjectedConfig } from '../../types'; +import { NewsfeedApiDriver, getApi } from './api'; + +const localStorageGet = sinon.stub(); +const sessionStoragetGet = sinon.stub(); + +Object.defineProperty(window, 'localStorage', { + value: { + getItem: localStorageGet, + setItem: stub(), + }, + writable: true, +}); +Object.defineProperty(window, 'sessionStorage', { + value: { + getItem: sessionStoragetGet, + setItem: stub(), + }, + writable: true, +}); + +describe('NewsfeedApiDriver', () => { + const kibanaVersion = 'test_version'; + const userLanguage = 'en'; + const fetchInterval = 2000; + const getDriver = () => new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval); + + afterEach(() => { + sinon.reset(); + }); + + describe('shouldFetch', () => { + it('defaults to true', () => { + const driver = getDriver(); + expect(driver.shouldFetch()).toBe(true); + }); + + it('returns true if last fetch time precedes page load time', () => { + sessionStoragetGet.throws('Wrong key passed!'); + sessionStoragetGet.withArgs(NEWSFEED_LAST_FETCH_STORAGE_KEY).returns(322642800000); // 1980-03-23 + const driver = getDriver(); + expect(driver.shouldFetch()).toBe(true); + }); + + it('returns false if last fetch time is recent enough', () => { + sessionStoragetGet.throws('Wrong key passed!'); + sessionStoragetGet.withArgs(NEWSFEED_LAST_FETCH_STORAGE_KEY).returns(3005017200000); // 2065-03-23 + const driver = getDriver(); + expect(driver.shouldFetch()).toBe(false); + }); + }); + + describe('updateHashes', () => { + it('returns previous and current storage', () => { + const driver = getDriver(); + const items: NewsfeedItem[] = [ + { + title: 'Good news, everyone!', + description: 'good item description', + linkText: 'click here', + linkUrl: 'about:blank', + badge: 'test', + publishOn: moment(1572489035150), + expireOn: moment(1572489047858), + hash: 'hash1oneoneoneone', + }, + ]; + expect(driver.updateHashes(items)).toMatchInlineSnapshot(` + Object { + "current": Array [ + "hash1oneoneoneone", + ], + "previous": Array [], + } + `); + }); + + it('concatenates the previous hashes with the current', () => { + localStorageGet.throws('Wrong key passed!'); + localStorageGet.withArgs(NEWSFEED_HASH_SET_STORAGE_KEY).returns('happyness'); + const driver = getDriver(); + const items: NewsfeedItem[] = [ + { + title: 'Better news, everyone!', + description: 'better item description', + linkText: 'click there', + linkUrl: 'about:blank', + badge: 'concatentated', + publishOn: moment(1572489035150), + expireOn: moment(1572489047858), + hash: 'three33hash', + }, + ]; + expect(driver.updateHashes(items)).toMatchInlineSnapshot(` + Object { + "current": Array [ + "happyness", + "three33hash", + ], + "previous": Array [ + "happyness", + ], + } + `); + }); + }); + + it('Validates items for required fields', () => { + const driver = getDriver(); + expect(driver.validateItem({})).toBe(false); + expect( + driver.validateItem({ + title: 'Gadzooks!', + description: 'gadzooks item description', + linkText: 'click here', + linkUrl: 'about:blank', + badge: 'test', + publishOn: moment(1572489035150), + expireOn: moment(1572489047858), + hash: 'hash2twotwotwotwotwo', + }) + ).toBe(true); + expect( + driver.validateItem({ + title: 'Gadzooks!', + description: 'gadzooks item description', + linkText: 'click here', + linkUrl: 'about:blank', + publishOn: moment(1572489035150), + hash: 'hash2twotwotwotwotwo', + }) + ).toBe(true); + expect( + driver.validateItem({ + title: 'Gadzooks!', + description: 'gadzooks item description', + linkText: 'click here', + linkUrl: 'about:blank', + publishOn: moment(1572489035150), + // hash: 'hash2twotwotwotwotwo', // should fail because this is missing + }) + ).toBe(false); + }); + + describe('modelItems', () => { + it('Models empty set with defaults', () => { + const driver = getDriver(); + const apiItems: ApiItem[] = []; + expect(driver.modelItems(apiItems)).toMatchInlineSnapshot(` + Object { + "error": null, + "feedItems": Array [], + "hasNew": false, + "kibanaVersion": "test_version", + } + `); + }); + + it('Selects default language', () => { + const driver = getDriver(); + const apiItems: ApiItem[] = [ + { + title: { + en: 'speaking English', + es: 'habla Espanol', + }, + description: { + en: 'language test', + es: 'idiomas', + }, + languages: ['en', 'es'], + link_text: { + en: 'click here', + es: 'aqui', + }, + link_url: { + en: 'xyzxyzxyz', + es: 'abcabc', + }, + badge: { + en: 'firefighter', + es: 'bombero', + }, + publish_on: new Date('2014-10-31T04:23:47Z'), + expire_on: new Date('2049-10-31T04:23:47Z'), + hash: 'abcabc1231123123hash', + }, + ]; + expect(driver.modelItems(apiItems)).toMatchObject({ + error: null, + feedItems: [ + { + badge: 'firefighter', + description: 'language test', + hash: 'abcabc1231', + linkText: 'click here', + linkUrl: 'xyzxyzxyz', + title: 'speaking English', + }, + ], + hasNew: true, + kibanaVersion: 'test_version', + }); + }); + + it("Falls back to English when user language isn't present", () => { + // Set Language to French + const driver = new NewsfeedApiDriver(kibanaVersion, 'fr', fetchInterval); + const apiItems: ApiItem[] = [ + { + title: { + en: 'speaking English', + fr: 'Le Title', + }, + description: { + en: 'not French', + fr: 'Le Description', + }, + languages: ['en', 'fr'], + link_text: { + en: 'click here', + fr: 'Le Link Text', + }, + link_url: { + en: 'xyzxyzxyz', + fr: 'le_url', + }, + badge: { + en: 'firefighter', + fr: 'le_badge', + }, + publish_on: new Date('2014-10-31T04:23:47Z'), + expire_on: new Date('2049-10-31T04:23:47Z'), + hash: 'frfrfrfr1231123123hash', + }, // fallback: no + { + title: { + en: 'speaking English', + es: 'habla Espanol', + }, + description: { + en: 'not French', + es: 'no Espanol', + }, + languages: ['en', 'es'], + link_text: { + en: 'click here', + es: 'aqui', + }, + link_url: { + en: 'xyzxyzxyz', + es: 'abcabc', + }, + badge: { + en: 'firefighter', + es: 'bombero', + }, + publish_on: new Date('2014-10-31T04:23:47Z'), + expire_on: new Date('2049-10-31T04:23:47Z'), + hash: 'enenenen1231123123hash', + }, // fallback: yes + ]; + expect(driver.modelItems(apiItems)).toMatchObject({ + error: null, + feedItems: [ + { + badge: 'le_badge', + description: 'Le Description', + hash: 'frfrfrfr12', + linkText: 'Le Link Text', + linkUrl: 'le_url', + title: 'Le Title', + }, + { + badge: 'firefighter', + description: 'not French', + hash: 'enenenen12', + linkText: 'click here', + linkUrl: 'xyzxyzxyz', + title: 'speaking English', + }, + ], + hasNew: true, + kibanaVersion: 'test_version', + }); + }); + + it('Models multiple items into an API FetchResult', () => { + const driver = getDriver(); + const apiItems: ApiItem[] = [ + { + title: { + en: 'guess what', + }, + description: { + en: 'this tests the modelItems function', + }, + link_text: { + en: 'click here', + }, + link_url: { + en: 'about:blank', + }, + publish_on: new Date('2014-10-31T04:23:47Z'), + expire_on: new Date('2049-10-31T04:23:47Z'), + hash: 'abcabc1231123123hash', + }, + { + title: { + en: 'guess when', + }, + description: { + en: 'this also tests the modelItems function', + }, + link_text: { + en: 'click here', + }, + link_url: { + en: 'about:blank', + }, + badge: { + en: 'hero', + }, + publish_on: new Date('2014-10-31T04:23:47Z'), + expire_on: new Date('2049-10-31T04:23:47Z'), + hash: 'defdefdef456456456', + }, + ]; + expect(driver.modelItems(apiItems)).toMatchObject({ + error: null, + feedItems: [ + { + badge: null, + description: 'this tests the modelItems function', + hash: 'abcabc1231', + linkText: 'click here', + linkUrl: 'about:blank', + title: 'guess what', + }, + { + badge: 'hero', + description: 'this also tests the modelItems function', + hash: 'defdefdef4', + linkText: 'click here', + linkUrl: 'about:blank', + title: 'guess when', + }, + ], + hasNew: true, + kibanaVersion: 'test_version', + }); + }); + + it('Filters expired', () => { + const driver = getDriver(); + const apiItems: ApiItem[] = [ + { + title: { + en: 'guess what', + }, + description: { + en: 'this tests the modelItems function', + }, + link_text: { + en: 'click here', + }, + link_url: { + en: 'about:blank', + }, + publish_on: new Date('2013-10-31T04:23:47Z'), + expire_on: new Date('2014-10-31T04:23:47Z'), // too old + hash: 'abcabc1231123123hash', + }, + ]; + expect(driver.modelItems(apiItems)).toMatchInlineSnapshot(` + Object { + "error": null, + "feedItems": Array [], + "hasNew": false, + "kibanaVersion": "test_version", + } + `); + }); + + it('Filters pre-published', () => { + const driver = getDriver(); + const apiItems: ApiItem[] = [ + { + title: { + en: 'guess what', + }, + description: { + en: 'this tests the modelItems function', + }, + link_text: { + en: 'click here', + }, + link_url: { + en: 'about:blank', + }, + publish_on: new Date('2055-10-31T04:23:47Z'), // too new + expire_on: new Date('2056-10-31T04:23:47Z'), + hash: 'abcabc1231123123hash', + }, + ]; + expect(driver.modelItems(apiItems)).toMatchInlineSnapshot(` + Object { + "error": null, + "feedItems": Array [], + "hasNew": false, + "kibanaVersion": "test_version", + } + `); + }); + }); +}); + +describe('getApi', () => { + const mockHttpGet = jest.fn(); + let httpMock = ({ + fetch: mockHttpGet, + } as unknown) as HttpServiceBase; + const getHttpMockWithItems = (mockApiItems: ApiItem[]) => ( + arg1: string, + arg2: { method: string } + ) => { + if ( + arg1 === 'http://fakenews.co/kibana-test/v6.8.2.json' && + arg2.method && + arg2.method === 'GET' + ) { + return Promise.resolve({ items: mockApiItems }); + } + return Promise.reject('wrong args!'); + }; + let configMock: NewsfeedPluginInjectedConfig; + + afterEach(() => { + jest.resetAllMocks(); + }); + + beforeEach(() => { + configMock = { + newsfeed: { + service: { + urlRoot: 'http://fakenews.co', + pathTemplate: '/kibana-test/v{VERSION}.json', + }, + defaultLanguage: 'en', + mainInterval: 86400000, + fetchInterval: 86400000, + }, + }; + httpMock = ({ + fetch: mockHttpGet, + } as unknown) as HttpServiceBase; + }); + + it('creates a result', done => { + mockHttpGet.mockImplementationOnce(() => Promise.resolve({ items: [] })); + getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + expect(result).toMatchInlineSnapshot(` + Object { + "error": null, + "feedItems": Array [], + "hasNew": false, + "kibanaVersion": "6.8.2", + } + `); + done(); + }); + }); + + it('hasNew is true when the service returns hashes not in the cache', done => { + const mockApiItems: ApiItem[] = [ + { + title: { + en: 'speaking English', + es: 'habla Espanol', + }, + description: { + en: 'language test', + es: 'idiomas', + }, + languages: ['en', 'es'], + link_text: { + en: 'click here', + es: 'aqui', + }, + link_url: { + en: 'xyzxyzxyz', + es: 'abcabc', + }, + badge: { + en: 'firefighter', + es: 'bombero', + }, + publish_on: new Date('2014-10-31T04:23:47Z'), + expire_on: new Date('2049-10-31T04:23:47Z'), + hash: 'abcabc1231123123hash', + }, + ]; + + mockHttpGet.mockImplementationOnce(getHttpMockWithItems(mockApiItems)); + + getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + expect(result).toMatchInlineSnapshot(` + Object { + "error": null, + "feedItems": Array [ + Object { + "badge": "firefighter", + "description": "language test", + "expireOn": "2049-10-31T04:23:47.000Z", + "hash": "abcabc1231", + "linkText": "click here", + "linkUrl": "xyzxyzxyz", + "publishOn": "2014-10-31T04:23:47.000Z", + "title": "speaking English", + }, + ], + "hasNew": true, + "kibanaVersion": "6.8.2", + } + `); + done(); + }); + }); + + it('hasNew is false when service returns hashes that are all stored', done => { + localStorageGet.throws('Wrong key passed!'); + localStorageGet.withArgs(NEWSFEED_HASH_SET_STORAGE_KEY).returns('happyness'); + const mockApiItems: ApiItem[] = [ + { + title: { en: 'hasNew test' }, + description: { en: 'test' }, + link_text: { en: 'click here' }, + link_url: { en: 'xyzxyzxyz' }, + badge: { en: 'firefighter' }, + publish_on: new Date('2014-10-31T04:23:47Z'), + expire_on: new Date('2049-10-31T04:23:47Z'), + hash: 'happyness', + }, + ]; + mockHttpGet.mockImplementationOnce(getHttpMockWithItems(mockApiItems)); + getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + expect(result).toMatchInlineSnapshot(` + Object { + "error": null, + "feedItems": Array [ + Object { + "badge": "firefighter", + "description": "test", + "expireOn": "2049-10-31T04:23:47.000Z", + "hash": "happyness", + "linkText": "click here", + "linkUrl": "xyzxyzxyz", + "publishOn": "2014-10-31T04:23:47.000Z", + "title": "hasNew test", + }, + ], + "hasNew": false, + "kibanaVersion": "6.8.2", + } + `); + done(); + }); + }); + + it('forwards an error', done => { + mockHttpGet.mockImplementationOnce((arg1, arg2) => Promise.reject('sorry, try again later!')); + + getApi(httpMock, configMock.newsfeed, '6.8.2').subscribe(result => { + expect(result).toMatchInlineSnapshot(` + Object { + "error": "sorry, try again later!", + "feedItems": Array [], + "hasNew": false, + "kibanaVersion": "6.8.2", + } + `); + done(); + }); + }); + + describe('Retry fetching', () => { + const successItems: ApiItem[] = [ + { + title: { en: 'hasNew test' }, + description: { en: 'test' }, + link_text: { en: 'click here' }, + link_url: { en: 'xyzxyzxyz' }, + badge: { en: 'firefighter' }, + publish_on: new Date('2014-10-31T04:23:47Z'), + expire_on: new Date('2049-10-31T04:23:47Z'), + hash: 'happyness', + }, + ]; + + it("retries until fetch doesn't error", done => { + configMock.newsfeed.mainInterval = 10; // fast retry for testing + mockHttpGet + .mockImplementationOnce(() => Promise.reject('Sorry, try again later!')) + .mockImplementationOnce(() => Promise.reject('Sorry, internal server error!')) + .mockImplementationOnce(() => Promise.reject("Sorry, it's too cold to go outside!")) + .mockImplementationOnce(getHttpMockWithItems(successItems)); + + getApi(httpMock, configMock.newsfeed, '6.8.2') + .pipe(take(4), toArray()) + .subscribe(result => { + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "error": "Sorry, try again later!", + "feedItems": Array [], + "hasNew": false, + "kibanaVersion": "6.8.2", + }, + Object { + "error": "Sorry, internal server error!", + "feedItems": Array [], + "hasNew": false, + "kibanaVersion": "6.8.2", + }, + Object { + "error": "Sorry, it's too cold to go outside!", + "feedItems": Array [], + "hasNew": false, + "kibanaVersion": "6.8.2", + }, + Object { + "error": null, + "feedItems": Array [ + Object { + "badge": "firefighter", + "description": "test", + "expireOn": "2049-10-31T04:23:47.000Z", + "hash": "happyness", + "linkText": "click here", + "linkUrl": "xyzxyzxyz", + "publishOn": "2014-10-31T04:23:47.000Z", + "title": "hasNew test", + }, + ], + "hasNew": false, + "kibanaVersion": "6.8.2", + }, + ] + `); + done(); + }); + }); + + it("doesn't retry if fetch succeeds", done => { + configMock.newsfeed.mainInterval = 10; // fast retry for testing + mockHttpGet.mockImplementation(getHttpMockWithItems(successItems)); + + const timeout$ = interval(1000); // lets us capture some results after a short time + let timesFetched = 0; + + const get$ = getApi(httpMock, configMock.newsfeed, '6.8.2').pipe( + tap(() => { + timesFetched++; + }) + ); + + race(get$, timeout$).subscribe(() => { + expect(timesFetched).toBe(1); // first fetch was successful, so there was no retry + done(); + }); + }); + }); +}); diff --git a/src/plugins/newsfeed/public/lib/api.ts b/src/plugins/newsfeed/public/lib/api.ts new file mode 100644 index 00000000000000..6920dd9b2bccc2 --- /dev/null +++ b/src/plugins/newsfeed/public/lib/api.ts @@ -0,0 +1,194 @@ +/* + * 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 * as Rx from 'rxjs'; +import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { catchError, filter, mergeMap, tap } from 'rxjs/operators'; +import { HttpServiceBase } from 'src/core/public'; +import { + NEWSFEED_FALLBACK_LANGUAGE, + NEWSFEED_LAST_FETCH_STORAGE_KEY, + NEWSFEED_HASH_SET_STORAGE_KEY, +} from '../../constants'; +import { NewsfeedPluginInjectedConfig, ApiItem, NewsfeedItem, FetchResult } from '../../types'; + +type ApiConfig = NewsfeedPluginInjectedConfig['newsfeed']['service']; + +export class NewsfeedApiDriver { + private readonly loadedTime = moment().utc(); // the date is compared to time in UTC format coming from the service + + constructor( + private readonly kibanaVersion: string, + private readonly userLanguage: string, + private readonly fetchInterval: number + ) {} + + shouldFetch(): boolean { + const lastFetchUtc: string | null = sessionStorage.getItem(NEWSFEED_LAST_FETCH_STORAGE_KEY); + if (lastFetchUtc == null) { + return true; + } + const last = moment(lastFetchUtc, 'x'); // parse as unix ms timestamp (already is UTC) + + // does the last fetch time precede the time that the page was loaded? + if (this.loadedTime.diff(last) > 0) { + return true; + } + + const now = moment.utc(); // always use UTC to compare timestamps that came from the service + const duration = moment.duration(now.diff(last)); + + return duration.asMilliseconds() > this.fetchInterval; + } + + updateLastFetch() { + sessionStorage.setItem(NEWSFEED_LAST_FETCH_STORAGE_KEY, Date.now().toString()); + } + + updateHashes(items: NewsfeedItem[]): { previous: string[]; current: string[] } { + // replace localStorage hashes with new hashes + const stored: string | null = localStorage.getItem(NEWSFEED_HASH_SET_STORAGE_KEY); + let old: string[] = []; + if (stored != null) { + old = stored.split(','); + } + + const newHashes = items.map(i => i.hash); + const updatedHashes = [...new Set(old.concat(newHashes))]; + localStorage.setItem(NEWSFEED_HASH_SET_STORAGE_KEY, updatedHashes.join(',')); + + return { previous: old, current: updatedHashes }; + } + + fetchNewsfeedItems(http: HttpServiceBase, config: ApiConfig): Rx.Observable { + const urlPath = config.pathTemplate.replace('{VERSION}', this.kibanaVersion); + const fullUrl = config.urlRoot + urlPath; + + return Rx.from( + http + .fetch(fullUrl, { + method: 'GET', + }) + .then(({ items }) => this.modelItems(items)) + ); + } + + validateItem(item: Partial) { + const hasMissing = [ + item.title, + item.description, + item.linkText, + item.linkUrl, + item.publishOn, + item.hash, + ].includes(undefined); + + return !hasMissing; + } + + modelItems(items: ApiItem[]): FetchResult { + const feedItems: NewsfeedItem[] = items.reduce((accum: NewsfeedItem[], it: ApiItem) => { + let chosenLanguage = this.userLanguage; + const { + expire_on: expireOnUtc, + publish_on: publishOnUtc, + languages, + title, + description, + link_text: linkText, + link_url: linkUrl, + badge, + hash, + } = it; + + if (moment(expireOnUtc).isBefore(Date.now())) { + return accum; // ignore item if expired + } + + if (moment(publishOnUtc).isAfter(Date.now())) { + return accum; // ignore item if publish date hasn't occurred yet (pre-published) + } + + if (languages && !languages.includes(chosenLanguage)) { + chosenLanguage = NEWSFEED_FALLBACK_LANGUAGE; // don't remove the item: fallback on a language + } + + const tempItem: NewsfeedItem = { + title: title[chosenLanguage], + description: description[chosenLanguage], + linkText: linkText[chosenLanguage], + linkUrl: linkUrl[chosenLanguage], + badge: badge != null ? badge![chosenLanguage] : null, + publishOn: moment(publishOnUtc), + expireOn: moment(expireOnUtc), + hash: hash.slice(0, 10), // optimize for storage and faster parsing + }; + + if (!this.validateItem(tempItem)) { + return accum; // ignore if title, description, etc is missing + } + + return [...accum, tempItem]; + }, []); + + // calculate hasNew + const { previous, current } = this.updateHashes(feedItems); + const hasNew = current.length > previous.length; + + return { + error: null, + kibanaVersion: this.kibanaVersion, + hasNew, + feedItems, + }; + } +} + +/* + * Creates an Observable to newsfeed items, powered by the main interval + * Computes hasNew value from new item hashes saved in localStorage + */ +export function getApi( + http: HttpServiceBase, + config: NewsfeedPluginInjectedConfig['newsfeed'], + kibanaVersion: string +): Rx.Observable { + const userLanguage = i18n.getLocale() || config.defaultLanguage; + const fetchInterval = config.fetchInterval; + const driver = new NewsfeedApiDriver(kibanaVersion, userLanguage, fetchInterval); + + return Rx.timer(0, config.mainInterval).pipe( + filter(() => driver.shouldFetch()), + mergeMap(() => + driver.fetchNewsfeedItems(http, config.service).pipe( + catchError(err => { + window.console.error(err); + return Rx.of({ + error: err, + kibanaVersion, + hasNew: false, + feedItems: [], + }); + }) + ) + ), + tap(() => driver.updateLastFetch()) + ); +} diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx new file mode 100644 index 00000000000000..5ea5e5b324717d --- /dev/null +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as Rx from 'rxjs'; +import { catchError, takeUntil } from 'rxjs/operators'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { NewsfeedPluginInjectedConfig } from '../types'; +import { NewsfeedNavButton, NewsfeedApiFetchResult } from './components/newsfeed_header_nav_button'; +import { getApi } from './lib/api'; + +export type Setup = void; +export type Start = void; + +export class NewsfeedPublicPlugin implements Plugin { + private readonly kibanaVersion: string; + private readonly stop$ = new Rx.ReplaySubject(1); + + constructor(initializerContext: PluginInitializerContext) { + this.kibanaVersion = initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup): Setup {} + + public start(core: CoreStart): Start { + const api$ = this.fetchNewsfeed(core); + core.chrome.navControls.registerRight({ + order: 1000, + mount: target => this.mount(api$, target), + }); + } + + public stop() { + this.stop$.next(); + } + + private fetchNewsfeed(core: CoreStart) { + const { http, injectedMetadata } = core; + const config = injectedMetadata.getInjectedVar( + 'newsfeed' + ) as NewsfeedPluginInjectedConfig['newsfeed']; + + return getApi(http, config, this.kibanaVersion).pipe( + takeUntil(this.stop$), // stop the interval when stop method is called + catchError(() => Rx.of(null)) // do not throw error + ); + } + + private mount(api$: NewsfeedApiFetchResult, targetDomElement: HTMLElement) { + ReactDOM.render( + + + , + targetDomElement + ); + return () => ReactDOM.unmountComponentAtNode(targetDomElement); + } +} diff --git a/src/plugins/newsfeed/types.ts b/src/plugins/newsfeed/types.ts new file mode 100644 index 00000000000000..78485c6ee4f590 --- /dev/null +++ b/src/plugins/newsfeed/types.ts @@ -0,0 +1,63 @@ +/* + * 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 { Moment } from 'moment'; + +export interface NewsfeedPluginInjectedConfig { + newsfeed: { + service: { + urlRoot: string; + pathTemplate: string; + }; + defaultLanguage: string; + mainInterval: number; // how often to check last updated time + fetchInterval: number; // how often to fetch remote service and set last updated + }; +} + +export interface ApiItem { + hash: string; + expire_on: Date; + publish_on: Date; + title: { [lang: string]: string }; + description: { [lang: string]: string }; + link_text: { [lang: string]: string }; + link_url: { [lang: string]: string }; + badge?: { [lang: string]: string } | null; + languages?: string[] | null; + image_url?: null; // not used phase 1 +} + +export interface NewsfeedItem { + title: string; + description: string; + linkText: string; + linkUrl: string; + badge: string | null; + publishOn: Moment; + expireOn: Moment; + hash: string; +} + +export interface FetchResult { + kibanaVersion: string; + hasNew: boolean; + feedItems: NewsfeedItem[]; + error: Error | null; +} diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index 31656df2cb6447..f5a1e63617dfa0 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -41,6 +41,7 @@ export function getFunctionalTestGroupRunConfigs({ kibanaInstallDir } = {}) { 'scripts/functional_tests', '--include-tag', tag, '--config', 'test/functional/config.js', + '--config', 'test/ui_capabilities/newsfeed_err/config.ts', // '--config', 'test/functional/config.firefox.js', '--bail', '--debug', diff --git a/test/api_integration/apis/ui_metric/ui_metric.js b/test/api_integration/apis/ui_metric/ui_metric.js index efa6be47b50c9b..f0c86f2904638c 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.js +++ b/test/api_integration/apis/ui_metric/ui_metric.js @@ -18,48 +18,59 @@ */ import expect from '@kbn/expect'; -import { ReportManager } from '@kbn/analytics'; +import { ReportManager, METRIC_TYPE } from '@kbn/analytics'; export default function ({ getService }) { const supertest = getService('supertest'); const es = getService('es'); - const createMetric = (eventName) => ({ - key: ReportManager.createMetricKey({ appName: 'myApp', type: 'click', eventName }), + const createStatsMetric = (eventName) => ({ + key: ReportManager.createMetricKey({ appName: 'myApp', type: METRIC_TYPE.CLICK, eventName }), eventName, appName: 'myApp', - type: 'click', + type: METRIC_TYPE.CLICK, stats: { sum: 1, avg: 1, min: 1, max: 1 }, }); + const createUserAgentMetric = (appName) => ({ + key: ReportManager.createMetricKey({ appName, type: METRIC_TYPE.USER_AGENT }), + appName, + type: METRIC_TYPE.USER_AGENT, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36', + }); + describe('ui_metric API', () => { - const uiStatsMetric = createMetric('myEvent'); - const report = { - uiStatsMetrics: { - [uiStatsMetric.key]: uiStatsMetric, - } - }; + it('increments the count field in the document defined by the {app}/{action_type} path', async () => { + const uiStatsMetric = createStatsMetric('myEvent'); + const report = { + uiStatsMetrics: { + [uiStatsMetric.key]: uiStatsMetric, + } + }; await supertest .post('/api/telemetry/report') .set('kbn-xsrf', 'kibana') .set('content-type', 'application/json') - .send({ report }) + .send(report) .expect(200); - return es.search({ - index: '.kibana', - q: 'type:user-action', - }).then(response => { - const ids = response.hits.hits.map(({ _id }) => _id); - expect(ids.includes('user-action:myApp:myEvent')); - }); + const response = await es.search({ index: '.kibana', q: 'type:ui-metric' }); + const ids = response.hits.hits.map(({ _id }) => _id); + expect(ids.includes('ui-metric:myApp:myEvent')).to.eql(true); }); it('supports multiple events', async () => { - const uiStatsMetric1 = createMetric('myEvent1'); - const uiStatsMetric2 = createMetric('myEvent2'); + const userAgentMetric = createUserAgentMetric('kibana'); + const uiStatsMetric1 = createStatsMetric('myEvent'); + const hrTime = process.hrtime(); + const nano = hrTime[0] * 1000000000 + hrTime[1]; + const uniqueEventName = `myEvent${nano}`; + const uiStatsMetric2 = createStatsMetric(uniqueEventName); const report = { + userAgent: { + [userAgentMetric.key]: userAgentMetric, + }, uiStatsMetrics: { [uiStatsMetric1.key]: uiStatsMetric1, [uiStatsMetric2.key]: uiStatsMetric2, @@ -69,17 +80,14 @@ export default function ({ getService }) { .post('/api/telemetry/report') .set('kbn-xsrf', 'kibana') .set('content-type', 'application/json') - .send({ report }) + .send(report) .expect(200); - return es.search({ - index: '.kibana', - q: 'type:user-action', - }).then(response => { - const ids = response.hits.hits.map(({ _id }) => _id); - expect(ids.includes('user-action:myApp:myEvent1')); - expect(ids.includes('user-action:myApp:myEvent2')); - }); + const response = await es.search({ index: '.kibana', q: 'type:ui-metric' }); + const ids = response.hits.hits.map(({ _id }) => _id); + expect(ids.includes('ui-metric:myApp:myEvent')).to.eql(true); + expect(ids.includes(`ui-metric:myApp:${uniqueEventName}`)).to.eql(true); + expect(ids.includes(`ui-metric:kibana-user_agent:${userAgentMetric.userAgent}`)).to.eql(true); }); }); } diff --git a/test/common/config.js b/test/common/config.js index 44e4bef99bf620..58161e545bd06a 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -17,6 +17,7 @@ * under the License. */ +import path from 'path'; import { format as formatUrl } from 'url'; import { OPTIMIZE_BUNDLE_DIR, esTestConfig, kbnTestConfig } from '@kbn/test'; import { services } from './services'; @@ -57,9 +58,12 @@ export default function () { `--kibana.disableWelcomeScreen=true`, '--telemetry.banner=false', `--server.maxPayloadBytes=1679958`, + // newsfeed mock service + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'newsfeed')}`, + `--newsfeed.service.urlRoot=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`, + `--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/v{VERSION}.json`, ], }, - services }; } diff --git a/test/common/fixtures/plugins/newsfeed/index.ts b/test/common/fixtures/plugins/newsfeed/index.ts new file mode 100644 index 00000000000000..beee9bb5c6069d --- /dev/null +++ b/test/common/fixtures/plugins/newsfeed/index.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 Hapi from 'hapi'; +import { initPlugin as initNewsfeed } from './newsfeed_simulation'; + +const NAME = 'newsfeed-FTS-external-service-simulators'; + +// eslint-disable-next-line import/no-default-export +export default function(kibana: any) { + return new kibana.Plugin({ + name: NAME, + init: (server: Hapi.Server) => { + initNewsfeed(server, `/api/_${NAME}`); + }, + }); +} diff --git a/test/common/fixtures/plugins/newsfeed/newsfeed_simulation.ts b/test/common/fixtures/plugins/newsfeed/newsfeed_simulation.ts new file mode 100644 index 00000000000000..2a7ea3793324da --- /dev/null +++ b/test/common/fixtures/plugins/newsfeed/newsfeed_simulation.ts @@ -0,0 +1,114 @@ +/* + * 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 Hapi from 'hapi'; + +interface WebhookRequest extends Hapi.Request { + payload: string; +} + +export async function initPlugin(server: Hapi.Server, path: string) { + server.route({ + method: ['GET'], + path: `${path}/kibana/v{version}.json`, + options: { + cors: { + origin: ['*'], + additionalHeaders: [ + 'Sec-Fetch-Mode', + 'Access-Control-Request-Method', + 'Access-Control-Request-Headers', + 'cache-control', + 'x-requested-with', + 'Origin', + 'User-Agent', + 'DNT', + 'content-type', + 'kbn-version', + ], + }, + }, + handler: newsfeedHandler, + }); + + server.route({ + method: ['GET'], + path: `${path}/kibana/crash.json`, + options: { + cors: { + origin: ['*'], + additionalHeaders: [ + 'Sec-Fetch-Mode', + 'Access-Control-Request-Method', + 'Access-Control-Request-Headers', + 'cache-control', + 'x-requested-with', + 'Origin', + 'User-Agent', + 'DNT', + 'content-type', + 'kbn-version', + ], + }, + }, + handler() { + throw new Error('Internal server error'); + }, + }); +} + +function newsfeedHandler(request: WebhookRequest, h: any) { + return htmlResponse(h, 200, JSON.stringify(mockNewsfeed(request.params.version))); +} + +const mockNewsfeed = (version: string) => ({ + items: [ + { + title: { en: `You are functionally testing the newsfeed widget with fixtures!` }, + description: { en: 'See test/common/fixtures/plugins/newsfeed/newsfeed_simulation' }, + link_text: { en: 'Generic feed-viewer could go here' }, + link_url: { en: 'https://feeds.elastic.co' }, + languages: null, + badge: null, + image_url: null, + publish_on: '2019-06-21T00:00:00', + expire_on: '2019-12-31T00:00:00', + hash: '39ca7d409c7eb25f4c69a5a6a11309b2f5ced7ca3f9b3a0109517126e0fd91ca', + }, + { + title: { en: 'Staging too!' }, + description: { en: 'Hello world' }, + link_text: { en: 'Generic feed-viewer could go here' }, + link_url: { en: 'https://feeds-staging.elastic.co' }, + languages: null, + badge: null, + image_url: null, + publish_on: '2019-06-21T00:00:00', + expire_on: '2019-12-31T00:00:00', + hash: 'db445c9443eb50ea2eb15f20edf89cf0f7dac2b058b11cafc2c8c288b6e4ce2a', + }, + ], +}); + +function htmlResponse(h: any, code: number, text: string) { + return h + .response(text) + .type('application/json') + .code(code); +} diff --git a/test/common/fixtures/plugins/newsfeed/package.json b/test/common/fixtures/plugins/newsfeed/package.json new file mode 100644 index 00000000000000..5291b1031b0a94 --- /dev/null +++ b/test/common/fixtures/plugins/newsfeed/package.json @@ -0,0 +1,7 @@ +{ + "name": "newsfeed-fixtures", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/test/functional/apps/discover/_shared_links.js b/test/functional/apps/discover/_shared_links.js index f0d34207a87a2f..0b2b4f14f126dd 100644 --- a/test/functional/apps/discover/_shared_links.js +++ b/test/functional/apps/discover/_shared_links.js @@ -25,11 +25,12 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'share', 'timePicker']); + const browser = getService('browser'); describe('shared links', function describeIndexTests() { let baseUrl; - before(async function () { + async function setup({ storeStateInSessionStorage }) { baseUrl = PageObjects.common.getHostPort(); log.debug('baseUrl = ' + baseUrl); // browsers don't show the ':port' if it's 80 or 443 so we have to @@ -47,9 +48,12 @@ export default function ({ getService, getPageObjects }) { log.debug('load kibana index with default index pattern'); await esArchiver.load('discover'); - await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace({ + 'state:storeInSessionStorage': storeStateInSessionStorage + }); + log.debug('discover'); await PageObjects.common.navigateToApp('discover'); @@ -60,47 +64,103 @@ export default function ({ getService, getPageObjects }) { await PageObjects.common.sleep(1000); await PageObjects.share.clickShareTopNavButton(); - }); - describe('permalink', function () { - it('should allow for copying the snapshot URL', async function () { - const expectedUrl = - baseUrl + - '/app/kibana?_t=1453775307251#' + - '/discover?_g=(refreshInterval:(pause:!t,value:0),time' + - ':(from:\'2015-09-19T06:31:44.000Z\',to:\'2015-09' + - '-23T18:31:44.000Z\'))&_a=(columns:!(_source),index:\'logstash-' + - '*\',interval:auto,query:(language:kuery,query:\'\')' + - ',sort:!(!(\'@timestamp\',desc)))'; - const actualUrl = await PageObjects.share.getSharedUrl(); - // strip the timestamp out of each URL - expect(actualUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')).to.be( - expectedUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP') - ); + return async () => { + await kibanaServer.uiSettings.replace({ + 'state:storeInSessionStorage': undefined + }); + }; + } + + describe('shared links with state in query', async () => { + let teardown; + before(async function () { + teardown = await setup({ storeStateInSessionStorage: false }); + }); + + after(async function () { + await teardown(); }); - it('should allow for copying the snapshot URL as a short URL', async function () { - const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$'); - await PageObjects.share.checkShortenUrl(); - await retry.try(async () => { + describe('permalink', function () { + it('should allow for copying the snapshot URL', async function () { + const expectedUrl = + baseUrl + + '/app/kibana?_t=1453775307251#' + + '/discover?_g=(refreshInterval:(pause:!t,value:0),time' + + ':(from:\'2015-09-19T06:31:44.000Z\',to:\'2015-09' + + '-23T18:31:44.000Z\'))&_a=(columns:!(_source),index:\'logstash-' + + '*\',interval:auto,query:(language:kuery,query:\'\')' + + ',sort:!(!(\'@timestamp\',desc)))'; + const actualUrl = await PageObjects.share.getSharedUrl(); + // strip the timestamp out of each URL + expect(actualUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP')).to.be( + expectedUrl.replace(/_t=\d{13}/, '_t=TIMESTAMP') + ); + }); + + it('should allow for copying the snapshot URL as a short URL', async function () { + const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$'); + await PageObjects.share.checkShortenUrl(); + await retry.try(async () => { + const actualUrl = await PageObjects.share.getSharedUrl(); + expect(actualUrl).to.match(re); + }); + }); + + it('should allow for copying the saved object URL', async function () { + const expectedUrl = + baseUrl + + '/app/kibana#' + + '/discover/ab12e3c0-f231-11e6-9486-733b1ac9221a' + + '?_g=(refreshInterval%3A(pause%3A!t%2Cvalue%3A0)' + + '%2Ctime%3A(from%3A\'2015-09-19T06%3A31%3A44.000Z\'%2C' + + 'to%3A\'2015-09-23T18%3A31%3A44.000Z\'))'; + await PageObjects.discover.loadSavedSearch('A Saved Search'); + await PageObjects.share.clickShareTopNavButton(); + await PageObjects.share.exportAsSavedObject(); const actualUrl = await PageObjects.share.getSharedUrl(); - expect(actualUrl).to.match(re); + expect(actualUrl).to.be(expectedUrl); }); }); + }); + + describe('shared links with state in sessionStorage', async () => { + let teardown; + before(async function () { + teardown = await setup({ storeStateInSessionStorage: true }); + }); - it('should allow for copying the saved object URL', async function () { - const expectedUrl = - baseUrl + - '/app/kibana#' + - '/discover/ab12e3c0-f231-11e6-9486-733b1ac9221a' + - '?_g=(refreshInterval%3A(pause%3A!t%2Cvalue%3A0)' + - '%2Ctime%3A(from%3A\'2015-09-19T06%3A31%3A44.000Z\'%2C' + - 'to%3A\'2015-09-23T18%3A31%3A44.000Z\'))'; - await PageObjects.discover.loadSavedSearch('A Saved Search'); - await PageObjects.share.clickShareTopNavButton(); - await PageObjects.share.exportAsSavedObject(); - const actualUrl = await PageObjects.share.getSharedUrl(); - expect(actualUrl).to.be(expectedUrl); + after(async function () { + await teardown(); + }); + + describe('permalink', function () { + it('should allow for copying the snapshot URL as a short URL and should open it', async function () { + const re = new RegExp(baseUrl + '/goto/[0-9a-f]{32}$'); + await PageObjects.share.checkShortenUrl(); + let actualUrl; + await retry.try(async () => { + actualUrl = await PageObjects.share.getSharedUrl(); + expect(actualUrl).to.match(re); + }); + + const actualTime = await PageObjects.timePicker.getTimeConfig(); + + await browser.clearSessionStorage(); + await browser.get(actualUrl, false); + await retry.waitFor( + 'shortUrl resolves and opens', + async () => { + const resolvedUrl = await browser.getCurrentUrl(); + expect(resolvedUrl).to.match(/discover/); + const resolvedTime = await PageObjects.timePicker.getTimeConfig(); + expect(resolvedTime.start).to.equal(actualTime.start); + expect(resolvedTime.end).to.equal(actualTime.end); + return true; + } + ); + }); }); }); }); diff --git a/test/functional/apps/home/_newsfeed.ts b/test/functional/apps/home/_newsfeed.ts new file mode 100644 index 00000000000000..35d7ac8adefa53 --- /dev/null +++ b/test/functional/apps/home/_newsfeed.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const globalNav = getService('globalNav'); + const PageObjects = getPageObjects(['common', 'newsfeed']); + + describe('Newsfeed', () => { + before(async () => { + await PageObjects.newsfeed.resetPage(); + }); + + it('has red icon which is a sign of not checked news', async () => { + const hasCheckedNews = await PageObjects.newsfeed.getRedButtonSign(); + expect(hasCheckedNews).to.be(true); + }); + + it('clicking on newsfeed icon should open you newsfeed', async () => { + await globalNav.clickNewsfeed(); + const isOpen = await PageObjects.newsfeed.openNewsfeedPanel(); + expect(isOpen).to.be(true); + }); + + it('no red icon, because all news is checked', async () => { + const hasCheckedNews = await PageObjects.newsfeed.getRedButtonSign(); + expect(hasCheckedNews).to.be(false); + }); + + it('shows all news from newsfeed', async () => { + const objects = await PageObjects.newsfeed.getNewsfeedList(); + expect(objects).to.eql([ + '21 June 2019\nYou are functionally testing the newsfeed widget with fixtures!\nSee test/common/fixtures/plugins/newsfeed/newsfeed_simulation\nGeneric feed-viewer could go here', + '21 June 2019\nStaging too!\nHello world\nGeneric feed-viewer could go here', + ]); + }); + + it('clicking on newsfeed icon should close opened newsfeed', async () => { + await globalNav.clickNewsfeed(); + const isOpen = await PageObjects.newsfeed.openNewsfeedPanel(); + expect(isOpen).to.be(false); + }); + }); +} diff --git a/test/functional/apps/home/index.js b/test/functional/apps/home/index.js index 17c93680088cbc..f3f564fbd29193 100644 --- a/test/functional/apps/home/index.js +++ b/test/functional/apps/home/index.js @@ -29,6 +29,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_navigation')); loadTestFile(require.resolve('./_home')); + loadTestFile(require.resolve('./_newsfeed')); loadTestFile(require.resolve('./_add_data')); loadTestFile(require.resolve('./_sample_data')); }); diff --git a/test/functional/apps/visualize/input_control_vis/input_control_options.js b/test/functional/apps/visualize/input_control_vis/input_control_options.js index b659d29b158b7d..4088ab6193a59f 100644 --- a/test/functional/apps/visualize/input_control_vis/input_control_options.js +++ b/test/functional/apps/visualize/input_control_vis/input_control_options.js @@ -133,13 +133,13 @@ export default function ({ getService, getPageObjects }) { describe('updateFiltersOnChange is true', () => { before(async () => { await PageObjects.visualize.clickVisEditorTab('options'); - await PageObjects.visualize.checkCheckbox('inputControlEditorUpdateFiltersOnChangeCheckbox'); + await PageObjects.visualize.checkSwitch('inputControlEditorUpdateFiltersOnChangeCheckbox'); await PageObjects.visualize.clickGo(); }); after(async () => { await PageObjects.visualize.clickVisEditorTab('options'); - await PageObjects.visualize.uncheckCheckbox('inputControlEditorUpdateFiltersOnChangeCheckbox'); + await PageObjects.visualize.uncheckSwitch('inputControlEditorUpdateFiltersOnChangeCheckbox'); await PageObjects.visualize.clickGo(); }); diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index ca141114f976dc..af3a15e9b30153 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -347,7 +347,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { async clickSave() { log.debug('DashboardPage.clickSave'); - await testSubjects.clickWhenNotDisabled('confirmSaveSavedObjectButton'); + await testSubjects.click('confirmSaveSavedObjectButton'); } async pressEnterKey() { @@ -543,9 +543,10 @@ export function DashboardPageProvider({ getService, getPageObjects }) { async setSaveAsNewCheckBox(checked) { log.debug('saveAsNewCheckbox: ' + checked); const saveAsNewCheckbox = await testSubjects.find('saveAsNewCheckbox'); - const isAlreadyChecked = (await saveAsNewCheckbox.getAttribute('checked') === 'true'); + const isAlreadyChecked = (await saveAsNewCheckbox.getAttribute('aria-checked') === 'true'); if (isAlreadyChecked !== checked) { log.debug('Flipping save as new checkbox'); + const saveAsNewCheckbox = await testSubjects.find('saveAsNewCheckbox'); await retry.try(() => saveAsNewCheckbox.click()); } } @@ -553,9 +554,10 @@ export function DashboardPageProvider({ getService, getPageObjects }) { async setStoreTimeWithDashboard(checked) { log.debug('Storing time with dashboard: ' + checked); const storeTimeCheckbox = await testSubjects.find('storeTimeWithDashboard'); - const isAlreadyChecked = (await storeTimeCheckbox.getAttribute('checked') === 'true'); + const isAlreadyChecked = (await storeTimeCheckbox.getAttribute('aria-checked') === 'true'); if (isAlreadyChecked !== checked) { log.debug('Flipping store time checkbox'); + const storeTimeCheckbox = await testSubjects.find('storeTimeWithDashboard'); await retry.try(() => storeTimeCheckbox.click()); } } diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 1e8c454f42cfe3..84562990191d1b 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -35,6 +35,7 @@ import { HeaderPageProvider } from './header_page'; import { HomePageProvider } from './home_page'; // @ts-ignore not TS yet import { MonitoringPageProvider } from './monitoring_page'; +import { NewsfeedPageProvider } from './newsfeed_page'; // @ts-ignore not TS yet import { PointSeriesPageProvider } from './point_series_page'; // @ts-ignore not TS yet @@ -61,6 +62,7 @@ export const pageObjects = { header: HeaderPageProvider, home: HomePageProvider, monitoring: MonitoringPageProvider, + newsfeed: NewsfeedPageProvider, pointSeries: PointSeriesPageProvider, settings: SettingsPageProvider, share: SharePageProvider, diff --git a/test/functional/page_objects/newsfeed_page.ts b/test/functional/page_objects/newsfeed_page.ts new file mode 100644 index 00000000000000..24ff21f0b47de8 --- /dev/null +++ b/test/functional/page_objects/newsfeed_page.ts @@ -0,0 +1,73 @@ +/* + * 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 { FtrProviderContext } from '../ftr_provider_context'; + +export function NewsfeedPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const retry = getService('retry'); + const flyout = getService('flyout'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + + class NewsfeedPage { + async resetPage() { + await PageObjects.common.navigateToUrl('home'); + } + + async closeNewsfeedPanel() { + await flyout.ensureClosed('NewsfeedFlyout'); + log.debug('clickNewsfeed icon'); + await retry.waitFor('newsfeed flyout', async () => { + if (await testSubjects.exists('NewsfeedFlyout')) { + await testSubjects.click('NewsfeedFlyout > euiFlyoutCloseButton'); + return false; + } + return true; + }); + } + + async openNewsfeedPanel() { + log.debug('clickNewsfeed icon'); + return await testSubjects.exists('NewsfeedFlyout'); + } + + async getRedButtonSign() { + return await testSubjects.exists('showBadgeNews'); + } + + async getNewsfeedList() { + const list = await testSubjects.find('NewsfeedFlyout'); + const cells = await list.findAllByCssSelector('[data-test-subj="newsHeadAlert"]'); + + const objects = []; + for (const cell of cells) { + objects.push(await cell.getVisibleText()); + } + + return objects; + } + + async openNewsfeedEmptyPanel() { + return await testSubjects.exists('emptyNewsfeed'); + } + } + + return new NewsfeedPage(); +} diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 570511bee4bc5b..4b65de57f12d81 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -305,9 +305,9 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro public async getRhythmChartLegendValue(nth = 0) { await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - const metricValue = (await find.allByCssSelector( - `.echLegendItem .echLegendItem__displayValue` - ))[nth]; + const metricValue = ( + await find.allByCssSelector(`.echLegendItem .echLegendItem__displayValue`) + )[nth]; await metricValue.moveMouseTo(); return await metricValue.getVisibleText(); } diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index f3a90f20b6686c..81d26a4b69478e 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -372,6 +372,28 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli } } + async isSwitchChecked(selector) { + const checkbox = await testSubjects.find(selector); + const isChecked = await checkbox.getAttribute('aria-checked'); + return isChecked === 'true'; + } + + async checkSwitch(selector) { + const isChecked = await this.isSwitchChecked(selector); + if (!isChecked) { + log.debug(`checking switch ${selector}`); + await testSubjects.click(selector); + } + } + + async uncheckSwitch(selector) { + const isChecked = await this.isSwitchChecked(selector); + if (isChecked) { + log.debug(`unchecking switch ${selector}`); + await testSubjects.click(selector); + } + } + async setSelectByOptionText(selectId, optionText) { const selectField = await find.byCssSelector(`#${selectId}`); const options = await find.allByCssSelector(`#${selectId} > option`); @@ -1009,7 +1031,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli async setIsFilteredByCollarCheckbox(value = true) { await retry.try(async () => { - const isChecked = await this.isChecked('isFilteredByCollarCheckbox'); + const isChecked = await this.isSwitchChecked('isFilteredByCollarCheckbox'); if (isChecked !== value) { await testSubjects.click('isFilteredByCollarCheckbox'); throw new Error('isFilteredByCollar not set correctly'); diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 97e02958f3787f..a8ce4270d42055 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -429,6 +429,15 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { ); } + /** + * Clears session storage for the focused window/frame. + * + * @return {Promise} + */ + public async clearSessionStorage(): Promise { + await driver.executeScript('return window.sessionStorage.clear();'); + } + /** * Closes the currently focused window. In most environments, after the window has been * closed, it is necessary to explicitly switch to whatever window is now focused. diff --git a/test/functional/services/global_nav.ts b/test/functional/services/global_nav.ts index 164ea999fa2793..df3aac67f22a1b 100644 --- a/test/functional/services/global_nav.ts +++ b/test/functional/services/global_nav.ts @@ -32,6 +32,10 @@ export function GlobalNavProvider({ getService }: FtrProviderContext) { return await testSubjects.click('headerGlobalNav > logo'); } + public async clickNewsfeed(): Promise { + return await testSubjects.click('headerGlobalNav > newsfeed'); + } + public async exists(): Promise { return await testSubjects.exists('headerGlobalNav'); } diff --git a/test/functional/services/remote/poll_for_log_entry.ts b/test/functional/services/remote/poll_for_log_entry.ts index b6b68cc0d3cf97..71e2711906fce7 100644 --- a/test/functional/services/remote/poll_for_log_entry.ts +++ b/test/functional/services/remote/poll_for_log_entry.ts @@ -95,10 +95,7 @@ export function pollForLogEntry$( [new logging.Entry('SEVERE', `ERROR FETCHING BROWSR LOGS: ${error.message}`)], // pause 10 seconds then resubscribe - Rx.of(1).pipe( - delay(10 * 1000), - mergeMapTo(resubscribe) - ) + Rx.of(1).pipe(delay(10 * 1000), mergeMapTo(resubscribe)) ); }) ) diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index f134fde028e09d..d6de0be0c172e6 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -118,15 +118,17 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide await testSubjects.setValue('saveQueryFormDescription', description); const currentIncludeFiltersValue = - (await testSubjects.getAttribute('saveQueryFormIncludeFiltersOption', 'checked')) === + (await testSubjects.getAttribute('saveQueryFormIncludeFiltersOption', 'aria-checked')) === 'true'; if (currentIncludeFiltersValue !== includeFilters) { await testSubjects.click('saveQueryFormIncludeFiltersOption'); } const currentIncludeTimeFilterValue = - (await testSubjects.getAttribute('saveQueryFormIncludeTimeFilterOption', 'checked')) === - 'true'; + (await testSubjects.getAttribute( + 'saveQueryFormIncludeTimeFilterOption', + 'aria-checked' + )) === 'true'; if (currentIncludeTimeFilterValue !== includeTimeFilter) { await testSubjects.click('saveQueryFormIncludeTimeFilterOption'); } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 766e6168002c29..da1bb597f57308 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "14.8.0", + "@elastic/eui": "14.9.0", "react": "^16.8.0", "react-dom": "^16.8.0" } diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/kibana.json b/test/plugin_functional/plugins/core_plugin_chromeless/kibana.json new file mode 100644 index 00000000000000..a8a5616627726d --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_chromeless/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "core_plugin_chromeless", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["core_plugin_chromeless"], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/package.json b/test/plugin_functional/plugins/core_plugin_chromeless/package.json new file mode 100644 index 00000000000000..eff6c1e1f142a0 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_chromeless/package.json @@ -0,0 +1,17 @@ +{ + "name": "core_plugin_chromeless", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_plugin_chromeless", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.5.3" + } +} diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/public/application.tsx b/test/plugin_functional/plugins/core_plugin_chromeless/public/application.tsx new file mode 100644 index 00000000000000..556a9ca140715f --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/application.tsx @@ -0,0 +1,74 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import { BrowserRouter as Router, Route } from 'react-router-dom'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +import { AppMountContext, AppMountParameters } from 'kibana/public'; + +const Home = () => ( + + + + +

Welcome to Chromeless!

+
+
+
+ + + + +

Chromeless home page section title

+
+
+
+ Where did all the chrome go? +
+
+); + +const ChromelessApp = ({ basename }: { basename: string; context: AppMountContext }) => ( + + + + + +); + +export const renderApp = ( + context: AppMountContext, + { appBasePath, element }: AppMountParameters +) => { + render(, element); + + return () => unmountComponentAtNode(element); +}; diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts b/test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts new file mode 100644 index 00000000000000..6e9959ecbdf9e5 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { PluginInitializer } from 'kibana/public'; +import { + CorePluginChromelessPlugin, + CorePluginChromelessPluginSetup, + CorePluginChromelessPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + CorePluginChromelessPluginSetup, + CorePluginChromelessPluginStart +> = () => new CorePluginChromelessPlugin(); diff --git a/src/legacy/core_plugins/kibana/public/dev_tools/directives/dev_tools_app.js b/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx similarity index 51% rename from src/legacy/core_plugins/kibana/public/dev_tools/directives/dev_tools_app.js rename to test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx index c7cb877be676b1..03870410fb3342 100644 --- a/src/legacy/core_plugins/kibana/public/dev_tools/directives/dev_tools_app.js +++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx @@ -17,34 +17,31 @@ * under the License. */ -import { uiModules } from 'ui/modules'; -import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; -import template from '../partials/dev_tools_app.html'; +import { Plugin, CoreSetup } from 'kibana/public'; -uiModules - .get('apps/dev_tools') - .directive('kbnDevToolsApp', function (Private, $location) { - const devToolsRegistry = Private(DevToolsRegistryProvider); +export class CorePluginChromelessPlugin + implements Plugin { + public setup(core: CoreSetup, deps: {}) { + core.application.register({ + id: 'chromeless', + title: 'Chromeless', + chromeless: true, + async mount(context, params) { + const { renderApp } = await import('./application'); + return renderApp(context, params); + }, + }); return { - restrict: 'E', - replace: true, - template, - transclude: true, - scope: { - topNavConfig: '=' + getGreeting() { + return 'Hello from Plugin Chromeless!'; }, - bindToController: true, - controllerAs: 'kbnDevToolsApp', - controller() { - this.devTools = devToolsRegistry.inOrder; - this.currentPath = `#${$location.path()}`; - - this.onClick = (item, $event) => { - if (item.disabled) { - $event.preventDefault(); - } - }; - } }; - }); + } + + public start() {} + public stop() {} +} + +export type CorePluginChromelessPluginSetup = ReturnType; +export type CorePluginChromelessPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json new file mode 100644 index 00000000000000..5fcaeafbb0d852 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts b/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts index 377163251010c2..298eaaaf420e04 100644 --- a/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts +++ b/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts @@ -53,9 +53,7 @@ import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common'; * @param context - context supplied by other plugins. * @param search - a search function to access other strategies that have already been registered. */ -export const demoClientSearchStrategyProvider: TSearchStrategyProvider< - typeof DEMO_SEARCH_STRATEGY -> = ( +export const demoClientSearchStrategyProvider: TSearchStrategyProvider = ( context: ISearchContext, search: ISearchGeneric ): ISearchStrategy => { diff --git a/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts b/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts index acb75b15196d69..d3f2360add6c03 100644 --- a/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts +++ b/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts @@ -20,9 +20,7 @@ import { TSearchStrategyProvider } from 'src/plugins/data/server'; import { DEMO_SEARCH_STRATEGY } from '../common'; -export const demoSearchStrategyProvider: TSearchStrategyProvider< - typeof DEMO_SEARCH_STRATEGY -> = () => { +export const demoSearchStrategyProvider: TSearchStrategyProvider = () => { return { search: request => { return Promise.resolve({ diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 7c5b6f6be58af2..4d0444265825a2 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "14.8.0", + "@elastic/eui": "14.9.0", "react": "^16.8.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index ef472b4026957e..196e64af399850 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "14.8.0", + "@elastic/eui": "14.9.0", "react": "^16.8.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 277bb09ac745c5..33e60128d08065 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "14.8.0", + "@elastic/eui": "14.9.0", "react": "^16.8.0" }, "scripts": { diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index eec2ec019a5150..c16847dab9dc23 100644 --- a/test/plugin_functional/test_suites/core_plugins/applications.ts +++ b/test/plugin_functional/test_suites/core_plugins/applications.ts @@ -91,6 +91,22 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider await testSubjects.existOrFail('fooAppPageA'); }); + it('chromeless applications are not visible in apps list', async () => { + expect(await appsMenu.linkExists('Chromeless')).to.be(false); + }); + + it('navigating to chromeless application hides chrome', async () => { + await PageObjects.common.navigateToApp('chromeless'); + await loadingScreenNotShown(); + expect(await testSubjects.exists('headerGlobalNav')).to.be(false); + }); + + it('navigating away from chromeless application shows chrome', async () => { + await PageObjects.common.navigateToApp('foo'); + await loadingScreenNotShown(); + expect(await testSubjects.exists('headerGlobalNav')).to.be(true); + }); + it('can navigate from NP apps to legacy apps', async () => { await appsMenu.clickLink('Management'); await loadingScreenShown(); diff --git a/src/legacy/core_plugins/data/public/query/query_service.mock.ts b/test/ui_capabilities/newsfeed_err/config.ts similarity index 52% rename from src/legacy/core_plugins/data/public/query/query_service.mock.ts rename to test/ui_capabilities/newsfeed_err/config.ts index 19a00632ca1921..1f5f770e8447ce 100644 --- a/src/legacy/core_plugins/data/public/query/query_service.mock.ts +++ b/test/ui_capabilities/newsfeed_err/config.ts @@ -17,35 +17,29 @@ * under the License. */ -import { QueryService, QuerySetup } from '.'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +// @ts-ignore untyped module +import getFunctionalConfig from '../../functional/config'; -type QueryServiceClientContract = PublicMethodsOf; +// eslint-disable-next-line import/no-default-export +export default async ({ readConfigFile }: FtrConfigProviderContext) => { + const functionalConfig = await getFunctionalConfig({ readConfigFile }); -const createSetupContractMock = () => { - const setupContract: jest.Mocked = { - helpers: { - fromUser: jest.fn(), - toUser: jest.fn(), - getQueryLog: jest.fn(), - }, - }; + return { + ...functionalConfig, - return setupContract; -}; - -const createMock = () => { - const mocked: jest.Mocked = { - setup: jest.fn(), - start: jest.fn(), - stop: jest.fn(), - }; + testFiles: [require.resolve('./test')], - mocked.setup.mockReturnValue(createSetupContractMock()); - return mocked; -}; + kbnTestServer: { + ...functionalConfig.kbnTestServer, + serverArgs: [ + ...functionalConfig.kbnTestServer.serverArgs, + `--newsfeed.service.pathTemplate=/api/_newsfeed-FTS-external-service-simulators/kibana/crash.json`, + ], + }, -export const queryServiceMock = { - create: createMock, - createSetupContract: createSetupContractMock, - createStartContract: createSetupContractMock, + junit: { + reportName: 'Newsfeed Error Handling', + }, + }; }; diff --git a/test/ui_capabilities/newsfeed_err/test.ts b/test/ui_capabilities/newsfeed_err/test.ts new file mode 100644 index 00000000000000..2aa81f34028a02 --- /dev/null +++ b/test/ui_capabilities/newsfeed_err/test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function uiCapabilitiesTests({ getService, getPageObjects }: FtrProviderContext) { + const globalNav = getService('globalNav'); + const PageObjects = getPageObjects(['common', 'newsfeed']); + + describe('Newsfeed icon button handle errors', function() { + this.tags('ciGroup6'); + + before(async () => { + await PageObjects.newsfeed.resetPage(); + }); + + it('clicking on newsfeed icon should open you empty newsfeed', async () => { + await globalNav.clickNewsfeed(); + const isOpen = await PageObjects.newsfeed.openNewsfeedPanel(); + expect(isOpen).to.be(true); + + const hasNewsfeedEmptyPanel = await PageObjects.newsfeed.openNewsfeedEmptyPanel(); + expect(hasNewsfeedEmptyPanel).to.be(true); + }); + + it('no red icon', async () => { + const hasCheckedNews = await PageObjects.newsfeed.getRedButtonSign(); + expect(hasCheckedNews).to.be(false); + }); + + it('shows empty panel due to error response', async () => { + const objects = await PageObjects.newsfeed.getNewsfeedList(); + expect(objects).to.eql([]); + }); + + it('clicking on newsfeed icon should close opened newsfeed', async () => { + await globalNav.clickNewsfeed(); + const isOpen = await PageObjects.newsfeed.openNewsfeedPanel(); + expect(isOpen).to.be(false); + }); + }); +} diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 0fba7d2cefbd52..9d601e680cf87b 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -47,7 +47,10 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) '[/\\\\]node_modules(?![\\/\\\\]@elastic[\\/\\\\]eui)(?![\\/\\\\]monaco-editor)[/\\\\].+\\.js$', ], - snapshotSerializers: [`${kibanaDirectory}/node_modules/enzyme-to-json/serializer`], + snapshotSerializers: [ + `${kibanaDirectory}/node_modules/enzyme-to-json/serializer`, + `${kibanaDirectory}/src/plugins/kibana_react/public/util/test_helpers/react_mount_serializer.ts` + ], reporters: [ 'default', [ diff --git a/x-pack/legacy/common/eui_draggable/index.d.ts b/x-pack/legacy/common/eui_draggable/index.d.ts index a85da7a69534cb..322966b3c982ef 100644 --- a/x-pack/legacy/common/eui_draggable/index.d.ts +++ b/x-pack/legacy/common/eui_draggable/index.d.ts @@ -8,7 +8,7 @@ import React from 'react'; import { EuiDraggable, EuiDragDropContext } from '@elastic/eui'; type PropsOf = T extends React.ComponentType ? ComponentProps : never; -type FirstArgumentOf = Func extends ((arg1: infer FirstArgument, ...rest: any[]) => any) +type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any ? FirstArgument : never; export type DragHandleProps = FirstArgumentOf< diff --git a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx b/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx index 8e5fba31ac5a44..8becf6892ff92c 100644 --- a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx +++ b/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx @@ -38,6 +38,6 @@ const { injectGlobal, keyframes, withTheme, -} = styledComponents as ThemedStyledComponentsModule; +} = (styledComponents as unknown) as ThemedStyledComponentsModule; export { css, euiStyled, EuiThemeProvider, injectGlobal, keyframes, withTheme }; diff --git a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts index 3e71725713070d..a5bf42bc2cc019 100644 --- a/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/legacy/plugins/actions/server/lib/task_runner_factory.test.ts @@ -111,11 +111,9 @@ test('executes the task by calling the executor with proper parameters', async ( expect(runnerResult).toBeUndefined(); expect(spaceIdToNamespace).toHaveBeenCalledWith('test'); - expect(mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser).toHaveBeenCalledWith( - 'action_task_params', - '3', - { namespace: 'namespace-test' } - ); + expect( + mockedEncryptedSavedObjectsPlugin.getDecryptedAsInternalUser + ).toHaveBeenCalledWith('action_task_params', '3', { namespace: 'namespace-test' }); expect(mockedActionExecutor.execute).toHaveBeenCalledWith({ actionId: '2', params: { baz: true }, diff --git a/x-pack/legacy/plugins/apm/common/apm_saved_object_constants.ts b/x-pack/legacy/plugins/apm/common/apm_saved_object_constants.ts new file mode 100644 index 00000000000000..ac43b700117c62 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/apm_saved_object_constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// APM Services telemetry +export const APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE = + 'apm-services-telemetry'; +export const APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID = 'apm-services-telemetry'; + +// APM indices +export const APM_INDICES_SAVED_OBJECT_TYPE = 'apm-indices'; +export const APM_INDICES_SAVED_OBJECT_ID = 'apm-indices'; diff --git a/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts index 9a8f11c6493c50..522f6d39ac71a2 100644 --- a/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts +++ b/x-pack/legacy/plugins/apm/common/projections/util/merge_projection/index.ts @@ -21,14 +21,14 @@ type SourceProjection = Omit, 'body'> & { }; type DeepMerge = U extends PlainObject - ? (T extends PlainObject - ? (Omit & - { - [key in keyof U]: T extends { [k in key]: any } - ? DeepMerge - : U[key]; - }) - : U) + ? T extends PlainObject + ? Omit & + { + [key in keyof U]: T extends { [k in key]: any } + ? DeepMerge + : U[key]; + } + : U : U; export function mergeProjection< diff --git a/x-pack/legacy/plugins/grokdebugger/public/sections/grokdebugger/directives/grokdebugger/index.js b/x-pack/legacy/plugins/apm/common/transaction_types.ts similarity index 61% rename from x-pack/legacy/plugins/grokdebugger/public/sections/grokdebugger/directives/grokdebugger/index.js rename to x-pack/legacy/plugins/apm/common/transaction_types.ts index b33973ed71736d..4dd59af63047d5 100644 --- a/x-pack/legacy/plugins/grokdebugger/public/sections/grokdebugger/directives/grokdebugger/index.js +++ b/x-pack/legacy/plugins/apm/common/transaction_types.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './grokdebugger'; +export const TRANSACTION_PAGE_LOAD = 'page-load'; +export const TRANSACTION_ROUTE_CHANGE = 'route-change'; +export const TRANSACTION_REQUEST = 'request'; diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index 556bce9d37bb54..bfbfb4bb99c6a5 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -7,13 +7,10 @@ import { i18n } from '@kbn/i18n'; import { Server } from 'hapi'; import { resolve } from 'path'; -import { - InternalCoreSetup, - PluginInitializerContext -} from '../../../../src/core/server'; +import { PluginInitializerContext } from '../../../../src/core/server'; import { LegacyPluginInitializer } from '../../../../src/legacy/types'; import mappings from './mappings.json'; -import { plugin } from './server/new-platform/index'; +import { plugin } from './server/new-platform'; export const apm: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ @@ -48,7 +45,10 @@ export const apm: LegacyPluginInitializer = kibana => { }, hacks: ['plugins/apm/hacks/toggle_app_link_in_nav'], savedObjectSchemas: { - 'apm-telemetry': { + 'apm-services-telemetry': { + isNamespaceAgnostic: true + }, + 'apm-indices': { isNamespaceAgnostic: true } }, @@ -90,7 +90,7 @@ export const apm: LegacyPluginInitializer = kibana => { catalogue: ['apm'], privileges: { all: { - api: ['apm'], + api: ['apm', 'apm_write'], catalogue: ['apm'], savedObject: { all: [], @@ -111,12 +111,13 @@ export const apm: LegacyPluginInitializer = kibana => { }); const initializerContext = {} as PluginInitializerContext; - const core = { - http: { - server - } - } as InternalCoreSetup; - plugin(initializerContext).setup(core); + const legacySetup = { + server + }; + plugin(initializerContext).setup( + server.newPlatform.setup.core, + legacySetup + ); } }); }; diff --git a/x-pack/legacy/plugins/apm/mappings.json b/x-pack/legacy/plugins/apm/mappings.json index 0b31798242fadc..02296606b1c012 100644 --- a/x-pack/legacy/plugins/apm/mappings.json +++ b/x-pack/legacy/plugins/apm/mappings.json @@ -1,5 +1,5 @@ { - "apm-telemetry": { + "apm-services-telemetry": { "properties": { "has_any_services": { "type": "boolean" diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx index 53f1893a168aca..69f0cf61af2427 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { Component } from 'react'; +import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; import { startMLJob } from '../../../../../services/rest/ml'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; @@ -71,7 +72,7 @@ export class MachineLearningFlyout extends Component { defaultMessage: 'Job creation failed' } ), - text: ( + text: toMountPoint(

{i18n.translate( 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText', @@ -105,7 +106,7 @@ export class MachineLearningFlyout extends Component { defaultMessage: 'Job successfully created' } ), - text: ( + text: toMountPoint(

{i18n.translate( 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText', diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 291208b2d90322..d52c869b958722 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -30,6 +30,7 @@ import { padLeft, range } from 'lodash'; import moment from 'moment-timezone'; import React, { Component } from 'react'; import styled from 'styled-components'; +import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { KibanaCoreContext } from '../../../../../../observability/public'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { KibanaLink } from '../../../shared/Links/KibanaLink'; @@ -219,7 +220,7 @@ export class WatcherFlyout extends Component< defaultMessage: 'Watch creation failed' } ), - text: ( + text: toMountPoint(

{i18n.translate( 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreationFailedNotificationText', @@ -243,7 +244,7 @@ export class WatcherFlyout extends Component< defaultMessage: 'New watch created!' } ), - text: ( + text: toMountPoint(

{i18n.translate( 'xpack.apm.serviceDetails.enableErrorReportsPanel.watchCreatedNotificationText', diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx index 276d309cbb3e39..8005fc17f2a20c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -29,9 +29,7 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) { const { data } = useServiceMetricCharts(urlParams, agentName); const { start, end } = urlParams; - const localFiltersConfig: React.ComponentProps< - typeof LocalUIFilters - > = useMemo( + const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['host', 'containerId', 'podName'], params: { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx index b69076b3a1f704..a118871a5e268a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -34,9 +34,7 @@ const ServiceNodeOverview = () => { const { uiFilters, urlParams } = useUrlParams(); const { serviceName, start, end } = urlParams; - const localFiltersConfig: React.ComponentProps< - typeof LocalUIFilters - > = useMemo( + const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['host', 'containerId', 'podName'], params: { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx index d03e70fc99cc69..0702e092a714f5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -9,6 +9,7 @@ import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo } from 'react'; import url from 'url'; +import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { useFetcher } from '../../../hooks/useFetcher'; import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; @@ -55,7 +56,7 @@ export function ServiceOverview() { defaultMessage: 'Legacy data was detected within the selected time range' }), - text: ( + text: toMountPoint(

{i18n.translate('xpack.apm.serviceOverview.toastText', { defaultMessage: @@ -84,9 +85,7 @@ export function ServiceOverview() { useTrackPageview({ app: 'apm', path: 'services_overview' }); useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); - const localFiltersConfig: React.ComponentProps< - typeof LocalUIFilters - > = useMemo( + const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['host', 'agentName'], projection: PROJECTION.SERVICES diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index db0ddb56e70880..fc86f4bb78afbe 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -195,10 +195,9 @@ export const TransactionDistribution: FunctionComponent = ( } backgroundHover={(bucket: IChartPoint) => bucket.y > 0 && bucket.sample} tooltipHeader={(bucket: IChartPoint) => - `${timeFormatter(bucket.x0, { withUnit: false })} - ${timeFormatter( - bucket.x, - { withUnit: false } - )} ${unit}` + `${timeFormatter(bucket.x0, { + withUnit: false + })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` } tooltipFooter={(bucket: IChartPoint) => !bucket.sample && diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx index d81b7417570a57..f016052df56a2b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -94,9 +94,7 @@ export function TransactionOverview() { } }, [http, serviceName, transactionType]); - const localFiltersConfig: React.ComponentProps< - typeof LocalUIFilters - > = useMemo( + const localFiltersConfig: React.ComponentProps = useMemo( () => ({ filterNames: ['transactionResult', 'host', 'containerId', 'podName'], params: { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 7c0b6f24f87a7b..9918f162a01f43 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -103,12 +103,14 @@ export function KueryBar() { const boolFilter = getBoolFilter(urlParams); try { - const suggestions = (await getSuggestions( - inputValue, - selectionStart, - indexPattern, - boolFilter - )) + const suggestions = ( + await getSuggestions( + inputValue, + selectionStart, + indexPattern, + boolFilter + ) + ) .filter(suggestion => !startsWith(suggestion.text, 'span.')) .slice(0, 15); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/Section.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/Section.tsx index 6f67b2458ea102..0aaeae3e4ce44f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/Section.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/Section.tsx @@ -5,17 +5,18 @@ */ import React from 'react'; +import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { KeyValueTable } from '../KeyValueTable'; import { KeyValuePair } from '../../../utils/flattenObject'; interface Props { - keyValuePairs?: KeyValuePair[]; + keyValuePairs: KeyValuePair[]; } export function Section({ keyValuePairs }: Props) { - if (keyValuePairs) { + if (!isEmpty(keyValuePairs)) { return ; } return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx index bdf895f423913e..4398c129aa7b84 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx @@ -21,7 +21,10 @@ describe('MetadataTable', () => { label: 'Bar', required: false, properties: ['props.A', 'props.B'], - rows: [{ key: 'props.A', value: 'A' }, { key: 'props.B', value: 'B' }] + rows: [ + { key: 'props.A', value: 'A' }, + { key: 'props.B', value: 'B' } + ] } ] as unknown) as SectionsWithRows; const output = render(); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx index 7e68b2f84eeadf..4378c7fdeee0c3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx @@ -11,7 +11,7 @@ import { expectTextsInDocument } from '../../../../utils/testHelpers'; describe('Section', () => { it('shows "empty state message" if no data is available', () => { - const output = render(

); - expectTextsInDocument(output, ['No data available']); + const component = render(
); + expectTextsInDocument(component, ['No data available']); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx index ca14be237d22bb..b7963b5c75a927 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx @@ -78,29 +78,26 @@ interface StackframesGroup { } export function getGroupedStackframes(stackframes: IStackframe[]) { - return stackframes.reduce( - (acc, stackframe) => { - const prevGroup = last(acc); - const shouldAppend = - prevGroup && - prevGroup.isLibraryFrame === stackframe.library_frame && - !prevGroup.excludeFromGrouping && - !stackframe.exclude_from_grouping; + return stackframes.reduce((acc, stackframe) => { + const prevGroup = last(acc); + const shouldAppend = + prevGroup && + prevGroup.isLibraryFrame === stackframe.library_frame && + !prevGroup.excludeFromGrouping && + !stackframe.exclude_from_grouping; - // append to group - if (shouldAppend) { - prevGroup.stackframes.push(stackframe); - return acc; - } - - // create new group - acc.push({ - isLibraryFrame: Boolean(stackframe.library_frame), - excludeFromGrouping: Boolean(stackframe.exclude_from_grouping), - stackframes: [stackframe] - }); + // append to group + if (shouldAppend) { + prevGroup.stackframes.push(stackframe); return acc; - }, - [] as StackframesGroup[] - ); + } + + // create new group + acc.push({ + isLibraryFrame: Boolean(stackframe.library_frame), + excludeFromGrouping: Boolean(stackframe.exclude_from_grouping), + stackframes: [stackframe] + }); + return acc; + }, [] as StackframesGroup[]); } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js index 52afdffcb0839b..d765a57a56a180 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js @@ -32,7 +32,10 @@ class VoronoiPlot extends PureComponent { onMouseLeave={this.props.onMouseLeave} > { formatYShort={t => `${asDecimal(t)} occ.`} formatYLong={t => `${asDecimal(t)} occurrences`} tooltipHeader={bucket => - `${timeFormatter(bucket.x0, { withUnit: false })} - ${timeFormatter( - bucket.x, - { withUnit: false } - )} ${unit}` + `${timeFormatter(bucket.x0, { + withUnit: false + })} - ${timeFormatter(bucket.x, { withUnit: false })} ${unit}` } width={800} /> diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js index 7b9586634c7d0b..50c94fe88e6ad2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js @@ -209,7 +209,10 @@ export class HistogramInner extends PureComponent { )} { return { ...bucket, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx index 12872fd64a3c42..adcce161c7ac1c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx @@ -19,7 +19,7 @@ export const ChoroplethToolTip: React.SFC<{
{name}
{i18n.translate( - 'xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.avgPageLoadDuration', + 'xpack.apm.metrics.durationByCountryMap.RegionMapChart.ToolTip.avgPageLoadDuration', { defaultMessage: 'Avg. page load duration:' } @@ -31,7 +31,7 @@ export const ChoroplethToolTip: React.SFC<{
( {i18n.translate( - 'xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.countPageLoads', + 'xpack.apm.metrics.durationByCountryMap.RegionMapChart.ToolTip.countPageLoads', { values: { docCount: asInteger(docCount) }, defaultMessage: '{docCount} page loads' diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/PageLoadCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx similarity index 88% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/PageLoadCharts/index.tsx rename to x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx index 40c6150149eb5c..6176397170797c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/PageLoadCharts/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { useAvgDurationByCountry } from '../../../../../hooks/useAvgDurationByCountry'; import { ChoroplethMap } from '../ChoroplethMap'; -export const PageLoadCharts: React.SFC = () => { +export const DurationByCountryMap: React.SFC = () => { const { data } = useAvgDurationByCountry(); return ( @@ -20,7 +20,7 @@ export const PageLoadCharts: React.SFC = () => { {i18n.translate( - 'xpack.apm.metrics.pageLoadCharts.avgPageLoadByCountryLabel', + 'xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel', { defaultMessage: 'Avg. page load duration distribution by country' diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 132067a6c32b71..94f30a8a2325a6 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -34,7 +34,12 @@ import { LicenseContext } from '../../../../context/LicenseContext'; import { TransactionLineChart } from './TransactionLineChart'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; import { getTimeFormatter } from '../../../../utils/formatters'; -import { PageLoadCharts } from './PageLoadCharts'; +import { DurationByCountryMap } from './DurationByCountryMap'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_ROUTE_CHANGE, + TRANSACTION_REQUEST +} from '../../../../../common/transaction_types'; interface TransactionChartProps { hasMLJob: boolean; @@ -55,8 +60,6 @@ const ShiftedEuiText = styled(EuiText)` top: 5px; `; -const RUM_PAGE_LOAD_TYPE = 'page-load'; - export class TransactionCharts extends Component { public getMaxY = (responseTimeSeries: TimeSeries[]) => { const coordinates = flatten( @@ -200,19 +203,19 @@ export class TransactionCharts extends Component { - {transactionType === RUM_PAGE_LOAD_TYPE ? ( + {transactionType === TRANSACTION_PAGE_LOAD && ( <> - + - ) : null} + )} ); } } function tpmLabel(type?: string) { - return type === 'request' + return type === TRANSACTION_REQUEST ? i18n.translate( 'xpack.apm.metrics.transactionChart.requestsPerMinuteLabel', { @@ -229,14 +232,14 @@ function tpmLabel(type?: string) { function responseTimeLabel(type?: string) { switch (type) { - case RUM_PAGE_LOAD_TYPE: + case TRANSACTION_PAGE_LOAD: return i18n.translate( 'xpack.apm.metrics.transactionChart.pageLoadTimesLabel', { defaultMessage: 'Page load times' } ); - case 'route-change': + case TRANSACTION_ROUTE_CHANGE: return i18n.translate( 'xpack.apm.metrics.transactionChart.routeChangeTimesLabel', { diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByCountry.ts b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByCountry.ts index b794332d4aa631..6b3fa1f0d98f7a 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByCountry.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByCountry.ts @@ -9,7 +9,7 @@ import { useUrlParams } from './useUrlParams'; export function useAvgDurationByCountry() { const { - urlParams: { serviceName, start, end }, + urlParams: { serviceName, start, end, transactionName }, uiFilters } = useUrlParams(); @@ -24,13 +24,14 @@ export function useAvgDurationByCountry() { query: { start, end, - uiFilters: JSON.stringify(uiFilters) + uiFilters: JSON.stringify(uiFilters), + transactionName } } }); } }, - [serviceName, start, end, uiFilters] + [serviceName, start, end, uiFilters, transactionName] ); return { diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx index ba74b0175ff71a..bc6382841be3f1 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx @@ -8,6 +8,7 @@ import React, { useContext, useEffect, useState, useMemo } from 'react'; import { idx } from '@kbn/elastic-idx'; import { i18n } from '@kbn/i18n'; import { IHttpFetchError } from 'src/core/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext'; import { useComponentId } from './useComponentId'; import { useKibanaCore } from '../../../observability/public'; @@ -92,7 +93,7 @@ export function useFetcher( title: i18n.translate('xpack.apm.fetcher.error.title', { defaultMessage: `Error while fetching resource` }), - text: ( + text: toMountPoint(
{i18n.translate('xpack.apm.fetcher.error.status', { diff --git a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts index 80a1b96efb3d6d..2b0263f69db8fa 100644 --- a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts +++ b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts @@ -35,9 +35,18 @@ describe('chartSelectors', () => { describe('getResponseTimeSeries', () => { const apmTimeseries = { responseTimes: { - avg: [{ x: 0, y: 100 }, { x: 1000, y: 200 }], - p95: [{ x: 0, y: 200 }, { x: 1000, y: 300 }], - p99: [{ x: 0, y: 300 }, { x: 1000, y: 400 }] + avg: [ + { x: 0, y: 100 }, + { x: 1000, y: 200 } + ], + p95: [ + { x: 0, y: 200 }, + { x: 1000, y: 300 } + ], + p99: [ + { x: 0, y: 300 }, + { x: 1000, y: 400 } + ] }, tpmBuckets: [], overallAvgDuration: 200 @@ -49,21 +58,30 @@ describe('chartSelectors', () => { ).toEqual([ { color: '#3185fc', - data: [{ x: 0, y: 100 }, { x: 1000, y: 200 }], + data: [ + { x: 0, y: 100 }, + { x: 1000, y: 200 } + ], legendValue: '0 ms', title: 'Avg.', type: 'linemark' }, { color: '#e6c220', - data: [{ x: 0, y: 200 }, { x: 1000, y: 300 }], + data: [ + { x: 0, y: 200 }, + { x: 1000, y: 300 } + ], title: '95th percentile', titleShort: '95th', type: 'linemark' }, { color: '#f98510', - data: [{ x: 0, y: 300 }, { x: 1000, y: 400 }], + data: [ + { x: 0, y: 300 }, + { x: 1000, y: 400 } + ], title: '99th percentile', titleShort: '99th', type: 'linemark' @@ -87,7 +105,13 @@ describe('chartSelectors', () => { p99: [] }, tpmBuckets: [ - { key: 'HTTP 2xx', dataPoints: [{ x: 0, y: 5 }, { x: 0, y: 2 }] }, + { + key: 'HTTP 2xx', + dataPoints: [ + { x: 0, y: 5 }, + { x: 0, y: 2 } + ] + }, { key: 'HTTP 4xx', dataPoints: [{ x: 0, y: 1 }] }, { key: 'HTTP 5xx', dataPoints: [{ x: 0, y: 0 }] } ], @@ -99,7 +123,10 @@ describe('chartSelectors', () => { expect(getTpmSeries(apmTimeseries, transactionType)).toEqual([ { color: successColor, - data: [{ x: 0, y: 5 }, { x: 0, y: 2 }], + data: [ + { x: 0, y: 5 }, + { x: 0, y: 2 } + ], legendValue: '3.5 tpm', title: 'HTTP 2xx', type: 'linemark' @@ -220,9 +247,18 @@ describe('chartSelectors', () => { describe('when empty', () => { it('produces an empty series', () => { const responseTimes = { - avg: [{ x: 0, y: 1 }, { x: 100, y: 1 }], - p95: [{ x: 0, y: 1 }, { x: 100, y: 1 }], - p99: [{ x: 0, y: 1 }, { x: 100, y: 1 }] + avg: [ + { x: 0, y: 1 }, + { x: 100, y: 1 } + ], + p95: [ + { x: 0, y: 1 }, + { x: 100, y: 1 } + ], + p99: [ + { x: 0, y: 1 }, + { x: 100, y: 1 } + ] }; const series = getTpmSeries( { ...apmTimeseries, responseTimes, tpmBuckets: [] }, diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index a18882120fe757..a224df9e59e58f 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -97,6 +97,7 @@ interface MockSetup { start: number; end: number; client: any; + internalClient: any; config: { get: any; has: any; @@ -122,12 +123,21 @@ export async function inspectSearchParams( } }); + const internalClientSpy = jest.fn().mockReturnValueOnce({ + hits: { + total: 0 + } + }); + const mockSetup = { start: 1528113600000, end: 1528977600000, client: { search: clientSpy } as any, + internalClient: { + search: internalClientSpy + } as any, config: { get: () => 'myIndex' as any, has: () => true @@ -153,8 +163,15 @@ export async function inspectSearchParams( // we're only extracting the search params } + let params; + if (clientSpy.mock.calls.length) { + params = clientSpy.mock.calls[0][0]; + } else { + params = internalClientSpy.mock.calls[0][0]; + } + return { - params: clientSpy.mock.calls[0][0], + params, teardown: () => clientSpy.mockClear() }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/__test__/index.test.ts similarity index 83% rename from x-pack/legacy/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts rename to x-pack/legacy/plugins/apm/server/lib/apm_telemetry/__test__/index.test.ts index 6db6e8848ef07e..26cae303542a40 100644 --- a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/__test__/apm_telemetry.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/__test__/index.test.ts @@ -5,11 +5,11 @@ */ import { SavedObjectAttributes } from 'src/core/server'; +import { createApmTelementry, storeApmServicesTelemetry } from '../index'; import { - APM_TELEMETRY_DOC_ID, - createApmTelementry, - storeApmTelemetry -} from '../apm_telemetry'; + APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE, + APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID +} from '../../../../common/apm_saved_object_constants'; describe('apm_telemetry', () => { describe('createApmTelementry', () => { @@ -44,7 +44,7 @@ describe('apm_telemetry', () => { }); }); - describe('storeApmTelemetry', () => { + describe('storeApmServicesTelemetry', () => { let server: any; let apmTelemetry: SavedObjectAttributes; let savedObjectsClientInstance: any; @@ -75,24 +75,24 @@ describe('apm_telemetry', () => { }); it('should call savedObjectsClient create with the given ApmTelemetry object', () => { - storeApmTelemetry(server, apmTelemetry); + storeApmServicesTelemetry(server, apmTelemetry); expect(savedObjectsClientInstance.create.mock.calls[0][1]).toBe( apmTelemetry ); }); it('should call savedObjectsClient create with the apm-telemetry document type and ID', () => { - storeApmTelemetry(server, apmTelemetry); + storeApmServicesTelemetry(server, apmTelemetry); expect(savedObjectsClientInstance.create.mock.calls[0][0]).toBe( - 'apm-telemetry' + APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE ); expect(savedObjectsClientInstance.create.mock.calls[0][2].id).toBe( - APM_TELEMETRY_DOC_ID + APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID ); }); it('should call savedObjectsClient create with overwrite: true', () => { - storeApmTelemetry(server, apmTelemetry); + storeApmServicesTelemetry(server, apmTelemetry); expect(savedObjectsClientInstance.create.mock.calls[0][2].overwrite).toBe( true ); diff --git a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts deleted file mode 100644 index 54106cce10bac0..00000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/apm_telemetry.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Server } from 'hapi'; -import { countBy } from 'lodash'; -import { SavedObjectAttributes } from 'src/core/server'; -import { isAgentName } from '../../../common/agent_name'; -import { getSavedObjectsClient } from '../helpers/saved_objects_client'; - -export const APM_TELEMETRY_DOC_ID = 'apm-telemetry'; - -export function createApmTelementry( - agentNames: string[] = [] -): SavedObjectAttributes { - const validAgentNames = agentNames.filter(isAgentName); - return { - has_any_services: validAgentNames.length > 0, - services_per_agent: countBy(validAgentNames) - }; -} - -export async function storeApmTelemetry( - server: Server, - apmTelemetry: SavedObjectAttributes -) { - try { - const savedObjectsClient = getSavedObjectsClient(server); - await savedObjectsClient.create('apm-telemetry', apmTelemetry, { - id: APM_TELEMETRY_DOC_ID, - overwrite: true - }); - } catch (e) { - // eslint-disable-next-line no-console - console.error('Could not send APM telemetry:', e.message); - } -} diff --git a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts index 754666b0a9fa2e..640072d6ec4d8b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/index.ts @@ -4,9 +4,77 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - storeApmTelemetry, - createApmTelementry, - APM_TELEMETRY_DOC_ID -} from './apm_telemetry'; -export { makeApmUsageCollector } from './make_apm_usage_collector'; +import { Server } from 'hapi'; +import { countBy } from 'lodash'; +import { SavedObjectAttributes } from 'src/core/server'; +import { CoreSetup } from 'src/core/server'; +import { isAgentName } from '../../../common/agent_name'; +import { getInternalSavedObjectsClient } from '../helpers/saved_objects_client'; +import { + APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE, + APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID +} from '../../../common/apm_saved_object_constants'; +import { LegacySetup } from '../../new-platform/plugin'; + +export function createApmTelementry( + agentNames: string[] = [] +): SavedObjectAttributes { + const validAgentNames = agentNames.filter(isAgentName); + return { + has_any_services: validAgentNames.length > 0, + services_per_agent: countBy(validAgentNames) + }; +} + +export async function storeApmServicesTelemetry( + server: Server, + apmTelemetry: SavedObjectAttributes +) { + try { + const internalSavedObjectsClient = getInternalSavedObjectsClient(server); + await internalSavedObjectsClient.create( + APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE, + apmTelemetry, + { + id: APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID, + overwrite: true + } + ); + } catch (e) { + server.log(['error'], `Unable to save APM telemetry data: ${e.message}`); + } +} + +interface LegacySetupWithUsageCollector extends LegacySetup { + server: LegacySetup['server'] & { + usage: { + collectorSet: { + makeUsageCollector: (options: unknown) => unknown; + register: (options: unknown) => unknown; + }; + }; + }; +} + +export function makeApmUsageCollector( + core: CoreSetup, + { server }: LegacySetupWithUsageCollector +) { + const apmUsageCollector = server.usage.collectorSet.makeUsageCollector({ + type: 'apm', + fetch: async () => { + const internalSavedObjectsClient = getInternalSavedObjectsClient(server); + try { + const apmTelemetrySavedObject = await internalSavedObjectsClient.get( + APM_SERVICES_TELEMETRY_SAVED_OBJECT_TYPE, + APM_SERVICES_TELEMETRY_SAVED_OBJECT_ID + ); + return apmTelemetrySavedObject.attributes; + } catch (err) { + return createApmTelementry(); + } + }, + isReady: () => true + }); + server.usage.collectorSet.register(apmUsageCollector); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/make_apm_usage_collector.ts b/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/make_apm_usage_collector.ts deleted file mode 100644 index 8a91bd8781fe77..00000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/apm_telemetry/make_apm_usage_collector.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { InternalCoreSetup } from 'src/core/server'; -import { getSavedObjectsClient } from '../helpers/saved_objects_client'; -import { APM_TELEMETRY_DOC_ID, createApmTelementry } from './apm_telemetry'; - -export interface CoreSetupWithUsageCollector extends InternalCoreSetup { - http: InternalCoreSetup['http'] & { - server: { - usage: { - collectorSet: { - makeUsageCollector: (options: unknown) => unknown; - register: (options: unknown) => unknown; - }; - }; - }; - }; -} - -export function makeApmUsageCollector(core: CoreSetupWithUsageCollector) { - const { server } = core.http; - - const apmUsageCollector = server.usage.collectorSet.makeUsageCollector({ - type: 'apm', - fetch: async () => { - const savedObjectsClient = getSavedObjectsClient(server); - try { - const apmTelemetrySavedObject = await savedObjectsClient.get( - 'apm-telemetry', - APM_TELEMETRY_DOC_ID - ); - return apmTelemetrySavedObject.attributes; - } catch (err) { - return createApmTelementry(); - } - }, - isReady: () => true - }); - server.usage.collectorSet.register(apmUsageCollector); -} diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts index b7081c43465bf8..5bbd6be14a7082 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/distribution/__tests__/get_buckets.test.ts @@ -31,6 +31,9 @@ describe('timeseriesFetcher', () => { client: { search: clientSpy } as any, + internalClient: { + search: clientSpy + } as any, config: { get: () => 'myIndex' as any, has: () => true diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts index ee41599454dd69..9c111910f16f9c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts @@ -66,8 +66,12 @@ async function getParamsForSearchRequest( apmOptions?: APMOptions ) { const uiSettings = req.getUiSettingsService(); + const { server } = req; const [indices, includeFrozen] = await Promise.all([ - getApmIndices(req.server), + getApmIndices({ + config: server.config(), + savedObjectsClient: server.savedObjects.getScopedSavedObjectsClient(req) + }), uiSettings.get('search:includeFrozen') ]); @@ -92,10 +96,23 @@ interface APMOptions { includeLegacyData: boolean; } -export function getESClient(req: Legacy.Request) { +interface ClientCreateOptions { + clientAsInternalUser?: boolean; +} + +export type ESClient = ReturnType; + +export function getESClient( + req: Legacy.Request, + { clientAsInternalUser = false }: ClientCreateOptions = {} +) { const cluster = req.server.plugins.elasticsearch.getCluster('data'); const query = req.query as Record; + const callMethod = clientAsInternalUser + ? cluster.callWithInternalUser.bind(cluster) + : cluster.callWithRequest.bind(cluster, req); + return { search: async < TDocument = unknown, @@ -121,20 +138,18 @@ export function getESClient(req: Legacy.Request) { console.log(JSON.stringify(nextParams.body, null, 4)); } - return (cluster.callWithRequest( - req, - 'search', - nextParams - ) as unknown) as Promise>; + return (callMethod('search', nextParams) as unknown) as Promise< + ESSearchResponse + >; }, index: (params: APMIndexDocumentParams) => { - return cluster.callWithRequest(req, 'index', params); + return callMethod('index', params); }, delete: (params: IndicesDeleteParams) => { - return cluster.callWithRequest(req, 'delete', params); + return callMethod('delete', params); }, indicesCreate: (params: IndicesCreateParams) => { - return cluster.callWithRequest(req, 'indices.create', params); + return callMethod('indices.create', params); } }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.test.ts index 3af1d8c706f46f..c685ffdd801dc0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSavedObjectsClient } from './saved_objects_client'; +import { getInternalSavedObjectsClient } from './saved_objects_client'; describe('saved_objects/client', () => { describe('getSavedObjectsClient', () => { @@ -31,7 +31,7 @@ describe('saved_objects/client', () => { }); it('should use internal user "admin"', () => { - getSavedObjectsClient(server); + getInternalSavedObjectsClient(server); expect(server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith( 'admin' @@ -39,7 +39,7 @@ describe('saved_objects/client', () => { }); it('should call getSavedObjectsRepository with a cluster using the internal user context', () => { - getSavedObjectsClient(server); + getInternalSavedObjectsClient(server); expect( server.savedObjects.getSavedObjectsRepository @@ -47,9 +47,9 @@ describe('saved_objects/client', () => { }); it('should return a SavedObjectsClient initialized with the saved objects internal repository', () => { - const result = getSavedObjectsClient(server); + const internalSavedObjectsClient = getInternalSavedObjectsClient(server); - expect(result).toBe(savedObjectsClientInstance); + expect(internalSavedObjectsClient).toBe(savedObjectsClientInstance); expect(server.savedObjects.SavedObjectsClient).toHaveBeenCalledWith( internalRepository ); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.ts index 81dd8b34c88472..f164ca39d51c0f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/saved_objects_client.ts @@ -6,7 +6,10 @@ import { Server } from 'hapi'; -export function getSavedObjectsClient(server: Server, clusterName = 'admin') { +export function getInternalSavedObjectsClient( + server: Server, + clusterName = 'admin' +) { const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; const { callWithInternalUser } = server.plugins.elasticsearch.getCluster( clusterName diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts index 57de438be7f2ad..f43b9ea11487a1 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -21,6 +21,7 @@ jest.mock('../settings/apm_indices/get_apm_indices', () => ({ function getMockRequest() { const callWithRequestSpy = jest.fn(); + const callWithInternalUserSpy = jest.fn(); const mockRequest = ({ params: {}, query: {}, @@ -28,14 +29,20 @@ function getMockRequest() { config: () => ({ get: () => 'apm-*' }), plugins: { elasticsearch: { - getCluster: () => ({ callWithRequest: callWithRequestSpy }) + getCluster: () => ({ + callWithRequest: callWithRequestSpy, + callWithInternalUser: callWithInternalUserSpy + }) } + }, + savedObjects: { + getScopedSavedObjectsClient: () => ({ get: async () => false }) } }, getUiSettingsService: () => ({ get: async () => false }) } as any) as Legacy.Request; - return { callWithRequestSpy, mockRequest }; + return { callWithRequestSpy, callWithInternalUserSpy, mockRequest }; } describe('setupRequest', () => { @@ -57,6 +64,27 @@ describe('setupRequest', () => { }); }); + it('should call callWithInternalUser with default args', async () => { + const { mockRequest, callWithInternalUserSpy } = getMockRequest(); + const { internalClient } = await setupRequest(mockRequest); + await internalClient.search({ + index: 'apm-*', + body: { foo: 'bar' } + } as any); + expect(callWithInternalUserSpy).toHaveBeenCalledWith('search', { + index: 'apm-*', + body: { + foo: 'bar', + query: { + bool: { + filter: [{ range: { 'observer.version_major': { gte: 7 } } }] + } + } + }, + ignore_throttled: true + }); + }); + describe('observer.version_major filter', () => { describe('if index is apm-*', () => { it('should merge `observer.version_major` filter with existing boolean filters', async () => { diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index 3ec519d5e71b57..dcc034287863af 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -31,17 +31,21 @@ export type Setup = PromiseReturnType; export async function setupRequest(req: Legacy.Request) { const query = (req.query as unknown) as APMRequestQuery; const { server } = req; + const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient( + req + ); const config = server.config(); const [uiFiltersES, indices] = await Promise.all([ decodeUiFilters(server, query.uiFilters), - getApmIndices(server) + getApmIndices({ config, savedObjectsClient }) ]); return { start: moment.utc(query.start).valueOf(), end: moment.utc(query.end).valueOf(), uiFiltersES, - client: getESClient(req), + client: getESClient(req, { clientAsInternalUser: false }), + internalClient: getESClient(req, { clientAsInternalUser: true }), config, indices }; diff --git a/x-pack/legacy/plugins/apm/server/lib/index_pattern/index.ts b/x-pack/legacy/plugins/apm/server/lib/index_pattern/index.ts index 0b9407b288b1df..1aff1d772c5c23 100644 --- a/x-pack/legacy/plugins/apm/server/lib/index_pattern/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/index_pattern/index.ts @@ -4,18 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { Server } from 'hapi'; -import { getSavedObjectsClient } from '../helpers/saved_objects_client'; +import { getInternalSavedObjectsClient } from '../helpers/saved_objects_client'; import apmIndexPattern from '../../../../../../../src/legacy/core_plugins/kibana/server/tutorials/apm/index_pattern.json'; export async function getAPMIndexPattern(server: Server) { const config = server.config(); const apmIndexPatternTitle = config.get('apm_oss.indexPattern'); - const savedObjectsClient = getSavedObjectsClient(server); + const internalSavedObjectsClient = getInternalSavedObjectsClient(server); try { - return await savedObjectsClient.get('index-pattern', apmIndexPattern.id); + return await internalSavedObjectsClient.get( + 'index-pattern', + apmIndexPattern.id + ); } catch (error) { // if GET fails, then create a new index pattern saved object - return await savedObjectsClient.create( + return await internalSavedObjectsClient.create( 'index-pattern', { ...apmIndexPattern.attributes, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index 861732ee039232..434eda8c0f46eb 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -4,16 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InternalCoreSetup } from 'src/core/server'; +import { CoreSetup } from 'src/core/server'; import { CallCluster } from '../../../../../../../../src/legacy/core_plugins/elasticsearch'; import { getApmIndices } from '../apm_indices/get_apm_indices'; +import { LegacySetup } from '../../../new-platform/plugin'; +import { getInternalSavedObjectsClient } from '../../helpers/saved_objects_client'; export async function createApmAgentConfigurationIndex( - core: InternalCoreSetup + core: CoreSetup, + { server }: LegacySetup ) { try { - const { server } = core.http; - const indices = await getApmIndices(server); + const config = server.config(); + const internalSavedObjectsClient = getInternalSavedObjectsClient(server); + const indices = await getApmIndices({ + savedObjectsClient: internalSavedObjectsClient, + config + }); const index = indices['apm_oss.apmAgentConfigurationIndex']; const { callWithInternalUser } = server.plugins.elasticsearch.getCluster( 'admin' diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 25a4f5141498f4..23faa4b74cf8fb 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -21,7 +21,7 @@ export async function createOrUpdateConfiguration({ >; setup: Setup; }) { - const { client, indices } = setup; + const { internalClient, indices } = setup; const params: APMIndexDocumentParams = { refresh: true, @@ -44,5 +44,5 @@ export async function createOrUpdateConfiguration({ params.id = configurationId; } - return client.index(params); + return internalClient.index(params); } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts index 896363c054ba78..ed20a58b271e10 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/delete_configuration.ts @@ -13,7 +13,7 @@ export async function deleteConfiguration({ configurationId: string; setup: Setup; }) { - const { client, indices } = setup; + const { internalClient, indices } = setup; const params = { refresh: 'wait_for', @@ -21,5 +21,5 @@ export async function deleteConfiguration({ id: configurationId }; - return client.delete(params); + return internalClient.delete(params); } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts index d5aa389cea3357..52efc2b50305b9 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts @@ -19,7 +19,7 @@ export async function getExistingEnvironmentsForService({ serviceName: string | undefined; setup: Setup; }) { - const { client, indices } = setup; + const { internalClient, indices } = setup; const bool = serviceName ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] } @@ -42,7 +42,7 @@ export async function getExistingEnvironmentsForService({ } }; - const resp = await client.search(params); + const resp = await internalClient.search(params); const buckets = idx(resp.aggregations, _ => _.environments.buckets) || []; return buckets.map(bucket => bucket.key as string); } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts index 283f30b51441d3..dd4d019ef7263b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/list_configurations.ts @@ -12,13 +12,13 @@ export type AgentConfigurationListAPIResponse = PromiseReturnType< typeof listConfigurations >; export async function listConfigurations({ setup }: { setup: Setup }) { - const { client, indices } = setup; + const { internalClient, indices } = setup; const params = { index: indices['apm_oss.apmAgentConfigurationIndex'] }; - const resp = await client.search(params); + const resp = await internalClient.search(params); return resp.hits.hits.map(item => ({ id: item._id, ...item._source diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts index e5349edb67f308..b7b9c21172140a 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts @@ -16,7 +16,7 @@ export async function markAppliedByAgent({ body: AgentConfiguration; setup: Setup; }) { - const { client, indices } = setup; + const { internalClient, indices } = setup; const params = { index: indices['apm_oss.apmAgentConfigurationIndex'], @@ -27,5 +27,5 @@ export async function markAppliedByAgent({ } }; - return client.index(params); + return internalClient.index(params); } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts index 400bd0207771af..dcf7329b229d85 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts @@ -16,6 +16,7 @@ describe('search configurations', () => { setup: ({ config: { get: () => '' }, client: { search: async () => searchMocks }, + internalClient: { search: async () => searchMocks }, indices: { apm_oss: { sourcemapIndices: 'myIndex', @@ -41,6 +42,7 @@ describe('search configurations', () => { setup: ({ config: { get: () => '' }, client: { search: async () => searchMocks }, + internalClient: { search: async () => searchMocks }, indices: { apm_oss: { sourcemapIndices: 'myIndex', diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts index 35d76d745cf4f1..969bbc542f8a6e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts @@ -20,7 +20,7 @@ export async function searchConfigurations({ environment?: string; setup: Setup; }) { - const { client, indices } = setup; + const { internalClient, indices } = setup; // sorting order // 1. exact match: service.name AND service.environment (eg. opbeans-node / production) @@ -49,7 +49,9 @@ export async function searchConfigurations({ } }; - const resp = await client.search(params); + const resp = await internalClient.search( + params + ); const { hits } = resp.hits; const exactMatch = hits.find( diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index cd237a52640990..e942a26da373ef 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import { merge } from 'lodash'; import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { getSavedObjectsClient } from '../../helpers/saved_objects_client'; -import { Setup } from '../../helpers/setup_request'; +import { Server } from 'hapi'; import { PromiseReturnType } from '../../../../typings/common'; +import { + APM_INDICES_SAVED_OBJECT_TYPE, + APM_INDICES_SAVED_OBJECT_ID +} from '../../../../common/apm_saved_object_constants'; export interface ApmIndicesConfig { 'apm_oss.sourcemapIndices': string; @@ -23,11 +25,13 @@ export interface ApmIndicesConfig { export type ApmIndicesName = keyof ApmIndicesConfig; -export const APM_INDICES_SAVED_OBJECT_TYPE = 'apm-indices'; -export const APM_INDICES_SAVED_OBJECT_ID = 'apm-indices'; +export type ScopedSavedObjectsClient = ReturnType< + Server['savedObjects']['getScopedSavedObjectsClient'] +>; -async function getApmIndicesSavedObject(server: Server) { - const savedObjectsClient = getSavedObjectsClient(server, 'data'); +async function getApmIndicesSavedObject( + savedObjectsClient: ScopedSavedObjectsClient +) { const apmIndices = await savedObjectsClient.get>( APM_INDICES_SAVED_OBJECT_TYPE, APM_INDICES_SAVED_OBJECT_ID @@ -53,13 +57,21 @@ function getApmIndicesConfig(config: KibanaConfig): ApmIndicesConfig { }; } -export async function getApmIndices(server: Server) { +export async function getApmIndices({ + savedObjectsClient, + config +}: { + savedObjectsClient: ScopedSavedObjectsClient; + config: KibanaConfig; +}) { try { - const apmIndicesSavedObject = await getApmIndicesSavedObject(server); - const apmIndicesConfig = getApmIndicesConfig(server.config()); + const apmIndicesSavedObject = await getApmIndicesSavedObject( + savedObjectsClient + ); + const apmIndicesConfig = getApmIndicesConfig(config); return merge({}, apmIndicesConfig, apmIndicesSavedObject); } catch (error) { - return getApmIndicesConfig(server.config()); + return getApmIndicesConfig(config); } } @@ -74,16 +86,15 @@ const APM_UI_INDICES: ApmIndicesName[] = [ ]; export async function getApmIndexSettings({ - setup, - server + config, + savedObjectsClient }: { - setup: Setup; - server: Server; + config: KibanaConfig; + savedObjectsClient: ScopedSavedObjectsClient; }) { - const { config } = setup; let apmIndicesSavedObject: PromiseReturnType; try { - apmIndicesSavedObject = await getApmIndicesSavedObject(server); + apmIndicesSavedObject = await getApmIndicesSavedObject(savedObjectsClient); } catch (error) { if (error.output && error.output.statusCode === 404) { apmIndicesSavedObject = {}; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/save_apm_indices.ts b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/save_apm_indices.ts index 8de47c5c44144f..e57e64942ab898 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/save_apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/save_apm_indices.ts @@ -4,19 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; -import { getSavedObjectsClient } from '../../helpers/saved_objects_client'; +import { ApmIndicesConfig, ScopedSavedObjectsClient } from './get_apm_indices'; import { - ApmIndicesConfig, APM_INDICES_SAVED_OBJECT_TYPE, APM_INDICES_SAVED_OBJECT_ID -} from './get_apm_indices'; +} from '../../../../common/apm_saved_object_constants'; export async function saveApmIndices( - server: Server, + savedObjectsClient: ScopedSavedObjectsClient, apmIndicesSavedObject: Partial ) { - const savedObjectsClient = getSavedObjectsClient(server, 'data'); return await savedObjectsClient.create( APM_INDICES_SAVED_OBJECT_TYPE, apmIndicesSavedObject, diff --git a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts index 99553690359cf5..ca10183bb259e4 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transaction_groups/fetcher.test.ts @@ -13,6 +13,9 @@ function getSetup() { client: { search: jest.fn() } as any, + internalClient: { + search: jest.fn() + } as any, config: { get: jest.fn((key: string) => { switch (key) { diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index e092942a25ba65..ed6bdf203f2d44 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -9,19 +9,26 @@ import { PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_DURATION, - TRANSACTION_TYPE + TRANSACTION_TYPE, + TRANSACTION_NAME } from '../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../helpers/setup_request'; import { rangeFilter } from '../../helpers/range_filter'; +import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; export async function getTransactionAvgDurationByCountry({ setup, - serviceName + serviceName, + transactionName }: { setup: Setup; serviceName: string; + transactionName?: string; }) { const { uiFiltersES, client, start, end, indices } = setup; + const transactionNameFilter = transactionName + ? [{ term: { [TRANSACTION_NAME]: transactionName } }] + : []; const params = { index: indices['apm_oss.transactionIndices'], body: { @@ -30,8 +37,9 @@ export async function getTransactionAvgDurationByCountry({ bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, + ...transactionNameFilter, { term: { [PROCESSOR_EVENT]: 'transaction' } }, - { term: { [TRANSACTION_TYPE]: 'page-load' } }, + { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, { exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } }, { range: rangeFilter(start, end) }, ...uiFiltersES diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 67816d67a29a2a..2648851789c662 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -30,6 +30,7 @@ describe('getTransactionBreakdown', () => { start: 0, end: 500000, client: { search: clientSpy } as any, + internalClient: { search: clientSpy } as any, config: { get: () => 'myIndex' as any, has: () => true @@ -54,6 +55,7 @@ describe('getTransactionBreakdown', () => { start: 0, end: 500000, client: { search: clientSpy } as any, + internalClient: { search: clientSpy } as any, config: { get: () => 'myIndex' as any, has: () => true @@ -95,6 +97,7 @@ describe('getTransactionBreakdown', () => { start: 0, end: 500000, client: { search: clientSpy } as any, + internalClient: { search: clientSpy } as any, config: { get: () => 'myIndex' as any, has: () => true @@ -135,6 +138,7 @@ describe('getTransactionBreakdown', () => { start: 0, end: 500000, client: { search: clientSpy } as any, + internalClient: { search: clientSpy } as any, config: { get: () => 'myIndex' as any, has: () => true @@ -159,6 +163,7 @@ describe('getTransactionBreakdown', () => { start: 0, end: 500000, client: { search: clientSpy } as any, + internalClient: { search: clientSpy } as any, config: { get: () => 'myIndex' as any, has: () => true diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts index a21c4f38ac30cc..3d425415de8326 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -151,56 +151,53 @@ export async function getTransactionBreakdown({ const bucketsByDate = idx(resp.aggregations, _ => _.by_date.buckets) || []; - const timeseriesPerSubtype = bucketsByDate.reduce( - (prev, bucket) => { - const formattedValues = formatBucket(bucket); - const time = bucket.key; - - const updatedSeries = kpiNames.reduce((p, kpiName) => { - const { name, percentage } = formattedValues.find( - val => val.name === kpiName - ) || { - name: kpiName, - percentage: null - }; - - if (!p[name]) { - p[name] = []; - } - return { - ...p, - [name]: p[name].concat({ - x: time, - y: percentage - }) - }; - }, prev); - - const lastValues = Object.values(updatedSeries).map(last); - - // If for a given timestamp, some series have data, but others do not, - // we have to set any null values to 0 to make sure the stacked area chart - // is drawn correctly. - // If we set all values to 0, the chart always displays null values as 0, - // and the chart looks weird. - const hasAnyValues = lastValues.some(value => value.y !== null); - const hasNullValues = lastValues.some(value => value.y === null); - - if (hasAnyValues && hasNullValues) { - Object.values(updatedSeries).forEach(series => { - const value = series[series.length - 1]; - const isEmpty = value.y === null; - if (isEmpty) { - // local mutation to prevent complicated map/reduce calls - value.y = 0; - } - }); + const timeseriesPerSubtype = bucketsByDate.reduce((prev, bucket) => { + const formattedValues = formatBucket(bucket); + const time = bucket.key; + + const updatedSeries = kpiNames.reduce((p, kpiName) => { + const { name, percentage } = formattedValues.find( + val => val.name === kpiName + ) || { + name: kpiName, + percentage: null + }; + + if (!p[name]) { + p[name] = []; } + return { + ...p, + [name]: p[name].concat({ + x: time, + y: percentage + }) + }; + }, prev); + + const lastValues = Object.values(updatedSeries).map(last); + + // If for a given timestamp, some series have data, but others do not, + // we have to set any null values to 0 to make sure the stacked area chart + // is drawn correctly. + // If we set all values to 0, the chart always displays null values as 0, + // and the chart looks weird. + const hasAnyValues = lastValues.some(value => value.y !== null); + const hasNullValues = lastValues.some(value => value.y === null); + + if (hasAnyValues && hasNullValues) { + Object.values(updatedSeries).forEach(series => { + const value = series[series.length - 1]; + const isEmpty = value.y === null; + if (isEmpty) { + // local mutation to prevent complicated map/reduce calls + value.y = 0; + } + }); + } - return updatedSeries; - }, - {} as Record> - ); + return updatedSeries; + }, {} as Record>); const timeseries = kpis.map(kpi => ({ title: kpi.name, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts index cddc66e52cf701..3b9e80c901fe92 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.test.ts @@ -26,6 +26,7 @@ describe('getAnomalySeries', () => { start: 0, end: 500000, client: { search: clientSpy } as any, + internalClient: { search: clientSpy } as any, config: { get: () => 'myIndex' as any, has: () => true diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts index 5056a100de3ce6..0345b0815679f6 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.test.ts @@ -21,6 +21,7 @@ describe('timeseriesFetcher', () => { start: 1528113600000, end: 1528977600000, client: { search: clientSpy } as any, + internalClient: { search: clientSpy } as any, config: { get: () => 'myIndex' as any, has: () => true diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts index 5d10a4ae27060a..a0149bec728c5c 100644 --- a/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts @@ -61,17 +61,14 @@ export const localUIFilterNames = Object.keys( filtersByName ) as LocalUIFilterName[]; -export const localUIFilters = localUIFilterNames.reduce( - (acc, key) => { - const field = filtersByName[key]; +export const localUIFilters = localUIFilterNames.reduce((acc, key) => { + const field = filtersByName[key]; - return { - ...acc, - [key]: { - ...field, - name: key - } - }; - }, - {} as LocalUIFilterMap -); + return { + ...acc, + [key]: { + ...field, + name: key + } + }; +}, {} as LocalUIFilterMap); diff --git a/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts b/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts index 0458c8e4fedf04..e1cb1774469f2a 100644 --- a/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts +++ b/x-pack/legacy/plugins/apm/server/new-platform/plugin.ts @@ -4,16 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InternalCoreSetup } from 'src/core/server'; +import { Server } from 'hapi'; +import { CoreSetup } from 'src/core/server'; import { makeApmUsageCollector } from '../lib/apm_telemetry'; -import { CoreSetupWithUsageCollector } from '../lib/apm_telemetry/make_apm_usage_collector'; import { createApmAgentConfigurationIndex } from '../lib/settings/agent_configuration/create_agent_config_index'; import { createApmApi } from '../routes/create_apm_api'; +export interface LegacySetup { + server: Server; +} + export class Plugin { - public setup(core: InternalCoreSetup) { - createApmApi().init(core); - createApmAgentConfigurationIndex(core); - makeApmUsageCollector(core as CoreSetupWithUsageCollector); + public setup(core: CoreSetup, __LEGACY: LegacySetup) { + createApmApi().init(core, __LEGACY); + createApmAgentConfigurationIndex(core, __LEGACY); + makeApmUsageCollector(core, __LEGACY); } } diff --git a/x-pack/legacy/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/legacy/plugins/apm/server/routes/create_api/index.test.ts index b0461f5cb3b68d..18fe547a34cf01 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_api/index.test.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_api/index.test.ts @@ -5,23 +5,25 @@ */ import * as t from 'io-ts'; import { createApi } from './index'; -import { InternalCoreSetup } from 'src/core/server'; +import { CoreSetup } from 'src/core/server'; import { Params } from '../typings'; +import { LegacySetup } from '../../new-platform/plugin'; -const getCoreMock = () => +const getCoreMock = () => (({} as unknown) as CoreSetup); + +const getLegacyMock = () => (({ - http: { - server: { - route: jest.fn() - } + server: { + route: jest.fn() } - } as unknown) as InternalCoreSetup & { - http: { server: { route: ReturnType } }; + } as unknown) as LegacySetup & { + server: { route: ReturnType }; }); describe('createApi', () => { it('registers a route with the server', () => { const coreMock = getCoreMock(); + const legacySetupMock = getLegacyMock(); createApi() .add(() => ({ @@ -36,11 +38,19 @@ describe('createApi', () => { }, handler: async () => null })) - .init(coreMock); + .add(() => ({ + path: '/baz', + method: 'PUT', + options: { + tags: ['access:apm', 'access:apm_write'] + }, + handler: async () => null + })) + .init(coreMock, legacySetupMock); - expect(coreMock.http.server.route).toHaveBeenCalledTimes(2); + expect(legacySetupMock.server.route).toHaveBeenCalledTimes(3); - const firstRoute = coreMock.http.server.route.mock.calls[0][0]; + const firstRoute = legacySetupMock.server.route.mock.calls[0][0]; expect(firstRoute).toEqual({ method: 'GET', @@ -51,7 +61,7 @@ describe('createApi', () => { handler: expect.any(Function) }); - const secondRoute = coreMock.http.server.route.mock.calls[1][0]; + const secondRoute = legacySetupMock.server.route.mock.calls[1][0]; expect(secondRoute).toEqual({ method: 'POST', @@ -61,11 +71,23 @@ describe('createApi', () => { path: '/bar', handler: expect.any(Function) }); + + const thirdRoute = legacySetupMock.server.route.mock.calls[2][0]; + + expect(thirdRoute).toEqual({ + method: 'PUT', + options: { + tags: ['access:apm', 'access:apm_write'] + }, + path: '/baz', + handler: expect.any(Function) + }); }); describe('when validating', () => { const initApi = (params: Params) => { const core = getCoreMock(); + const legacySetupMock = getLegacyMock(); const handler = jest.fn(); createApi() .add(() => ({ @@ -73,9 +95,9 @@ describe('createApi', () => { params, handler })) - .init(core); + .init(core, legacySetupMock); - const route = core.http.server.route.mock.calls[0][0]; + const route = legacySetupMock.server.route.mock.calls[0][0]; const routeHandler = route.handler; diff --git a/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts b/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts index f969e4d6024ca5..66f28a9bf6d445 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_api/index.ts @@ -5,7 +5,7 @@ */ import { merge, pick, omit, difference } from 'lodash'; import Boom from 'boom'; -import { InternalCoreSetup } from 'src/core/server'; +import { CoreSetup } from 'src/core/server'; import { Request, ResponseToolkit } from 'hapi'; import * as t from 'io-ts'; import { PathReporter } from 'io-ts/lib/PathReporter'; @@ -18,6 +18,7 @@ import { Params } from '../typings'; import { jsonRt } from '../../../common/runtime_types/json_rt'; +import { LegacySetup } from '../../new-platform/plugin'; const debugRt = t.partial({ _debug: jsonRt.pipe(t.boolean) }); @@ -29,15 +30,14 @@ export function createApi() { factoryFns.push(fn); return this as any; }, - init(core: InternalCoreSetup) { - const { server } = core.http; + init(core: CoreSetup, __LEGACY: LegacySetup) { + const { server } = __LEGACY; factoryFns.forEach(fn => { - const { params = {}, ...route } = fn(core) as Route< - string, - HttpMethod, - Params, - any - >; + const { + params = {}, + options = { tags: ['access:apm'] }, + ...route + } = fn(core, __LEGACY) as Route; const bodyRt = params.body; const fallbackBodyRt = bodyRt || t.null; @@ -54,9 +54,7 @@ export function createApi() { server.route( merge( { - options: { - tags: ['access:apm'] - }, + options, method: 'GET' }, route, @@ -70,41 +68,38 @@ export function createApi() { const parsedParams = (Object.keys(rts) as Array< keyof typeof rts - >).reduce( - (acc, key) => { - const codec = rts[key]; - const value = paramMap[key]; + >).reduce((acc, key) => { + const codec = rts[key]; + const value = paramMap[key]; - const result = codec.decode(value); + const result = codec.decode(value); - if (isLeft(result)) { - throw Boom.badRequest(PathReporter.report(result)[0]); - } + if (isLeft(result)) { + throw Boom.badRequest(PathReporter.report(result)[0]); + } - const strippedKeys = difference( - Object.keys(value || {}), - Object.keys(result.right || {}) - ); + const strippedKeys = difference( + Object.keys(value || {}), + Object.keys(result.right || {}) + ); - if (strippedKeys.length) { - throw Boom.badRequest( - `Unknown keys specified: ${strippedKeys}` - ); - } + if (strippedKeys.length) { + throw Boom.badRequest( + `Unknown keys specified: ${strippedKeys}` + ); + } - // hide _debug from route handlers - const parsedValue = - key === 'query' - ? omit(result.right, '_debug') - : result.right; + // hide _debug from route handlers + const parsedValue = + key === 'query' + ? omit(result.right, '_debug') + : result.right; - return { - ...acc, - [key]: parsedValue - }; - }, - {} as Record - ); + return { + ...acc, + [key]: parsedValue + }; + }, {} as Record); return route.handler( request, diff --git a/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts b/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts index 100df4dc238fe9..92e1284f3ed74e 100644 --- a/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/legacy/plugins/apm/server/routes/index_pattern.ts @@ -9,15 +9,14 @@ import { createRoute } from './create_route'; import { getKueryBarIndexPattern } from '../lib/index_pattern/getKueryBarIndexPattern'; import { setupRequest } from '../lib/helpers/setup_request'; -export const indexPatternRoute = createRoute(core => ({ +export const indexPatternRoute = createRoute((core, { server }) => ({ path: '/api/apm/index_pattern', handler: async () => { - const { server } = core.http; return await getAPMIndexPattern(server); } })); -export const kueryBarIndexPatternRoute = createRoute(core => ({ +export const kueryBarIndexPatternRoute = createRoute(() => ({ path: '/api/apm/kuery_bar_index_pattern', params: { query: t.partial({ @@ -30,9 +29,7 @@ export const kueryBarIndexPatternRoute = createRoute(core => ({ }, handler: async (request, { query }) => { const { processorEvent } = query; - const setup = await setupRequest(request); - return getKueryBarIndexPattern({ request, processorEvent, setup }); } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/services.ts b/x-pack/legacy/plugins/apm/server/routes/services.ts index 85d53925db86e4..4b955c7a6e981f 100644 --- a/x-pack/legacy/plugins/apm/server/routes/services.ts +++ b/x-pack/legacy/plugins/apm/server/routes/services.ts @@ -6,7 +6,10 @@ import * as t from 'io-ts'; import { AgentName } from '../../typings/es_schemas/ui/fields/Agent'; -import { createApmTelementry, storeApmTelemetry } from '../lib/apm_telemetry'; +import { + createApmTelementry, + storeApmServicesTelemetry +} from '../lib/apm_telemetry'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServices } from '../lib/services/get_services'; @@ -16,7 +19,7 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceMap } from '../lib/services/map'; -export const servicesRoute = createRoute(core => ({ +export const servicesRoute = createRoute((core, { server }) => ({ path: '/api/apm/services', params: { query: t.intersection([uiFiltersRt, rangeRt]) @@ -24,14 +27,13 @@ export const servicesRoute = createRoute(core => ({ handler: async req => { const setup = await setupRequest(req); const services = await getServices(setup); - const { server } = core.http; // Store telemetry data derived from services const agentNames = services.items.map( ({ agentName }) => agentName as AgentName ); const apmTelemetry = createApmTelementry(agentNames); - storeApmTelemetry(server, apmTelemetry); + storeApmServicesTelemetry(server, apmTelemetry); return services; } diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts index d25ad949d6ddef..2867cef28d952b 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts @@ -31,6 +31,9 @@ export const agentConfigurationRoute = createRoute(core => ({ export const deleteAgentConfigurationRoute = createRoute(() => ({ method: 'DELETE', path: '/api/apm/settings/agent-configuration/{configurationId}', + options: { + tags: ['access:apm', 'access:apm_write'] + }, params: { path: t.type({ configurationId: t.string @@ -108,6 +111,9 @@ export const createAgentConfigurationRoute = createRoute(() => ({ params: { body: agentPayloadRt }, + options: { + tags: ['access:apm', 'access:apm_write'] + }, handler: async (req, { body }) => { const setup = await setupRequest(req); return await createOrUpdateConfiguration({ configuration: body, setup }); @@ -117,6 +123,9 @@ export const createAgentConfigurationRoute = createRoute(() => ({ export const updateAgentConfigurationRoute = createRoute(() => ({ method: 'PUT', path: '/api/apm/settings/agent-configuration/{configurationId}', + options: { + tags: ['access:apm', 'access:apm_write'] + }, params: { path: t.type({ configurationId: t.string diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts index 3c82a35ec79033..4afcf135a1a76d 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts @@ -5,7 +5,6 @@ */ import * as t from 'io-ts'; -import { setupRequest } from '../../lib/helpers/setup_request'; import { createRoute } from '../create_route'; import { getApmIndices, @@ -14,28 +13,33 @@ import { import { saveApmIndices } from '../../lib/settings/apm_indices/save_apm_indices'; // get list of apm indices and values -export const apmIndexSettingsRoute = createRoute(core => ({ +export const apmIndexSettingsRoute = createRoute((core, { server }) => ({ method: 'GET', path: '/api/apm/settings/apm-index-settings', handler: async req => { - const { server } = core.http; - const setup = await setupRequest(req); - return await getApmIndexSettings({ setup, server }); + const config = server.config(); + const savedObjectsClient = req.server.savedObjects.getScopedSavedObjectsClient( + req + ); + return await getApmIndexSettings({ config, savedObjectsClient }); } })); // get apm indices configuration object -export const apmIndicesRoute = createRoute(core => ({ +export const apmIndicesRoute = createRoute((core, { server }) => ({ method: 'GET', path: '/api/apm/settings/apm-indices', handler: async req => { - const { server } = core.http; - return await getApmIndices(server); + const config = server.config(); + const savedObjectsClient = req.server.savedObjects.getScopedSavedObjectsClient( + req + ); + return await getApmIndices({ config, savedObjectsClient }); } })); // save ui indices -export const saveApmIndicesRoute = createRoute(core => ({ +export const saveApmIndicesRoute = createRoute(() => ({ method: 'POST', path: '/api/apm/settings/apm-indices/save', params: { @@ -50,7 +54,9 @@ export const saveApmIndicesRoute = createRoute(core => ({ }) }, handler: async (req, { body }) => { - const { server } = core.http; - return await saveApmIndices(server, body); + const savedObjectsClient = req.server.savedObjects.getScopedSavedObjectsClient( + req + ); + return await saveApmIndices(savedObjectsClient, body); } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts b/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts index cde9fd1dd4ca99..0b5c29fc298578 100644 --- a/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/legacy/plugins/apm/server/routes/transaction_groups.ts @@ -150,14 +150,20 @@ export const transactionGroupsAvgDurationByCountry = createRoute(() => ({ path: t.type({ serviceName: t.string }), - query: t.intersection([uiFiltersRt, rangeRt]) + query: t.intersection([ + uiFiltersRt, + rangeRt, + t.partial({ transactionName: t.string }) + ]) }, handler: async (req, { path, query }) => { const setup = await setupRequest(req); const { serviceName } = path; + const { transactionName } = query; return getTransactionAvgDurationByCountry({ serviceName, + transactionName, setup }); } diff --git a/x-pack/legacy/plugins/apm/server/routes/typings.ts b/x-pack/legacy/plugins/apm/server/routes/typings.ts index a0ddffe044c151..77d96d3677494c 100644 --- a/x-pack/legacy/plugins/apm/server/routes/typings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/typings.ts @@ -6,9 +6,10 @@ import t from 'io-ts'; import { Request, ResponseToolkit } from 'hapi'; -import { InternalCoreSetup } from 'src/core/server'; +import { CoreSetup } from 'src/core/server'; import { PickByValue, Optional } from 'utility-types'; import { FetchOptions } from '../../public/services/rest/callApi'; +import { LegacySetup } from '../new-platform/plugin'; export interface Params { query?: t.HasProps; @@ -33,6 +34,9 @@ export interface Route< path: TPath; method?: TMethod; params?: TParams; + options?: { + tags: Array<'access:apm' | 'access:apm_write'>; + }; handler: ( req: Request, params: DecodeParams, @@ -45,7 +49,10 @@ export type RouteFactoryFn< TMethod extends HttpMethod | undefined, TParams extends Params, TReturn -> = (core: InternalCoreSetup) => Route; +> = ( + core: CoreSetup, + __LEGACY: LegacySetup +) => Route; export interface RouteState { [key: string]: { @@ -76,7 +83,7 @@ export interface ServerAPI { }; } >; - init: (core: InternalCoreSetup) => void; + init: (core: CoreSetup, __LEGACY: LegacySetup) => void; } // without this, TS does not recognize possible existence of `params` in `options` below @@ -88,7 +95,9 @@ type GetOptionalParamKeys = keyof PickByValue< { [key in keyof TParams]: TParams[key] extends t.PartialType ? false - : (TParams[key] extends t.Any ? true : false); + : TParams[key] extends t.Any + ? true + : false; }, false >; diff --git a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts index 9d36946d29cf65..36508e53acce76 100644 --- a/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts +++ b/x-pack/legacy/plugins/apm/server/routes/ui_filters.ts @@ -45,9 +45,11 @@ export const uiFiltersEnvironmentsRoute = createRoute(() => ({ const filterNamesRt = t.type({ filterNames: jsonRt.pipe( t.array( - t.keyof(Object.fromEntries( - localUIFilterNames.map(filterName => [filterName, null]) - ) as Record) + t.keyof( + Object.fromEntries( + localUIFilterNames.map(filterName => [filterName, null]) + ) as Record + ) ) ) }); diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch/aggregations.ts index 9f17b0197a5b21..b694e9526e2b88 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch/aggregations.ts @@ -202,22 +202,19 @@ interface AggregationResponsePart< TDocument > > - : (TAggregationOptionsMap extends { + : TAggregationOptionsMap extends { filters: { filters: Record; }; } - ? { - buckets: { - [key in keyof TAggregationOptionsMap['filters']['filters']]: { - doc_count: number; - } & AggregationResponseMap< - TAggregationOptionsMap['aggs'], - TDocument - >; - }; - } - : never); + ? { + buckets: { + [key in keyof TAggregationOptionsMap['filters']['filters']]: { + doc_count: number; + } & AggregationResponseMap; + }; + } + : never; sampler: { doc_count: number; } & AggregationResponseMap; diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch/index.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch/index.ts index 56cd0ff23a3fb5..eff39838bd957e 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch/index.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch/index.ts @@ -36,8 +36,7 @@ export type ESSearchResponse< TDocument >; } - : {}) & - ({ + : {}) & { hits: Omit['hits'], 'total'> & (TOptions['restTotalHitsAsInt'] extends true ? { @@ -49,7 +48,7 @@ export type ESSearchResponse< relation: 'eq' | 'gte'; }; }); - }); + }; export interface ESFilter { [key: string]: { diff --git a/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts b/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts index 92137bdf5cc6e6..8972084018d986 100644 --- a/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts +++ b/x-pack/legacy/plugins/beats_management/common/config_block_validation.ts @@ -27,20 +27,17 @@ export const validateConfigurationBlocks = (configurationBlocks: ConfigurationBl ); } - const interfaceConfig = blockSchema.configs.reduce( - (props, config) => { - if (config.options) { - props[config.id] = t.keyof(Object.fromEntries( - config.options.map(opt => [opt.value, null]) - ) as Record); - } else if (config.validation) { - props[config.id] = validationMap[config.validation]; - } + const interfaceConfig = blockSchema.configs.reduce((props, config) => { + if (config.options) { + props[config.id] = t.keyof( + Object.fromEntries(config.options.map(opt => [opt.value, null])) as Record + ); + } else if (config.validation) { + props[config.id] = validationMap[config.validation]; + } - return props; - }, - {} as t.Props - ); + return props; + }, {} as t.Props); const runtimeInterface = createConfigurationBlockInterface( t.literal(blockSchema.id), diff --git a/x-pack/legacy/plugins/beats_management/common/domain_types.ts b/x-pack/legacy/plugins/beats_management/common/domain_types.ts index 0d5c67d09da8aa..bc77abc9815be8 100644 --- a/x-pack/legacy/plugins/beats_management/common/domain_types.ts +++ b/x-pack/legacy/plugins/beats_management/common/domain_types.ts @@ -13,9 +13,9 @@ export const OutputTypesArray = ['elasticsearch', 'logstash', 'kafka', 'redis']; // We can also pass in optional params to create spacific runtime checks that // can be used to validate blocs on the API and UI export const createConfigurationBlockInterface = ( - configType: t.LiteralType | t.KeyofC> = t.keyof(Object.fromEntries( - configBlockSchemas.map(s => [s.id, null]) - ) as Record), + configType: t.LiteralType | t.KeyofC> = t.keyof( + Object.fromEntries(configBlockSchemas.map(s => [s.id, null])) as Record + ), beatConfigInterface: t.Mixed = t.Dictionary ) => t.interface( diff --git a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx index 479be32ce1e127..3ac2ff72c0116d 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/autocomplete_field/index.tsx @@ -265,13 +265,11 @@ const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ selectedIndex: null, }); -const FixedEuiFieldSearch: React.SFC< - React.InputHTMLAttributes & - EuiFieldSearchProps & { - inputRef?: (element: HTMLInputElement | null) => void; - onSearch: (value: string) => void; - } -> = EuiFieldSearch as any; +const FixedEuiFieldSearch: React.SFC & + EuiFieldSearchProps & { + inputRef?: (element: HTMLInputElement | null) => void; + onSearch: (value: string) => void; + }> = EuiFieldSearch as any; const AutocompleteContainer = styled.div` position: relative; diff --git a/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx b/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx index 8736663dd08af8..f15e08c2ca230a 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx @@ -53,16 +53,13 @@ export class BreadcrumbProvider extends Component { - if (crumbStorageItem.parents) { - crumbs = crumbs.concat(crumbStorageItem.parents); - } - crumbs.push(crumbStorageItem.breadcrumb); - return crumbs; - }, - [] as Breadcrumb[] - ), + breadcrumbs: breadcrumbs.reduce((crumbs, crumbStorageItem) => { + if (crumbStorageItem.parents) { + crumbs = crumbs.concat(crumbStorageItem.parents); + } + crumbs.push(crumbStorageItem.breadcrumb); + return crumbs; + }, [] as Breadcrumb[]), addCrumb: this.addCrumb, removeCrumb: this.removeCrumb, }; diff --git a/x-pack/legacy/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/legacy/plugins/beats_management/public/components/table/table_type_configs.tsx index a93000a3e80f04..6f03f884563e16 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/table/table_type_configs.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -246,7 +246,10 @@ export const BeatsTableType: TableType = { name: i18n.translate('xpack.beatsManagement.beatsTable.typeLabel', { defaultMessage: 'Type', }), - options: uniq(data.map(({ type }: { type: any }) => ({ value: type })), 'value'), + options: uniq( + data.map(({ type }: { type: any }) => ({ value: type })), + 'value' + ), }, ], }), diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts index 4a7f768bd47408..b2e11461007fd2 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts @@ -26,9 +26,9 @@ export class RestBeatsAdapter implements CMBeatsAdapter { public async getBeatWithToken(enrollmentToken: string): Promise { try { - return (await this.REST.get>( - `/api/beats/agent/unknown/${enrollmentToken}` - )).item; + return ( + await this.REST.get>(`/api/beats/agent/unknown/${enrollmentToken}`) + ).item; } catch (e) { return null; } @@ -59,16 +59,20 @@ export class RestBeatsAdapter implements CMBeatsAdapter { public async removeTagsFromBeats( removals: BeatsTagAssignment[] ): Promise { - return (await this.REST.post(`/api/beats/agents_tags/removals`, { - removals, - })).results; + return ( + await this.REST.post(`/api/beats/agents_tags/removals`, { + removals, + }) + ).results; } public async assignTagsToBeats( assignments: BeatsTagAssignment[] ): Promise { - return (await this.REST.post(`/api/beats/agents_tags/assignments`, { - assignments, - })).results; + return ( + await this.REST.post(`/api/beats/agents_tags/assignments`, { + assignments, + }) + ).results; } } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts index 31c55b92721936..190c9e265463dc 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts @@ -20,9 +20,9 @@ export class RestTagsAdapter implements CMTagsAdapter { public async getTagsWithIds(tagIds: string[]): Promise { try { - return (await this.REST.get>( - `/api/beats/tags/${uniq(tagIds).join(',')}` - )).items; + return ( + await this.REST.get>(`/api/beats/tags/${uniq(tagIds).join(',')}`) + ).items; } catch (e) { return []; } @@ -37,9 +37,9 @@ export class RestTagsAdapter implements CMTagsAdapter { } public async delete(tagIds: string[]): Promise { - return (await this.REST.delete( - `/api/beats/tags/${uniq(tagIds).join(',')}` - )).success; + return ( + await this.REST.delete(`/api/beats/tags/${uniq(tagIds).join(',')}`) + ).success; } public async upsertTag(tag: BeatTag): Promise { @@ -53,9 +53,11 @@ export class RestTagsAdapter implements CMTagsAdapter { public async getAssignable(beats: CMBeat[]) { try { - return (await this.REST.get>( - `/api/beats/tags/assignable/${beats.map(beat => beat.id).join(',')}` - )).items; + return ( + await this.REST.get>( + `/api/beats/tags/assignable/${beats.map(beat => beat.id).join(',')}` + ) + ).items; } catch (e) { return []; } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts b/x-pack/legacy/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts index 8274764e759aba..92cfcc935ad9b0 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts @@ -12,12 +12,11 @@ export class RestTokensAdapter implements CMTokensAdapter { constructor(private readonly REST: RestAPIAdapter) {} public async createEnrollmentTokens(numTokens: number = 1): Promise { - const results = (await this.REST.post>( - '/api/beats/enrollment_tokens', - { + const results = ( + await this.REST.post>('/api/beats/enrollment_tokens', { num_tokens: numTokens, - } - )).results; + }) + ).results; return results.map(result => result.item); } } diff --git a/x-pack/legacy/plugins/beats_management/public/lib/compose/scripts.ts b/x-pack/legacy/plugins/beats_management/public/lib/compose/scripts.ts index 4718abc1b71537..83129384a77dfa 100644 --- a/x-pack/legacy/plugins/beats_management/public/lib/compose/scripts.ts +++ b/x-pack/legacy/plugins/beats_management/public/lib/compose/scripts.ts @@ -22,7 +22,11 @@ import { FrontendLibs } from '../types'; export function compose(basePath: string): FrontendLibs { const api = new NodeAxiosAPIAdapter('elastic', 'changeme', basePath); - const esAdapter = new MemoryElasticsearchAdapter(() => true, () => '', []); + const esAdapter = new MemoryElasticsearchAdapter( + () => true, + () => '', + [] + ); const elasticsearchLib = new ElasticsearchLib(esAdapter); const configBlocks = new ConfigBlocksLib( new RestConfigBlocksAdapter(api), diff --git a/x-pack/legacy/plugins/beats_management/server/lib/tags.ts b/x-pack/legacy/plugins/beats_management/server/lib/tags.ts index cfcc2c4eda3979..df9c65034115bb 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/tags.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/tags.ts @@ -40,15 +40,12 @@ export class CMTagsDomain { public async getNonConflictingTags(user: FrameworkUser, existingTagIds: string[]) { const tags = await this.adapter.getTagsWithIds(user, existingTagIds); const existingUniqueBlockTypes = uniq( - tags.reduce( - (existingUniqueTypes, tag) => { - if (tag.hasConfigurationBlocksTypes) { - existingUniqueTypes = existingUniqueTypes.concat(tag.hasConfigurationBlocksTypes); - } - return existingUniqueTypes; - }, - [] as string[] - ) + tags.reduce((existingUniqueTypes, tag) => { + if (tag.hasConfigurationBlocksTypes) { + existingUniqueTypes = existingUniqueTypes.concat(tag.hasConfigurationBlocksTypes); + } + return existingUniqueTypes; + }, [] as string[]) ).filter(type => UNIQUENESS_ENFORCING_TYPES.includes(type)); const safeTags = await this.adapter.getWithoutConfigTypes(user, existingUniqueBlockTypes); diff --git a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts index bb0376ab87d290..156304443431d1 100644 --- a/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts +++ b/x-pack/legacy/plugins/beats_management/server/rest_api/__tests__/beats_assignments.test.ts @@ -73,7 +73,10 @@ describe('assign_tags_to_beats', () => { authorization: 'loggedin', }, payload: { - assignments: [{ beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'development' }], + assignments: [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'development' }, + ], }, }); @@ -115,7 +118,10 @@ describe('assign_tags_to_beats', () => { authorization: 'loggedin', }, payload: { - assignments: [{ beatId: 'bar', tag: 'development' }, { beatId: 'bar', tag: 'production' }], + assignments: [ + { beatId: 'bar', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ], }, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/location.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/location.ts index c80c37bb1ed560..e592c6d22ef737 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/location.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/browser/location.ts @@ -32,7 +32,10 @@ export function location(): ExpressionFunction<'location', null, {}, Promise { const fn = functionWrapper(csv); const expected = { type: 'datatable', - columns: [{ name: 'name', type: 'string' }, { name: 'number', type: 'string' }], + columns: [ + { name: 'name', type: 'string' }, + { name: 'number', type: 'string' }, + ], rows: [ { name: 'one', number: '1' }, { name: 'two', number: '2' }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/get_flot_axis_config.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/get_flot_axis_config.test.js index bfc3212fa234ca..a5b65e6bdc62b9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/get_flot_axis_config.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/get_flot_axis_config.test.js @@ -84,10 +84,16 @@ describe('getFlotAxisConfig', () => { describe('ticks', () => { it('adds a tick mark mapping for string columns', () => { let result = getFlotAxisConfig('x', true, { columns, ticks }); - expect(result.ticks).toEqual([[2, 'product1'], [1, 'product2']]); + expect(result.ticks).toEqual([ + [2, 'product1'], + [1, 'product2'], + ]); result = getFlotAxisConfig('x', xAxisConfig, { columns, ticks }); - expect(result.ticks).toEqual([[2, 'product1'], [1, 'product2']]); + expect(result.ticks).toEqual([ + [2, 'product1'], + [1, 'product2'], + ]); }); }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/get_tick_hash.test.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/get_tick_hash.test.js index f44d37abe166f9..74c13be3abd7b9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/get_tick_hash.test.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/get_tick_hash.test.js @@ -30,7 +30,12 @@ describe('getTickHash', () => { x: { type: 'number', role: 'dimension', expression: 'id' }, y: { type: 'boolean', role: 'dimension', expression: 'running' }, }; - const rows = [{ x: 1, y: true }, { x: 2, y: true }, { x: 1, y: false }, { x: 2, y: false }]; + const rows = [ + { x: 1, y: true }, + { x: 2, y: true }, + { x: 1, y: false }, + { x: 2, y: false }, + ]; expect(getTickHash(columns, rows)).toEqual({ x: { hash: {}, counter: 0 }, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/math.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/math.ts index c180b6186ee921..718f2d8b11e1a9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/math.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/math.ts @@ -44,7 +44,10 @@ export function math(): ExpressionFunction<'math', Context, Arguments, number> { } const mathContext = isDatatable(context) - ? pivotObjectArray(context.rows, context.columns.map(col => col.name)) + ? pivotObjectArray( + context.rows, + context.columns.map(col => col.name) + ) : { value: context }; try { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts index 5889864df3c1df..5d5c3c1a735a02 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts @@ -187,7 +187,10 @@ export function pointseries(): ExpressionFunction< // Then compute that 1 value for each measure Object.values(measureKeys).forEach(valueRows => { const subtable = { type: 'datatable', columns: context.columns, rows: valueRows }; - const subScope = pivotObjectArray(subtable.rows, subtable.columns.map(col => col.name)); + const subScope = pivotObjectArray( + subtable.rows, + subtable.columns.map(col => col.name) + ); const measureValues = measureNames.map(measure => { try { const ev = evaluate(args[measure], subScope); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/pie/plugins/pie.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/pie/plugins/pie.js index 9229def146f8b0..042cbe83fb10fc 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/pie/plugins/pie.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/pie/plugins/pie.js @@ -683,7 +683,14 @@ function init(plot) { const p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5); const p5X = radius * Math.cos(s.startAngle + s.angle); const p5Y = radius * Math.sin(s.startAngle + s.angle); - const arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]]; + const arrPoly = [ + [0, 0], + [p1X, p1Y], + [p2X, p2Y], + [p3X, p3Y], + [p4X, p4Y], + [p5X, p5Y], + ]; const arrPoint = [x, y]; // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.examples.storyshot b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.examples.storyshot index bf68d217f18abb..0b9358714e71c6 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/__examples__/__snapshots__/simple_template.examples.storyshot @@ -13,23 +13,26 @@ exports[`Storyshots arguments/AxisConfig simple 1`] = `
- - - - + className="euiSwitch__body" + > + + + +
`; @@ -47,23 +50,26 @@ exports[`Storyshots arguments/AxisConfig/components simple template 1`] = `
- - - - + className="euiSwitch__body" + > + + + +
`; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.tsx index eb32881bc1f6d0..068854866dc1bd 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/axis_config/simple_template.tsx @@ -19,6 +19,8 @@ export const SimpleTemplate: FunctionComponent = ({ onValueChange, argVal compressed checked={Boolean(argValue)} onChange={() => onValueChange(!Boolean(argValue))} + showLabel={false} + label="" /> ); }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js index 6a7a89231f2509..4bb68973e80eab 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/pie.js @@ -14,7 +14,10 @@ const { Pie: strings } = ViewStrings; export const pie = () => ({ name: 'pie', displayName: strings.getDisplayName(), - modelArgs: [['color', { label: 'Slice Labels' }], ['size', { label: 'Slice Angles' }]], + modelArgs: [ + ['color', { label: 'Slice Labels' }], + ['size', { label: 'Slice Angles' }], + ], args: [ { name: 'palette', diff --git a/x-pack/legacy/plugins/canvas/public/apps/export/export/index.js b/x-pack/legacy/plugins/canvas/public/apps/export/export/index.js index 16ca644160c0a3..d40c5f787e44f8 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/export/export/index.js +++ b/x-pack/legacy/plugins/canvas/public/apps/export/export/index.js @@ -25,9 +25,6 @@ const mapDispatchToProps = dispatch => ({ const branches = [branch(({ workpad }) => workpad == null, renderComponent(LoadWorkpad))]; export const ExportApp = compose( - connect( - mapStateToProps, - mapDispatchToProps - ), + connect(mapStateToProps, mapDispatchToProps), ...branches )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/apps/home/home_app/index.js b/x-pack/legacy/plugins/canvas/public/apps/home/home_app/index.js index 8ecde2cc721e39..f26b3510c76826 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/home/home_app/index.js +++ b/x-pack/legacy/plugins/canvas/public/apps/home/home_app/index.js @@ -14,7 +14,4 @@ const mapDispatchToProps = dispatch => ({ }, }); -export const HomeApp = connect( - null, - mapDispatchToProps -)(Component); +export const HomeApp = connect(null, mapDispatchToProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/index.js b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/index.js index 8789577c92f63e..f0a5325baf3c04 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/index.js +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/index.js @@ -35,10 +35,7 @@ const mapDispatchToProps = dispatch => ({ const branches = [branch(({ workpad }) => workpad == null, renderComponent(LoadWorkpad))]; export const WorkpadApp = compose( - connect( - mapStateToProps, - mapDispatchToProps - ), + connect(mapStateToProps, mapDispatchToProps), ...branches, withElementsLoadedTelemetry )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/app/index.js b/x-pack/legacy/plugins/canvas/public/components/app/index.js index b3aa9c393096b3..65b811fe681348 100644 --- a/x-pack/legacy/plugins/canvas/public/components/app/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/app/index.js @@ -97,11 +97,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { }; export const App = compose( - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ), + connect(mapStateToProps, mapDispatchToProps, mergeProps), withProps(() => ({ onRouteChange: trackRouteChange, })) diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.js b/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.js index 69c4463e7d210b..6c05eec0c3c09d 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.js @@ -89,10 +89,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { }; export const AssetManager = compose( - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ), + connect(mapStateToProps, mapDispatchToProps, mergeProps), withProps({ onAssetCopy: asset => notify.success(`Copied '${asset.id}' to clipboard`) }) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/datasource/index.js b/x-pack/legacy/plugins/canvas/public/components/datasource/index.js index 2c7820df26ca91..7fb73b1672eb1b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/datasource/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/datasource/index.js @@ -82,11 +82,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { }; export const Datasource = compose( - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ), + connect(mapStateToProps, mapDispatchToProps, mergeProps), withState('stateArgs', 'updateArgs', ({ args }) => args), withState('selecting', 'setSelecting', false), withState('previewing', 'setPreviewing', false), diff --git a/x-pack/legacy/plugins/canvas/public/components/element_content/element_content.js b/x-pack/legacy/plugins/canvas/public/components/element_content/element_content.js index e85ce8c10da644..89c0b5b21c581a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_content/element_content.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_content/element_content.js @@ -21,9 +21,12 @@ import { InvalidElementType } from './invalid_element_type'; */ const branches = [ // no renderable or renderable config value, render loading - branch(({ renderable, state }) => { - return !state || !renderable; - }, renderComponent(({ backgroundColor }) => )), + branch( + ({ renderable, state }) => { + return !state || !renderable; + }, + renderComponent(({ backgroundColor }) => ) + ), // renderable is available, but no matching element is found, render invalid branch(({ renderable, renderFunction }) => { diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js b/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js index 26a0d926553629..dabf06a24aeb64 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js @@ -116,7 +116,10 @@ export class ElementTypes extends Component { }; _sortElements = elements => - sortBy(map(elements, (element, name) => ({ name, ...element })), 'displayName'); + sortBy( + map(elements, (element, name) => ({ name, ...element })), + 'displayName' + ); render() { const { diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js index 20d19d2fcbd828..8faaf278a07de3 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js @@ -95,11 +95,7 @@ export const ElementTypes = compose( withState('customElements', 'setCustomElements', []), withState('filterTags', 'setFilterTags', []), withProps(() => ({ elements: elementsRegistry.toJS() })), - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ) + connect(mapStateToProps, mapDispatchToProps, mergeProps) )(Component); ElementTypes.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx index 8a040c0c90c83e..612406c30f88ee 100644 --- a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/index.tsx @@ -105,9 +105,5 @@ export class EmbeddableFlyoutPortal extends React.Component { } export const AddEmbeddablePanel = compose void }>( - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ) + connect(mapStateToProps, mapDispatchToProps, mergeProps) )(EmbeddableFlyoutPortal); diff --git a/x-pack/legacy/plugins/canvas/public/components/expression/index.js b/x-pack/legacy/plugins/canvas/public/components/expression/index.js index 6ae4ef984264fd..4bcdb547186d15 100644 --- a/x-pack/legacy/plugins/canvas/public/components/expression/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/expression/index.js @@ -70,11 +70,7 @@ const expressionLifecycle = lifecycle({ }); export const Expression = compose( - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ), + connect(mapStateToProps, mapDispatchToProps, mergeProps), withState('functionDefinitions', 'setFunctionDefinitions', []), withState('formState', 'setFormState', ({ expression }) => ({ expression, diff --git a/x-pack/legacy/plugins/canvas/public/components/function_form/index.js b/x-pack/legacy/plugins/canvas/public/components/function_form/index.js index 32e9cbbd1d91d6..774214cf68cecd 100644 --- a/x-pack/legacy/plugins/canvas/public/components/function_form/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/function_form/index.js @@ -105,11 +105,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { }; }; -export const FunctionForm = connect( - mapStateToProps, - mapDispatchToProps, - mergeProps -)(Component); +export const FunctionForm = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); FunctionForm.propTypes = { expressionIndex: PropTypes.number, diff --git a/x-pack/legacy/plugins/canvas/public/components/page_config/index.js b/x-pack/legacy/plugins/canvas/public/components/page_config/index.js index a0692584d49864..a51e6b4b5d987c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/page_config/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/page_config/index.js @@ -43,8 +43,4 @@ const mergeProps = (stateProps, dispatchProps) => { }; }; -export const PageConfig = connect( - mapStateToProps, - mapDispatchToProps, - mergeProps -)(Component); +export const PageConfig = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/page_manager/index.js b/x-pack/legacy/plugins/canvas/public/components/page_manager/index.js index 06507e4eb9a718..ef5de3feb575c1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/page_manager/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/page_manager/index.js @@ -32,9 +32,6 @@ const mapDispatchToProps = dispatch => ({ }); export const PageManager = compose( - connect( - mapStateToProps, - mapDispatchToProps - ), + connect(mapStateToProps, mapDispatchToProps), withState('deleteId', 'setDeleteId', null) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/router/index.js b/x-pack/legacy/plugins/canvas/public/components/router/index.js index 51d856b0fc7a6a..430d6a53436624 100644 --- a/x-pack/legacy/plugins/canvas/public/components/router/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/router/index.js @@ -20,7 +20,4 @@ const mapDispatchToState = { setRefreshInterval, }; -export const Router = connect( - null, - mapDispatchToState -)(Component); +export const Router = connect(null, mapDispatchToState)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_content.js b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_content.js index 825ea8a4f24941..9de5f81b440ba8 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_content.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_content.js @@ -93,10 +93,6 @@ const branches = [ ]; export const SidebarContent = compose( - connect( - mapStateToProps, - null, - mergeProps - ), + connect(mapStateToProps, null, mergeProps), ...branches )(GlobalConfig); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/index.js b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/index.js index f60aad1fa3e72a..ac282962afb54a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/index.js @@ -56,10 +56,7 @@ const mapDispatchToProps = dispatch => ({ }); export const SidebarHeader = compose( - connect( - mapStateToProps, - mapDispatchToProps - ), + connect(mapStateToProps, mapDispatchToProps), withHandlers(basicHandlerCreators), withHandlers(clipboardHandlerCreators), withHandlers(layerHandlerCreators), diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad/index.js index 723b30aac37fa8..fca663f8532d86 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad/index.js @@ -72,11 +72,7 @@ export const Workpad = compose( router: PropTypes.object, }), withState('grid', 'setGrid', false), - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ), + connect(mapStateToProps, mapDispatchToProps, mergeProps), withState('transition', 'setTransition', null), withState('prevSelectedPageNumber', 'setPrevSelectedPageNumber', 0), withProps(({ selectedPageNumber, prevSelectedPageNumber, transition }) => { diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_color_picker/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_color_picker/index.ts index 2b966e7bd6bb8b..c6dddab3b5dd16 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_color_picker/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_color_picker/index.ts @@ -21,7 +21,4 @@ const mapDispatchToProps = { onRemoveColor: removeColor, }; -export const WorkpadColorPicker = connect( - mapStateToProps, - mapDispatchToProps -)(Component); +export const WorkpadColorPicker = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_config/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_config/index.js index b13740f177a23f..aa3bbdce978630 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_config/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_config/index.js @@ -31,7 +31,4 @@ const mapDispatchToProps = { setWorkpadCSS: css => setWorkpadCSS(css), }; -export const WorkpadConfig = connect( - mapStateToProps, - mapDispatchToProps -)(Component); +export const WorkpadConfig = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts index dc4c4461c59218..316a49c85c09da 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts @@ -32,7 +32,4 @@ const mapDispatchToProps = { setAutoplayInterval, }; -export const ControlSettings = connect( - mapStateToProps, - mapDispatchToProps -)(Component); +export const ControlSettings = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js index cf2e80cee03bf5..fc0bd4c74ba245 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js @@ -68,11 +68,7 @@ export const FullscreenControl = compose( getContext({ router: PropTypes.object, }), - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ), + connect(mapStateToProps, mapDispatchToProps, mergeProps), withState('transition', 'setTransition', null), withState('prevSelectedPageNumber', 'setPrevSelectedPageNumber', 0), withProps(({ selectedPageNumber, prevSelectedPageNumber, transition }) => { diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/index.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/index.tsx index 6d3c9a0640ba2c..d2fece567a8ad9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/index.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/index.tsx @@ -46,8 +46,4 @@ const mergeProps = ( toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), }); -export const WorkpadHeader = connect( - mapStateToProps, - mapDispatchToProps, - mergeProps -)(Component); +export const WorkpadHeader = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/index.ts index 718fec9f59f778..53c053811a273d 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/index.ts @@ -20,7 +20,4 @@ const mapDispatchToProps = { doRefresh: fetchAllRenderables, }; -export const RefreshControl = connect( - mapStateToProps, - mapDispatchToProps -)(Component); +export const RefreshControl = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx index 406d6b54729b90..b22a9d35aa7935 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx @@ -33,9 +33,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ }); export const WorkpadZoom = compose( - connect( - mapStateToProps, - mapDispatchToProps - ), + connect(mapStateToProps, mapDispatchToProps), withHandlers(zoomHandlerCreators) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/integration_utils.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/integration_utils.js index 34c3d446c5ca7d..731656dd4e0950 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_page/integration_utils.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/integration_utils.js @@ -135,7 +135,12 @@ export const globalStateUpdater = (dispatch, globalState) => state => { if (elementsToRemove.length) { // remove elements for groups that were ungrouped - dispatch(removeElements(elementsToRemove.map(e => e.id), page)); + dispatch( + removeElements( + elementsToRemove.map(e => e.id), + page + ) + ); } // set the selected element on the global store, if one element is selected diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js index 56ea35a6887ec6..4ee3a65172a2ed 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js @@ -58,6 +58,21 @@ const configuration = { tooltipZ: 1100, }; +// Polyfill for browsers (IE11) that don't have element.closest +// From: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest +function closest(s) { + let el = this; + const matchFn = el.matches ? 'matches' : 'msMatchesSelector'; + + do { + if (el[matchFn](s)) { + return el; + } + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; +} + const componentLayoutState = ({ aeroStore, setAeroStore, @@ -146,11 +161,7 @@ const mergeProps = ( }); export const InteractivePage = compose( - connect( - mapStateToProps, - mapDispatchToProps, - mergeProps - ), + connect(mapStateToProps, mapDispatchToProps, mergeProps), withState('aeroStore', 'setAeroStore'), withProps(componentLayoutState), withProps(({ aeroStore, updateGlobalState }) => ({ @@ -197,8 +208,15 @@ export const InteractivePage = compose( })), withProps((...props) => ({ ...props, - canDragElement: element => - !element.closest('.embeddable') || element.closest('.embPanel__header'), + canDragElement: element => { + const hasClosest = typeof element.closest === 'function'; + + if (hasClosest) { + return !element.closest('.embeddable') || element.closest('.embPanel__header'); + } else { + return !closest.call(element, '.embeddable') || closest.call(element, '.embPanel__header'); + } + }, })), withHandlers(eventHandlers), // Captures user intent, needs to have reconciled state () => InteractiveComponent diff --git a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/font.js b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/font.js index 893caba1465b8b..46a97f7c15d74f 100644 --- a/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/font.js +++ b/x-pack/legacy/plugins/canvas/public/expression_types/arg_types/font.js @@ -24,7 +24,11 @@ export const FontArgInput = props => { const spec = mapValues(chainArgs, '[0]'); function handleChange(newSpec) { - const newValue = set(argValue, ['chain', 0, 'arguments'], mapValues(newSpec, v => [v])); + const newValue = set( + argValue, + ['chain', 0, 'arguments'], + mapValues(newSpec, v => [v]) + ); return onValueChange(newValue); } diff --git a/x-pack/legacy/plugins/canvas/public/lib/aeroelastic/layout_functions.js b/x-pack/legacy/plugins/canvas/public/lib/aeroelastic/layout_functions.js index 4b99a5ef298d89..d3da1b55539587 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/aeroelastic/layout_functions.js +++ b/x-pack/legacy/plugins/canvas/public/lib/aeroelastic/layout_functions.js @@ -62,17 +62,46 @@ const resizeVertexTuples = [ ]; const connectorVertices = [ - [[-1, -1], [0, -1]], - [[0, -1], [1, -1]], - [[1, -1], [1, 0]], - [[1, 0], [1, 1]], - [[1, 1], [0, 1]], - [[0, 1], [-1, 1]], - [[-1, 1], [-1, 0]], - [[-1, 0], [-1, -1]], + [ + [-1, -1], + [0, -1], + ], + [ + [0, -1], + [1, -1], + ], + [ + [1, -1], + [1, 0], + ], + [ + [1, 0], + [1, 1], + ], + [ + [1, 1], + [0, 1], + ], + [ + [0, 1], + [-1, 1], + ], + [ + [-1, 1], + [-1, 0], + ], + [ + [-1, 0], + [-1, -1], + ], ]; -const cornerVertices = [[-1, -1], [1, -1], [-1, 1], [1, 1]]; +const cornerVertices = [ + [-1, -1], + [1, -1], + [-1, 1], + [1, 1], +]; const resizeMultiplierHorizontal = { left: -1, center: 0, right: 1 }; const resizeMultiplierVertical = { top: -1, center: 0, bottom: 1 }; @@ -91,7 +120,10 @@ const bidirectionalCursors = { '315': 'nwse-resize', }; -const identityAABB = () => [[Infinity, Infinity], [-Infinity, -Infinity]]; +const identityAABB = () => [ + [Infinity, Infinity], + [-Infinity, -Infinity], +]; const extend = ([[xMin, yMin], [xMax, yMax]], [x0, y0], [x1, y1]) => [ [Math.min(xMin, x0, x1), Math.min(yMin, y0, y1)], @@ -547,14 +579,18 @@ export const applyLocalTransforms = (shapes, transformIntents) => { // eslint-disable-next-line const getUpstreamTransforms = (shapes, shape) => shape.parent - ? getUpstreamTransforms(shapes, shapes.find(s => s.id === shape.parent)).concat([ - shape.localTransformMatrix, - ]) + ? getUpstreamTransforms( + shapes, + shapes.find(s => s.id === shape.parent) + ).concat([shape.localTransformMatrix]) : [shape.localTransformMatrix]; const getUpstreams = (shapes, shape) => shape.parent - ? getUpstreams(shapes, shapes.find(s => s.id === shape.parent)).concat([shape]) + ? getUpstreams( + shapes, + shapes.find(s => s.id === shape.parent) + ).concat([shape]) : [shape]; const snappedA = shape => shape.a + (shape.snapResizeVector ? shape.snapResizeVector[0] : 0); @@ -877,7 +913,12 @@ function resizeAnnotation(config, shapes, selectedShapes, shape) { const b = snappedB(properShape); const allowResize = properShape.type !== 'group' || - (config.groupResize && magic(config, properShape, shapes.filter(s => s.type !== 'annotation'))); + (config.groupResize && + magic( + config, + properShape, + shapes.filter(s => s.type !== 'annotation') + )); const resizeVertices = allowResize ? resizeVertexTuples : []; const resizePoints = resizeVertices.map(resizePointAnnotations(config, shape, a, b)); const connectors = connectorVertices.map(resizeEdgeAnnotations(config, shape, a, b)); @@ -1235,7 +1276,10 @@ export const getGrouping = (config, shapes, selectedShapes, groupAction, tuple) return config.groupResize ? { shapes: [ - ...resizeGroup(shapes.filter(s => s.type !== 'annotation'), elements[0]), + ...resizeGroup( + shapes.filter(s => s.type !== 'annotation'), + elements[0] + ), ...shapes.filter(s => s.type === 'annotation'), ], selectedShapes, diff --git a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js index 1005cc60e50ba3..7b7e87b027af53 100644 --- a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js @@ -227,9 +227,12 @@ export const removeElements = createThunk( // todo consider doing the group membership collation in aeroelastic, or the Redux reducer, when adding templates const allElements = getNodes(state, pageId); const allRoots = rootElementIds.map(id => allElements.find(e => id === e.id)).filter(d => d); - const elementIds = subMultitree(e => e.id, e => e.position.parent, allElements, allRoots).map( - e => e.id - ); + const elementIds = subMultitree( + e => e.id, + e => e.position.parent, + allElements, + allRoots + ).map(e => e.id); const shouldRefresh = elementIds.some(elementId => { const element = getNodeById(state, elementId, pageId); diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx index db0aac34336eab..9cf2ddc3a22e3e 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__tests__/app.test.tsx @@ -111,7 +111,7 @@ describe('', () => { wrapper.update(); expect(footer(wrapper).prop('isHidden')).toEqual(false); expect(footer(wrapper).prop('isAutohide')).toEqual(false); - toolbarCheck(wrapper).simulate('change'); + toolbarCheck(wrapper).simulate('click'); expect(footer(wrapper).prop('isAutohide')).toEqual(true); canvas(wrapper).simulate('mouseEnter'); expect(footer(wrapper).prop('isHidden')).toEqual(false); @@ -132,7 +132,7 @@ describe('', () => { .simulate('click'); await tick(20); wrapper.update(); - toolbarCheck(wrapper).simulate('change'); + toolbarCheck(wrapper).simulate('click'); await tick(20); // Simulate the mouse leaving the container diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot index b159e6499ed9fd..1e66e19b3c0e18 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot @@ -25,49 +25,52 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: off,
- - - - - + + + + - -

- - - - - + + + + - -

- - - - - + + + + - -

- - - - - + + + + - -
- - - - - + + + + - -
- - - - - + + + + - -
can navigate Autoplay Settings 1`] = ` data-focus-lock-disabled="disabled" >