diff --git a/.i18nrc.json b/.i18nrc.json index 01065201b9d64..51727ce014f58 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 64309aaae6cc3..f6c0f5bb83fcb 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 0000000000000..dc1e19bab80b2 --- /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 60cac357d1fe0..c500c080a5feb 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 16c8ffe07fc15..31513bda2e879 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 df0b963e2b627..cec307032094e 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 0000000000000..58f407904a576 --- /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 0fd0aca652cf0..0000000000000 --- 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 8c3e874804e08..8ce59d5d9ca78 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 8f624c285b180..a8f6915ea9bb7 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 c9a7c2b8fee92..0000000000000 --- 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 0000000000000..0cbbf29df073a --- /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 75f12b3d94561..9dd20b5899f3a 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 ffcf9e5c6dea2..3a6bc3a5e45da 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 8e9648031f0e2..6b651b310e974 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 28e596f0c09e3..67ebad919ed2a 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 c8a48b3fa46c9..39090fb8f1bbe 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 0e01dc1364d07..ce9a9a2fae691 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 0e236f2737b12..948181f825763 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 e47f6d5c8ac59..ae4a2de9fc75c 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 5025c83a666c8..9f27041175207 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 0000000000000..f44562120c9ee --- /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 04feca7ccc5a8..2b3b6c899e8de 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 da833c71bf93b..478e29696966c 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 cec70ee853bfa..1272a134ef5c4 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/getting-started/tutorial-dashboard.asciidoc b/docs/getting-started/tutorial-dashboard.asciidoc index aab93eb51ca23..87a1e38efcfe0 100644 --- a/docs/getting-started/tutorial-dashboard.asciidoc +++ b/docs/getting-started/tutorial-dashboard.asciidoc @@ -2,11 +2,12 @@ === Add visualizations to a dashboard A dashboard is a collection of visualizations that you can arrange and share. -You'll build a dashboard that contains the visualizations you saved during +You'll build a dashboard that contains the visualizations and map that you saved during this tutorial. . Open *Dashboard*. . On the Dashboard overview page, click *Create new dashboard*. +. Set the time filter to May 18, 2015 to May 20, 2015. . Click *Add* in the menu bar. . Add *Bar Example*, *Map Example*, *Markdown Example*, and *Pie Example*. + @@ -26,12 +27,12 @@ is on the lower right. ==== Inspect the data -Seeing visualizations of your data is great, +Seeing visualizations of your data is great, but sometimes you need to look at the actual data to understand what's really going on. You can inspect the data behind any visualization and view the {es} query used to retrieve it. -. In the dashboard, hover the pointer over the pie chart, and then click the icon in the upper right. +. In the dashboard, hover the pointer over the pie chart, and then click the icon in the upper right. . From the *Options* menu, select *Inspect*. + [role="screenshot"] diff --git a/docs/getting-started/tutorial-visualizing.asciidoc b/docs/getting-started/tutorial-visualizing.asciidoc index 5e61475cf2839..a16343aa4850a 100644 --- a/docs/getting-started/tutorial-visualizing.asciidoc +++ b/docs/getting-started/tutorial-visualizing.asciidoc @@ -3,11 +3,11 @@ In the Visualize application, you can shape your data using a variety of charts, tables, and maps, and more. In this tutorial, you'll create four -visualizations: +visualizations: * <> * <> -* <> +* <> * <> [float] @@ -25,7 +25,7 @@ types in Kibana. image::images/tutorial-visualize-wizard-step-1.png[] . Click *Pie*. -. In *Choose a source*, select the `ba*` index pattern. +. In *Choose a source*, select the `ba*` index pattern. + Initially, the pie contains a single "slice." That's because the default search matched all documents. @@ -76,7 +76,7 @@ in a ring around the balance ranges. [role="screenshot"] image::images/tutorial-visualize-pie-3.png[] -. To save this chart so you can use it later, click *Save* in +. To save this chart so you can use it later, click *Save* in the top menu bar and enter `Pie Example`. [float] @@ -123,14 +123,36 @@ you did at the beginning of the tutorial, when you marked the `play_name` field as `not analyzed`. [float] -[[tutorial-visualize-map]] -=== Coordinate map +[[tutorial-visualize-markdown]] +=== Markdown -Using a coordinate map, you can visualize geographic information in the log file sample data. +Create a Markdown widget to add formatted text to your dashboard. + +. Create a *Markdown* visualization. +. Copy the following text into the text box. ++ +[source,markdown] +# This is a tutorial dashboard! +The Markdown widget uses **markdown** syntax. +> Blockquotes in Markdown use the > character. -. Create a *Coordinate map* and set the search source to `logstash*`. +. Click *Apply changes* image:images/apply-changes-button.png[]. + -You haven't defined any buckets yet, so the visualization is a map of the world. +The Markdown renders in the preview pane. ++ +[role="screenshot"] +image::images/tutorial-visualize-md-2.png[] + +. *Save* this visualization with the name `Markdown Example`. + +[float] +[[tutorial-visualize-map]] +=== Map + +Using <>, you can visualize geographic information in the log file sample data. + +. Click *Maps* in the New Visualization +menu to create a Map. . Set the time. .. In the time filter, click *Show dates*. @@ -138,14 +160,19 @@ You haven't defined any buckets yet, so the visualization is a map of the world. .. Set the *Start date* to May 18, 2015. .. In the time filter, click *now*, then *Absolute*. .. Set the *End date* to May 20, 2015. +.. Click *Update* . Map the geo coordinates from the log files. -.. In the *Buckets* pane, click *Add > Geo coordinates*. -.. Set *Aggregation* to *Geohash*. -.. Set *Field* to *geo.coordinates*. +.. Click *Add layer*. +.. Click the *Grid aggregation* data source. +.. Set *Index pattern* to *logstash*. +.. Click the *Add layer* button. -. Click *Apply changes* image:images/apply-changes-button.png[]. +. Set the layer style. +.. For *Fill color*, select the yellow to red color ramp. +.. For *Border color*, select white. +.. Click *Save & close*. + The map now looks like this: + @@ -155,26 +182,3 @@ image::images/tutorial-visualize-map-2.png[] . Navigate the map by clicking and dragging. Use the controls on the left to zoom the map and set filters. . *Save* this map with the name `Map Example`. - -[float] -[[tutorial-visualize-markdown]] -=== Markdown - -The final visualization is a Markdown widget that renders formatted text. - -. Create a *Markdown* visualization. -. Copy the following text into the text box. -+ -[source,markdown] -# This is a tutorial dashboard! -The Markdown widget uses **markdown** syntax. -> Blockquotes in Markdown use the > character. - -. Click *Apply changes* image:images/apply-changes-button.png[]. -+ -The Markdown renders in the preview pane. -+ -[role="screenshot"] -image::images/tutorial-visualize-md-2.png[] - -. *Save* this visualization with the name `Markdown Example`. diff --git a/docs/images/tutorial-visualize-map-2.png b/docs/images/tutorial-visualize-map-2.png index db9f0d56bc963..f4d1d0e47fe6a 100644 Binary files a/docs/images/tutorial-visualize-map-2.png and b/docs/images/tutorial-visualize-map-2.png differ diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index caff7f5b1fdc6..38fceeb47d6fd 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 1e9600e4d4eb4..0000000000000 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 e9285ac914308..0000000000000 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 0773f0f3a3407..0000000000000 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 a82a09c27e6e8..0000000000000 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 97ac4392827dd..0000000000000 --- 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 0000000000000..11b396a4fe872 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 923b6cc1d6649..cd01acb2df7de 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 88ad6a26d3697..e6908ca773a2f 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -84,7 +84,7 @@ the {es} index `kibana_sample_data_logs` on the shared key iso2 = geo.src. . Set *Right source* to *kibana_sample_data_logs*. . Set *Right field* to *geo.src*. -===== Set the vector style +===== Set the layer style All of the world countries are still a single color because the layer is using <>. To shade the world countries based on which country is sending the most requests, you'll need to use <>. @@ -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]. @@ -161,9 +162,9 @@ image::maps/images/grid_metrics_both.png[] . Select *Sum* in the aggregation select. . Select *bytes* in the field select. -===== Set the vector style +===== Set the layer style -. In *Vector style*, change *Symbol size*: +. In *Layer style*, change *Symbol size*: .. Set *Min size* to 1. .. Set *Max size* to 25. .. In the field select, select *sum of bytes*. @@ -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 af67ff70c81b5..920c448acf6db 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 e47326a1d2068..8cbbcdcbca3ef 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 6e7f939a1d2ab..11a50fea92f05 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. diff --git a/docs/setup/upgrade.asciidoc b/docs/setup/upgrade.asciidoc index ce0259c690b82..8c03032bb8ac3 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 3f9d8fa17bddd..2923e42c1ab34 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 d521f9d8d2846..5427bdee79ecb 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 19f3df341338e..7b3bd10147966 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 7f0cea7fb3aa4..0b978462ae75e 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 0000000000000..b378883572f3c 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 296ba9d25ad5e..0000000000000 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 0000000000000..7d9816a8adb93 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 0000000000000..c1e89384b738c 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 7c7f177011576..0000000000000 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 0000000000000..43ac85537b9bb 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 e669a2d79e0e3..ca07f350f975e 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 0000000000000..e05418d7df7f9 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 0000000000000..5adcfa478652e 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 0000000000000..5015e39b8095a 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 5c87a2809a480..0000000000000 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 0000000000000..e8a72b8b358c5 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 737c081cd691b..94c9c4341d9b8 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 ff3568ed41afa..4ce287396f809 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 12a775ad047b0..7e1d6f94f15fa 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 a8e60e9749f72..8fa9bf1847eb8 100644 --- a/package.json +++ b/package.json @@ -106,10 +106,10 @@ "dependencies": { "@babel/core": "^7.5.5", "@babel/register": "^7.5.5", - "@elastic/charts": "^13.5.12", + "@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", @@ -360,7 +360,7 @@ "chance": "1.0.18", "cheerio": "0.22.0", "chokidar": "3.2.1", - "chromedriver": "^78.0.1", + "chromedriver": "^77.0.0", "classnames": "2.2.6", "dedent": "^0.7.0", "delete-empty": "^2.0.0", @@ -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/eslint-config-kibana/.eslintrc.js b/packages/eslint-config-kibana/.eslintrc.js index 36f0b95c8e69b..98fa62021b5bb 100644 --- a/packages/eslint-config-kibana/.eslintrc.js +++ b/packages/eslint-config-kibana/.eslintrc.js @@ -32,6 +32,10 @@ module.exports = { from: 'x-pack', toRelative: 'x-pack', }, + { + from: 'react-router', + to: 'react-router-dom', + }, ], ], } diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json index e2f3a59e95a47..b0ac86b465a62 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 63fd115fa7594..6514347b0b127 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 13b9e5dc59e4e..ceaf53cbc9753 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 7615fd20645e2..dc8cdcd3e4a1e 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 0000000000000..32282dc54bde6 --- /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 6187455fa60a5..333bc05d28f9b 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 37d23aa443090..98e29c1e4329e 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 2836c80f5b5ad..81fafdb4a7b33 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 f283554de527e..df1ce9e869d3b 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 8feab74d36321..1a292aff303af 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 259bfd782d3fb..21f02325cac66 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-es/src/custom_snapshots.js b/packages/kbn-es/src/custom_snapshots.js index 0af89b8e9b68c..be6bbeca538ff 100644 --- a/packages/kbn-es/src/custom_snapshots.js +++ b/packages/kbn-es/src/custom_snapshots.js @@ -26,7 +26,8 @@ function isVersionFlag(a) { function getCustomSnapshotUrl() { // force use of manually created snapshots until live ones are available if (!process.env.KBN_ES_SNAPSHOT_URL && !process.argv.some(isVersionFlag)) { - return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}'; + // return 'https://storage.googleapis.com/kibana-ci-tmp-artifacts/{name}-{version}-{os}-x86_64.{ext}'; + return; } if (process.env.KBN_ES_SNAPSHOT_URL && process.env.KBN_ES_SNAPSHOT_URL !== 'false') { diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 34a56615ed43a..ac46dd02757cf 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 b6d9a540ac940..b36246d97c1ad 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/README.md b/packages/kbn-spec-to-console/README.md index fc068b7ba65d6..6729f03b3d4db 100644 --- a/packages/kbn-spec-to-console/README.md +++ b/packages/kbn-spec-to-console/README.md @@ -23,14 +23,10 @@ At the root of the Kibana repository, run the following commands: ```sh # OSS -yarn spec_to_console \ - -g "/rest-api-spec/src/main/resources/rest-api-spec/api/*" \ - -d "src/legacy/core_plugins/console/api_server/spec/generated" +yarn spec_to_console -g "/rest-api-spec/src/main/resources/rest-api-spec/api/*" -d "src/legacy/core_plugins/console/server/api_server/spec/generated" # X-pack -yarn spec_to_console \ - -g "/x-pack/plugin/src/test/resources/rest-api-spec/api/*" \ - -d "x-pack/plugins/console_extensions/spec/generated" +yarn spec_to_console -g "/x-pack/plugin/src/test/resources/rest-api-spec/api/*" -d "x-pack/legacy/plugins/console_extensions/spec/generated" ``` ### Information used in Console that is not available in the REST spec diff --git a/packages/kbn-spec-to-console/package.json b/packages/kbn-spec-to-console/package.json index 2e5f897894a90..a6b3e8f96f7db 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/packages/kbn-ui-framework/doc_site/src/components/guide_nav/guide_nav.js b/packages/kbn-ui-framework/doc_site/src/components/guide_nav/guide_nav.js index cee256da79513..53bc42ce33276 100644 --- a/packages/kbn-ui-framework/doc_site/src/components/guide_nav/guide_nav.js +++ b/packages/kbn-ui-framework/doc_site/src/components/guide_nav/guide_nav.js @@ -20,9 +20,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { - Link, -} from 'react-router'; +import { Link } from 'react-router'; // eslint-disable-line import classNames from 'classnames'; diff --git a/packages/kbn-ui-framework/doc_site/src/index.js b/packages/kbn-ui-framework/doc_site/src/index.js index d82df375c59f4..33aa3f582d9ba 100644 --- a/packages/kbn-ui-framework/doc_site/src/index.js +++ b/packages/kbn-ui-framework/doc_site/src/index.js @@ -24,10 +24,7 @@ import 'regenerator-runtime/runtime'; import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; -import { - Router, - hashHistory, -} from 'react-router'; +import { Router, hashHistory } from 'react-router'; // eslint-disable-line // Store. import configureStore from './store/configure_store'; diff --git a/packages/kbn-ui-framework/doc_site/src/store/configure_store.js b/packages/kbn-ui-framework/doc_site/src/store/configure_store.js index 2d6a1b31b0d19..cb14900f88e89 100644 --- a/packages/kbn-ui-framework/doc_site/src/store/configure_store.js +++ b/packages/kbn-ui-framework/doc_site/src/store/configure_store.js @@ -23,11 +23,8 @@ import { compose, } from 'redux'; import thunk from 'redux-thunk'; -import { browserHistory } from 'react-router'; -import { - routerMiddleware, - routerReducer, -} from 'react-router-redux'; +import { browserHistory } from 'react-router'; // eslint-disable-line +import { routerMiddleware, routerReducer } from 'react-router-redux'; import codeViewerReducer from './reducers/code_viewer_reducer'; import sandboxReducer from './reducers/sandbox_reducer'; diff --git a/packages/kbn-ui-framework/doc_site/src/views/not_found/not_found_view.js b/packages/kbn-ui-framework/doc_site/src/views/not_found/not_found_view.js index 34c3a3f6bbe2e..f42c5b1d0c63a 100644 --- a/packages/kbn-ui-framework/doc_site/src/views/not_found/not_found_view.js +++ b/packages/kbn-ui-framework/doc_site/src/views/not_found/not_found_view.js @@ -19,9 +19,7 @@ import React from 'react'; -import { - Link, -} from 'react-router'; +import { Link } from 'react-router'; // eslint-disable-line export const NotFoundView = () => (
diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index d034e4393f58f..ca594fe44b6c7 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -60,7 +60,6 @@ "react-dom": "^16.2.0", "react-redux": "^5.0.6", "react-router": "^3.2.0", - "react-router-dom": "4.2.2", "react-router-redux": "^4.0.8", "redux": "3.7.2", "redux-thunk": "2.2.0", diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 472545b203a9b..9f4e678c6adf5 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 c26a383719fa1..366a5b65fbb99 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 5b1d4affe8840..5be22ea151c32 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 45e94040eeb4a..3390480e56bdd 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 87389d2c10f03..e686f03413dd5 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); @@ -129,6 +158,7 @@ export class ChromeService {
(FORCE_HIDDEN ? false : visibility)), - takeUntil(this.stop$) - )} + isVisible$={this.isVisible$} kibanaVersion={injectedMetadata.getKibanaVersion()} legacyMode={injectedMetadata.getLegacyMode()} navLinks$={navLinks.getNavLinks$()} @@ -165,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 new file mode 100644 index 0000000000000..c8e53b38c618e --- /dev/null +++ b/src/core/public/chrome/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 ELASTIC_SUPPORT_LINK = 'https://support.elastic.co/'; +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 8c135b3c4c49f..5a45491df28e7 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 affc639faf0b8..31a729f90cd93 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 cca16ddcd2a81..3c9713a93144a 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/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 3ddbe420ba284..1e97899be5854 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -179,6 +179,7 @@ interface Props { basePath: HttpStart['basePath']; isLocked?: boolean; onIsLockedUpdate?: (isLocked: boolean) => void; + isCloudEnabled: boolean; } interface State { @@ -296,6 +297,7 @@ class HeaderUI extends Component { kibanaVersion, onIsLockedUpdate, legacyMode, + isCloudEnabled, } = this.props; const { appTitle, @@ -394,7 +396,9 @@ class HeaderUI extends Component { - + diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index 688d76c9d75d9..c04fbaa07ba71 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -17,30 +17,29 @@ * under the License. */ +import * as Rx from 'rxjs'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import * as Rx from 'rxjs'; - +import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { - // TODO: add type annotations - // @ts-ignore - EuiButton, - // @ts-ignore + EuiButtonEmpty, EuiFlexGroup, - // @ts-ignore EuiFlexItem, - // @ts-ignore EuiHeaderSectionItemButton, EuiIcon, EuiPopover, EuiPopoverTitle, EuiSpacer, - EuiText, } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { HeaderExtension } from './header_extension'; import { ChromeHelpExtension } from '../../chrome_service'; +import { + ELASTIC_SUPPORT_LINK, + GITHUB_CREATE_ISSUE_LINK, + KIBANA_ASK_ELASTIC_LINK, + KIBANA_FEEDBACK_LINK, +} from '../../constants'; interface Props { helpExtension$: Rx.Observable; @@ -48,6 +47,7 @@ interface Props { kibanaVersion: string; useDefaultContent?: boolean; kibanaDocLink: string; + isCloudEnabled: boolean; } interface State { @@ -90,23 +90,50 @@ class HeaderHelpMenuUI extends Component { const defaultContent = useDefaultContent ? ( - -

- -

-
- - - - + + + + + + + + + + + + + + + + + + + - +
) : null; diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index d78504a899a34..1ee41fe64418e 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 7391cf7f9454c..e040b29814900 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 ef35fd2aa78ac..1110097c1c92b 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 29b289592b2ef..ca09d4a14bd7a 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 c6c127acbb033..61d73ac233188 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 57dc899016264..f96a0a6f362bf 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 83c2d52f3d77a..6e9de11683364 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 38e6d2a222990..f99a28617aa5c 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 24514cb11548b..b49bafda5b26e 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 799ca43c7fa93..31d49b5952e87 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 9e908bd628003..a68dfa7ebadac 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 13d81989417f1..2b16682c13aad 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 c49548abee0df..ff03e5dffb2ca 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 cfac4c3648053..0d8887774e900 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 a596ea394abda..1e97d8e066d09 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 f6f229496f49d..4b12d5bc6da51 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 048ae2ccbae7f..1170c42cea704 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 8a481fe1704dd..c58ba14d0da3e 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 a432094b15048..cf826eb276252 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 0000000000000..dbd7d5da435a6 --- /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 dcf599f6d1e10..6eaa140e5afad 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 0b24190741b10..58865d13c1afa 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/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index fcc232345a802..0ac2f59525c32 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -39,6 +39,7 @@ interface RequestFixtureOptions { path?: string; method?: RouteMethod; socket?: Socket; + routeTags?: string[]; } function createKibanaRequestMock({ @@ -49,6 +50,7 @@ function createKibanaRequestMock({ query = {}, method = 'get', socket = new Socket(), + routeTags, }: RequestFixtureOptions = {}) { const queryString = querystring.stringify(query); return KibanaRequest.from( @@ -61,10 +63,11 @@ function createKibanaRequestMock({ method, url: { path, + pathname: path, query: queryString, search: queryString ? `?${queryString}` : queryString, }, - route: { settings: {} }, + route: { settings: { tags: routeTags } }, raw: { req: { socket }, }, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 35e83da4ef30c..2a5631ad1c380 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 0000000000000..1330c5aee64fd --- /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 b7c55a8af7c18..99963ad9ce3e8 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 2964e34c370b1..38fe519567a63 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 9f7d8e4f35172..34acb66d4e931 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 3bac17bc46686..61e4d752445c4 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/index.ts b/src/core/server/saved_objects/service/index.ts index 15f46711fc94b..cf0769fced460 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -34,6 +34,7 @@ export interface SavedObjectsLegacyService { addScopedSavedObjectsClientWrapperFactory: SavedObjectsClientProvider< Request >['addClientWrapperFactory']; + setScopedSavedObjectsClientFactory: SavedObjectsClientProvider['setClientFactory']; getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; SavedObjectsClient: typeof SavedObjectsClient; types: string[]; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 6525590ee96c5..79a3e573ab98c 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -49,7 +49,6 @@ describe('SavedObjectsRepository', () => { hits: [ { _index: '.kibana', - _type: '_doc', _id: 'index-pattern:logstash-*', _score: 1, ...mockVersionProps, @@ -65,7 +64,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'config:6.0.0-alpha1', _score: 1, ...mockVersionProps, @@ -80,7 +78,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'index-pattern:stocks-*', _score: 1, ...mockVersionProps, @@ -96,7 +93,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'globaltype:something', _score: 1, ...mockVersionProps, @@ -118,7 +114,6 @@ describe('SavedObjectsRepository', () => { hits: [ { _index: '.kibana', - _type: '_doc', _id: 'foo-namespace:index-pattern:logstash-*', _score: 1, ...mockVersionProps, @@ -135,7 +130,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'foo-namespace:config:6.0.0-alpha1', _score: 1, ...mockVersionProps, @@ -151,7 +145,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'foo-namespace:index-pattern:stocks-*', _score: 1, ...mockVersionProps, @@ -168,7 +161,6 @@ describe('SavedObjectsRepository', () => { }, { _index: '.kibana', - _type: '_doc', _id: 'globaltype:something', _score: 1, ...mockVersionProps, @@ -290,7 +282,6 @@ describe('SavedObjectsRepository', () => { describe('#create', () => { beforeEach(() => { callAdminCluster.mockImplementation((method, params) => ({ - _type: '_doc', _id: params.id, ...mockVersionProps, })); @@ -863,7 +854,6 @@ describe('SavedObjectsRepository', () => { items: [ { create: { - _type: '_doc', _id: 'config:one', error: { reason: 'type[config] missing', @@ -872,7 +862,6 @@ describe('SavedObjectsRepository', () => { }, { create: { - _type: '_doc', _id: 'index-pattern:two', ...mockVersionProps, }, @@ -910,14 +899,12 @@ describe('SavedObjectsRepository', () => { items: [ { create: { - _type: '_doc', _id: 'config:one', ...mockVersionProps, }, }, { create: { - _type: '_doc', _id: 'index-pattern:two', ...mockVersionProps, }, @@ -962,7 +949,6 @@ describe('SavedObjectsRepository', () => { items: [ { create: { - _type: '_doc', _id: 'foo-namespace:config:one', _index: '.kibana-test', _primary_term: 1, @@ -971,7 +957,6 @@ describe('SavedObjectsRepository', () => { }, { create: { - _type: '_doc', _id: 'foo-namespace:index-pattern:two', _primary_term: 1, _seq_no: 2, @@ -1021,14 +1006,12 @@ describe('SavedObjectsRepository', () => { items: [ { create: { - _type: '_doc', _id: 'config:one', ...mockVersionProps, }, }, { create: { - _type: '_doc', _id: 'index-pattern:two', ...mockVersionProps, }, @@ -1092,7 +1075,7 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); - it('should return objects in the same order regardless of type', () => {}); + it('should return objects in the same order regardless of type', () => { }); }); describe('#delete', () => { @@ -1496,7 +1479,6 @@ describe('SavedObjectsRepository', () => { describe('#get', () => { const noNamespaceResult = { _id: 'index-pattern:logstash-*', - _type: '_doc', ...mockVersionProps, _source: { type: 'index-pattern', @@ -1509,7 +1491,6 @@ describe('SavedObjectsRepository', () => { }; const namespacedResult = { _id: 'foo-namespace:index-pattern:logstash-*', - _type: '_doc', ...mockVersionProps, _source: { namespace: 'foo-namespace', @@ -1699,14 +1680,12 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockResolvedValue({ docs: [ { - _type: '_doc', _id: 'config:good', found: true, ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test' } }, }, { - _type: '_doc', _id: 'config:bad', found: false, }, @@ -1728,14 +1707,12 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockResolvedValue({ docs: [ { - _type: '_doc', _id: 'config:good', found: true, ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test' } }, }, { - _type: '_doc', _id: 'config:bad', found: false, }, @@ -1770,21 +1747,18 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockResolvedValue({ docs: [ { - _type: '_doc', _id: 'one', found: true, ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test1' } }, }, { - _type: '_doc', _id: 'three', found: true, ...mockVersionProps, _source: { ...mockTimestampFields, config: { title: 'Test3' } }, }, { - _type: '_doc', _id: 'five', found: true, ...mockVersionProps, @@ -1859,7 +1833,6 @@ describe('SavedObjectsRepository', () => { beforeEach(() => { callAdminCluster.mockResolvedValue({ _id: `${type}:${id}`, - _type: '_doc', ...mockVersionProps, result: 'updated', }); @@ -2177,7 +2150,6 @@ describe('SavedObjectsRepository', () => { items: objects.map(items => ({ update: { _id: `${items.type}:${items.id}`, - _type: '_doc', ...mockVersionProps, result: 'updated', } @@ -2244,15 +2216,14 @@ describe('SavedObjectsRepository', () => { callAdminCluster.mockReturnValue({ items: objects - // remove invalid from mocks + // remove invalid from mocks .filter(item => item.id !== 'invalid') .map(items => { - switch(items.id) { + switch (items.id) { case 'version_clash': return ({ update: { _id: `${items.type}:${items.id}`, - _type: '_doc', error: { type: 'version_conflict_engine_exception' } @@ -2262,7 +2233,6 @@ describe('SavedObjectsRepository', () => { return ({ update: { _id: `${items.type}:${items.id}`, - _type: '_doc', ...mockVersionProps, result: 'updated', } @@ -2353,7 +2323,7 @@ describe('SavedObjectsRepository', () => { expect(callAdminCluster).toHaveBeenCalledTimes(1); - const [, { body: [{ update: firstUpdate },, { update: secondUpdate }] }] = callAdminCluster.mock.calls[0]; + const [, { body: [{ update: firstUpdate }, , { update: secondUpdate }] }] = callAdminCluster.mock.calls[0]; expect(firstUpdate).toMatchObject({ if_seq_no: 100, @@ -2407,7 +2377,7 @@ describe('SavedObjectsRepository', () => { expect(callAdminCluster).toHaveBeenCalledTimes(1); - const [, { body: [, { doc }] } ] = callAdminCluster.mock.calls[0]; + const [, { body: [, { doc }] }] = callAdminCluster.mock.calls[0]; expect(doc).toMatchObject({ references: [{ @@ -2434,7 +2404,7 @@ describe('SavedObjectsRepository', () => { expect(callAdminCluster).toHaveBeenCalledTimes(1); - const [, { body: [, { doc }] } ] = callAdminCluster.mock.calls[0]; + const [, { body: [, { doc }] }] = callAdminCluster.mock.calls[0]; expect(doc).toMatchObject({ references: [], @@ -2637,7 +2607,6 @@ describe('SavedObjectsRepository', () => { describe('#incrementCounter', () => { beforeEach(() => { callAdminCluster.mockImplementation((method, params) => ({ - _type: '_doc', _id: params.id, ...mockVersionProps, _index: '.kibana', @@ -2657,7 +2626,6 @@ describe('SavedObjectsRepository', () => { it('formats Elasticsearch response', async () => { callAdminCluster.mockImplementation((method, params) => ({ - _type: '_doc', _id: params.id, ...mockVersionProps, _index: '.kibana', @@ -2776,7 +2744,6 @@ describe('SavedObjectsRepository', () => { it(`doesn't prepend namespace to the id or add namespace property when providing namespace for namespace agnostic type`, async () => { callAdminCluster.mockImplementation((method, params) => ({ - _type: '_doc', _id: params.id, ...mockVersionProps, _index: '.kibana', diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 54b9938decb0a..51d4a8ad50ad6 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/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts index ad1ceb60cdb86..87607acd94fc4 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts @@ -100,7 +100,7 @@ export class SavedObjectsClientProvider { this._wrapperFactories.add(priority, { id, factory }); } - setClientFactory(customClientFactory: SavedObjectsClientFactory) { + setClientFactory(customClientFactory: SavedObjectsClientFactory) { if (this._clientFactory !== this._originalClientFactory) { throw new Error(`custom client factory is already set, unable to replace the current one`); } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 14943fc96f268..97a04a4a4efab 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 & { @@ -1492,6 +1462,8 @@ export interface SavedObjectsLegacyService { // (undocumented) schema: SavedObjectsSchema; // (undocumented) + setScopedSavedObjectsClientFactory: SavedObjectsClientProvider['setClientFactory']; + // (undocumented) types: string[]; } diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index aee6461580654..f912a31901ad8 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 46974e204c7a4..6c38de03f0f2d 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 423ff2a1dfd90..1a0f29f6ae6d9 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 022c3e4330032..775c890675410 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 0d9b2a6129de0..315ae3328c47f 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 aead3f35ba841..8e5d9f4860d95 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 77854f9af680b..08288343d9077 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 3ae7d33d24d68..5c0462ce86fa9 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 0926ef365c894..6609b905b81ec 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 0c785a84bb469..f5c20da89dcfa 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 8fe09db0a5874..9142955185a1a 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/es_archiver/lib/docs/__tests__/index_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js index e48af1f60509b..4a5d048637fcc 100644 --- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js @@ -35,11 +35,11 @@ import { const recordsToBulkBody = records => { return records.reduce((acc, record) => { - const { index, type, id, source } = record.value; + const { index, id, source } = record.value; return [ ...acc, - { index: { _index: index, _type: type, _id: id } }, + { index: { _index: index, _id: id } }, source ]; }, []); diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.js b/src/es_archiver/lib/docs/index_doc_records_stream.js index 943b1e6e2f329..3e88f10387f9e 100644 --- a/src/es_archiver/lib/docs/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/index_doc_records_stream.js @@ -30,7 +30,6 @@ export function createIndexDocRecordsStream(client, stats, progress) { { index: { _index: doc.index, - _type: doc.type, _id: doc.id, } }, diff --git a/src/es_archiver/lib/indices/__tests__/create_index_stream.js b/src/es_archiver/lib/indices/__tests__/create_index_stream.js index 830512f3476ed..4ce12ab3376a3 100644 --- a/src/es_archiver/lib/indices/__tests__/create_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/create_index_stream.js @@ -113,7 +113,6 @@ describe('esArchiver: createCreateIndexStream()', () => { sinon.assert.calledWith(client.indices.create, { method: 'PUT', index: 'index', - include_type_name: false, body: { settings: undefined, mappings: undefined, diff --git a/src/es_archiver/lib/indices/create_index_stream.js b/src/es_archiver/lib/indices/create_index_stream.js index 746f0d689ce56..0daccbee91bd0 100644 --- a/src/es_archiver/lib/indices/create_index_stream.js +++ b/src/es_archiver/lib/indices/create_index_stream.js @@ -41,9 +41,6 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { async function handleIndex(record) { const { index, settings, mappings, aliases } = record.value; - - // Determine if the mapping belongs to a pre-7.0 instance, for BWC tests, mainly - const isPre7Mapping = !!mappings && Object.keys(mappings).length > 0 && !mappings.properties; const isKibana = index.startsWith('.kibana'); async function attemptToCreate(attemptNumber = 1) { @@ -55,7 +52,6 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { await client.indices.create({ method: 'PUT', index, - include_type_name: isPre7Mapping, body: { settings, mappings, diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index dc916e11d698c..6f491783829a8 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -176,7 +176,6 @@ export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds } export async function createDefaultSpace({ index, client }) { await client.create({ index, - type: '_doc', id: 'space:default', ignore: 409, body: { diff --git a/src/fixtures/fake_row.js b/src/fixtures/fake_row.js index 5bc752de299b2..747c6e06be427 100644 --- a/src/fixtures/fake_row.js +++ b/src/fixtures/fake_row.js @@ -31,7 +31,6 @@ export function getFakeRow(id, mapping) { _id: id, _index: 'test', _source: getFakeRowVals('original', id, mapping), - _type: 'doc', sort: [id], }; } diff --git a/src/fixtures/hits.js b/src/fixtures/hits.js index 413501a3599ed..e8da3e8ee285b 100644 --- a/src/fixtures/hits.js +++ b/src/fixtures/hits.js @@ -37,7 +37,6 @@ export default function fitsFixture() { return { _score: 1, _id: 1000 + i, - _type: 'test', _index: 'test-index', _source: { '@timestamp': row[0], diff --git a/src/legacy/core_plugins/console/index.ts b/src/legacy/core_plugins/console/index.ts index 30a85f4e7d342..caef3ff6f99f3 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/constants/help_example.txt b/src/legacy/core_plugins/console/np_ready/public/application/constants/help_example.txt index 9bc22ecd2d630..fd37c41367033 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/constants/help_example.txt +++ b/src/legacy/core_plugins/console/np_ready/public/application/constants/help_example.txt @@ -1,8 +1,8 @@ # index a doc -PUT index/type/1 +PUT index/1 { "body": "here" } # and get it ... -GET index/type/1 +GET index/1 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 4e5afbdb5821e..518630c5a07c1 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 1a2d312823f6f..8c60ff23648be 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 188a738d59794..f02b0b5e72999 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 66a693d4b2af7..0000000000000 --- a/src/legacy/core_plugins/console/public/quarantined/index.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/src/legacy/core_plugins/console/public/quarantined/tests/src/input_tokenization.test.js b/src/legacy/core_plugins/console/public/quarantined/tests/src/input_tokenization.test.js index cfb0f1b8c24db..01f30f826ab26 100644 --- a/src/legacy/core_plugins/console/public/quarantined/tests/src/input_tokenization.test.js +++ b/src/legacy/core_plugins/console/public/quarantined/tests/src/input_tokenization.test.js @@ -251,10 +251,10 @@ describe('Input Tokenization', () => { 'paren.lparen', '{', 'paren.rparen', '}', 'paren.rparen', '}' ], 'POST _search\n' + - '{\n' + - ' "q": {}\n' + - ' \n' + - '}' + '{\n' + + ' "q": {}\n' + + ' \n' + + '}' ); tokenTest( @@ -263,10 +263,10 @@ describe('Input Tokenization', () => { 'paren.rparen', '}', 'paren.rparen', '}' ], 'POST _search\n' + - '{\n' + - ' "q": { "s": {}}\n' + - ' \n' + - '}' + '{\n' + + ' "q": { "s": {}}\n' + + ' \n' + + '}' ); function statesAsList() { @@ -305,44 +305,44 @@ describe('Input Tokenization', () => { statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "query": { "match_all": {} }\n' + - '}' + '{\n' + + ' "query": { "match_all": {} }\n' + + '}' ); statesTest( ['start', 'json', ['json', 'json'], ['json', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "query": { \n' + - ' "match_all": {} \n' + - ' }\n' + - '}' + '{\n' + + ' "query": { \n' + + ' "match_all": {} \n' + + ' }\n' + + '}' ); statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": { "source": "" }\n' + - '}' + '{\n' + + ' "script": { "source": "" }\n' + + '}' ); statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": ""\n' + - '}' + '{\n' + + ' "script": ""\n' + + '}' ); statesTest( ['start', 'json', ['json', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": {\n' + - ' }\n' + - '}' + '{\n' + + ' "script": {\n' + + ' }\n' + + '}' ); @@ -350,41 +350,41 @@ describe('Input Tokenization', () => { ['start', 'json', ['script-start', 'json', 'json', 'json'], ['script-start', 'json', 'json', 'json'], ['json', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "test": { "script": """\n' + - ' test script\n' + - ' """\n' + - ' }\n' + - '}' + '{\n' + + ' "test": { "script": """\n' + + ' test script\n' + + ' """\n' + + ' }\n' + + '}' ); statesTest( ['start', 'json', ['script-start', 'json'], ['script-start', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": """\n' + - ' test script\n' + - ' """,\n' + - '}' + '{\n' + + ' "script": """\n' + + ' test script\n' + + ' """,\n' + + '}' ); statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "script": """test script""",\n' + - '}' + '{\n' + + ' "script": """test script""",\n' + + '}' ); statesTest( ['start', 'json', ['string_literal', 'json'], ['string_literal', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "something": """\n' + - ' test script\n' + - ' """,\n' + - '}' + '{\n' + + ' "something": """\n' + + ' test script\n' + + ' """,\n' + + '}' ); statesTest( @@ -392,21 +392,21 @@ describe('Input Tokenization', () => { ['json', 'json'], ['json', 'json'], 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "something": { "f" : """\n' + - ' test script\n' + - ' """,\n' + - ' "g": 1\n' + - ' }\n' + - '}' + '{\n' + + ' "something": { "f" : """\n' + + ' test script\n' + + ' """,\n' + + ' "g": 1\n' + + ' }\n' + + '}' ); statesTest( ['start', 'json', 'json', 'start'], 'POST _search\n' + - '{\n' + - ' "something": """test script""",\n' + - '}' + '{\n' + + ' "something": """test script""",\n' + + '}' ); }); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js index 7c6e221d9ce81..aa7b764f84fc7 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/params.js @@ -61,7 +61,7 @@ describe('Console Proxy Route', () => { const { statusCode } = await server.inject({ method: 'POST', - url: '/api/console/proxy?method=GET&path=/baz/type/id', + url: '/api/console/proxy?method=GET&path=/baz/id', }); expect(statusCode).to.be(403); @@ -79,7 +79,7 @@ describe('Console Proxy Route', () => { const { statusCode } = await server.inject({ method: 'POST', - url: '/api/console/proxy?method=GET&path=/foo/type/id', + url: '/api/console/proxy?method=GET&path=/foo/id', }); expect(statusCode).to.be(200); @@ -98,7 +98,7 @@ describe('Console Proxy Route', () => { const { statusCode } = await server.inject({ method: 'POST', - url: '/api/console/proxy?method=GET&path=/foo/type/id', + url: '/api/console/proxy?method=GET&path=/foo/id', }); expect(statusCode).to.be(200); @@ -116,7 +116,7 @@ describe('Console Proxy Route', () => { server.route(createProxyRoute({ baseUrl: 'http://localhost:9200', getConfigForReq })); await server.inject({ method: 'POST', - url: '/api/console/proxy?method=HEAD&path=/index/type/id', + url: '/api/console/proxy?method=HEAD&path=/index/id', }); sinon.assert.calledOnce(getConfigForReq); @@ -125,8 +125,8 @@ describe('Console Proxy Route', () => { expect(args[0]).to.have.property('method', 'post'); expect(args[0]) .to.have.property('query') - .eql({ method: 'HEAD', path: '/index/type/id' }); - expect(args[1]).to.be('http://localhost:9200/index/type/id?pretty=true'); + .eql({ method: 'HEAD', path: '/index/id' }); + expect(args[1]).to.be('http://localhost:9200/index/id?pretty=true'); }); it('sends the returned timeout, agent, and base headers to request', async () => { @@ -154,7 +154,7 @@ describe('Console Proxy Route', () => { await server.inject({ method: 'POST', - url: '/api/console/proxy?method=HEAD&path=/index/type/id', + url: '/api/console/proxy?method=HEAD&path=/index/id', }); sinon.assert.calledOnce(requestModule.sendRequest); diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js b/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js index 7cabe89177164..f20adb897be65 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_route/query_string.js @@ -85,19 +85,19 @@ describe('Console Proxy Route', () => { }); describe('starts with a slash', () => { it('combines well with the base url', async () => { - await request('GET', '/index/type/id'); + await request('GET', '/index/id'); sinon.assert.calledOnce(requestModule.sendRequest); expect(requestModule.sendRequest.getCall(0).args[0].uri.href).to.be( - 'http://localhost:9200/index/type/id?pretty=true' + 'http://localhost:9200/index/id?pretty=true' ); }); }); describe(`doesn't start with a slash`, () => { it('combines well with the base url', async () => { - await request('GET', 'index/type/id'); + await request('GET', 'index/id'); sinon.assert.calledOnce(requestModule.sendRequest); expect(requestModule.sendRequest.getCall(0).args[0].uri.href).to.be( - 'http://localhost:9200/index/type/id?pretty=true' + 'http://localhost:9200/index/id?pretty=true' ); }); }); diff --git a/src/legacy/core_plugins/console/server/__tests__/wildcard_matcher.js b/src/legacy/core_plugins/console/server/__tests__/wildcard_matcher.js index 4eb0de4e5ebec..9999c98701ffc 100644 --- a/src/legacy/core_plugins/console/server/__tests__/wildcard_matcher.js +++ b/src/legacy/core_plugins/console/server/__tests__/wildcard_matcher.js @@ -40,7 +40,7 @@ describe('WildcardMatcher', function () { it('matches nothing', () => should('', '*')); it('does not match /', () => shouldNot('/', '*')); it('matches localhost', () => should('localhost', '*')); - it('matches a path', () => should('/index/type/_search', '*')); + it('matches a path', () => should('/index/_search', '*')); describe('defaultValue = /', function () { it('matches /', () => should('/', '*', '/')); @@ -52,7 +52,7 @@ describe('WildcardMatcher', function () { it('does not match https', () => shouldNot('https', 'http')); it('does not match nothing', () => shouldNot('', 'http')); it('does not match localhost', () => shouldNot('localhost', 'http')); - it('does not match a path', () => shouldNot('/index/type/_search', 'http')); + it('does not match a path', () => shouldNot('/index/_search', 'http')); }); describe('pattern = 560{1..9}', function () { diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/count.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/count.json index a657440f1fe4a..bd69fd0c77ec8 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/count.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/count.json @@ -30,8 +30,7 @@ ], "patterns": [ "_count", - "{indices}/_count", - "{indices}/{type}/_count" + "{indices}/_count" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-count.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json index 12735f2d4b693..3867efd814238 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/delete_by_query.json @@ -1,6 +1,7 @@ { "delete_by_query": { "url_params": { + "analyzer": "", "analyze_wildcard": "__flag__", "default_operator": [ "AND", @@ -30,7 +31,6 @@ "dfs_query_then_fetch" ], "search_timeout": "", - "size": "", "max_docs": "all documents", "sort": [], "_source": [], @@ -52,8 +52,7 @@ "POST" ], "patterns": [ - "{indices}/_delete_by_query", - "{indices}/{type}/_delete_by_query" + "{indices}/_delete_by_query" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-delete-by-query.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/exists.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/exists.json index 4b7b18b9fe1b3..a6799b9e361cd 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/exists.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/exists.json @@ -21,8 +21,7 @@ "HEAD" ], "patterns": [ - "{indices}/_doc/{id}", - "{indices}/{type}/{id}" + "{indices}/_doc/{id}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-get.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/explain.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/explain.json index be01e462878db..e4654a99a76ea 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/explain.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/explain.json @@ -22,8 +22,7 @@ "POST" ], "patterns": [ - "{indices}/_explain/{id}", - "{indices}/{type}/{id}/_explain" + "{indices}/_explain/{id}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-explain.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/get.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/get.json index a0b70545baff9..5a72761c7c32e 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/get.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/get.json @@ -21,8 +21,7 @@ "GET" ], "patterns": [ - "{indices}/_doc/{id}", - "{indices}/{type}/{id}" + "{indices}/_doc/{id}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-get.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_context.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_context.json new file mode 100644 index 0000000000000..528c261df7707 --- /dev/null +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_script_context.json @@ -0,0 +1,10 @@ +{ + "get_script_context": { + "methods": [ + "GET" + ], + "patterns": [ + "_script_context" + ] + } +} diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/get_source.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_source.json index 420e03a1bdcf1..8201960363a78 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/get_source.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/get_source.json @@ -20,8 +20,7 @@ "GET" ], "patterns": [ - "{indices}/_source/{id}", - "{indices}/{type}/{id}/_source" + "{indices}/_source/{id}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-get.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/index.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/index.json index 7b5551727d645..25977806776a7 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/index.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/index.json @@ -29,9 +29,7 @@ ], "patterns": [ "{indices}/_doc/{id}", - "{indices}/_doc", - "{indices}/{type}", - "{indices}/{type}/{id}" + "{indices}/_doc" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-index_.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.create.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.create.json index 1970f88b30958..8227e38d3c6d9 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.create.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.create.json @@ -1,7 +1,6 @@ { "indices.create": { "url_params": { - "include_type_name": "__flag__", "wait_for_active_shards": "", "timeout": "", "master_timeout": "" diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get.json index f515e73b250a7..7ca9e88274aa5 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get.json @@ -1,7 +1,6 @@ { "indices.get": { "url_params": { - "include_type_name": "__flag__", "local": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_field_mapping.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_field_mapping.json index ae82696434ced..ea952435566ed 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_field_mapping.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_field_mapping.json @@ -1,7 +1,6 @@ { "indices.get_field_mapping": { "url_params": { - "include_type_name": "__flag__", "include_defaults": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", @@ -18,9 +17,7 @@ ], "patterns": [ "_mapping/field/{fields}", - "{indices}/_mapping/field/{fields}", - "_mapping/{type}/field/{fields}", - "{indices}/_mapping/{type}/field/{fields}" + "{indices}/_mapping/field/{fields}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-get-field-mapping.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_mapping.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_mapping.json index 03f5de56ea351..73f4e42262bf2 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_mapping.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_mapping.json @@ -1,7 +1,6 @@ { "indices.get_mapping": { "url_params": { - "include_type_name": "__flag__", "ignore_unavailable": "__flag__", "allow_no_indices": "__flag__", "expand_wildcards": [ @@ -18,9 +17,7 @@ ], "patterns": [ "_mapping", - "{indices}/_mapping", - "_mapping/{type}", - "{indices}/_mapping/{type}" + "{indices}/_mapping" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-get-mapping.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_template.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_template.json index d5f52ec76b374..f5902929c25cc 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.get_template.json @@ -1,7 +1,6 @@ { "indices.get_template": { "url_params": { - "include_type_name": "__flag__", "flat_settings": "__flag__", "master_timeout": "", "local": "__flag__" diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_mapping.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_mapping.json index 3b833117be499..07a62a64b64e1 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_mapping.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_mapping.json @@ -1,7 +1,6 @@ { "indices.put_mapping": { "url_params": { - "include_type_name": "__flag__", "timeout": "", "master_timeout": "", "ignore_unavailable": "__flag__", @@ -18,14 +17,7 @@ "POST" ], "patterns": [ - "{indices}/_mapping", - "{indices}/{type}/_mapping", - "{indices}/_mapping/{type}", - "{indices}/{type}/_mappings", - "{indices}/_mappings/{type}", - "_mappings/{type}", - "{indices}/_mappings", - "_mapping/{type}" + "{indices}/_mapping" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-put-mapping.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_template.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_template.json index 8b3480f24d8fb..54a7625a2713c 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.put_template.json @@ -1,7 +1,6 @@ { "indices.put_template": { "url_params": { - "include_type_name": "__flag__", "order": "", "create": "__flag__", "timeout": "", diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.rollover.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.rollover.json index 7fa76a687eb77..19e0f1f909ab8 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.rollover.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.rollover.json @@ -1,7 +1,6 @@ { "indices.rollover": { "url_params": { - "include_type_name": "__flag__", "timeout": "", "dry_run": "__flag__", "master_timeout": "", diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json index 6fbdea0f1244b..31acc86a2fa56 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.shrink.json @@ -1,7 +1,6 @@ { "indices.shrink": { "url_params": { - "copy_settings": "__flag__", "timeout": "", "master_timeout": "", "wait_for_active_shards": "" diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json index 68f2e338cd201..1bfbaa078b796 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/indices.split.json @@ -1,7 +1,6 @@ { "indices.split": { "url_params": { - "copy_settings": "__flag__", "timeout": "", "master_timeout": "", "wait_for_active_shards": "" diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/mget.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/mget.json index f84b46a379cf4..612bef571fe3f 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/mget.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/mget.json @@ -16,8 +16,7 @@ ], "patterns": [ "_mget", - "{indices}/_mget", - "{indices}/{type}/_mget" + "{indices}/_mget" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-multi-get.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch.json index 502d3e25686df..ecb71e9ba23c0 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch.json @@ -20,8 +20,7 @@ ], "patterns": [ "_msearch", - "{indices}/_msearch", - "{indices}/{type}/_msearch" + "{indices}/_msearch" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-multi-search.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json index 02077dd91439b..0b0ca087b1819 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/msearch_template.json @@ -9,7 +9,8 @@ ], "typed_keys": "__flag__", "max_concurrent_searches": "", - "rest_total_hits_as_int": "__flag__" + "rest_total_hits_as_int": "__flag__", + "ccs_minimize_roundtrips": "__flag__" }, "methods": [ "GET", @@ -17,8 +18,7 @@ ], "patterns": [ "_msearch/template", - "{indices}/_msearch/template", - "{indices}/{type}/_msearch/template" + "{indices}/_msearch/template" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/mtermvectors.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/mtermvectors.json index f5c8cbe76bbc4..72a134eca4d2e 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/mtermvectors.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/mtermvectors.json @@ -25,8 +25,7 @@ ], "patterns": [ "_mtermvectors", - "{indices}/_mtermvectors", - "{indices}/{type}/_mtermvectors" + "{indices}/_mtermvectors" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-multi-termvectors.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/search.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/search.json index 24bda08dd5dbf..eb21b43644d77 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/search.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/search.json @@ -65,8 +65,7 @@ ], "patterns": [ "_search", - "{indices}/_search", - "{indices}/{type}/_search" + "{indices}/_search" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/search-search.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json index 6b73e939843a4..582ecab1dd614 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/search_template.json @@ -22,7 +22,8 @@ "explain": "__flag__", "profile": "__flag__", "typed_keys": "__flag__", - "rest_total_hits_as_int": "__flag__" + "rest_total_hits_as_int": "__flag__", + "ccs_minimize_roundtrips": "__flag__" }, "methods": [ "GET", @@ -30,8 +31,7 @@ ], "patterns": [ "_search/template", - "{indices}/_search/template", - "{indices}/{type}/_search/template" + "{indices}/_search/template" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/termvectors.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/termvectors.json index 80373d903aad8..d94cffc38b7af 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/termvectors.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/termvectors.json @@ -24,9 +24,7 @@ ], "patterns": [ "{indices}/_termvectors/{id}", - "{indices}/_termvectors", - "{indices}/{type}/{id}/_termvectors", - "{indices}/{type}/_termvectors" + "{indices}/_termvectors" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-termvectors.html" } diff --git a/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json b/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json index e5857f219af46..739ea16888146 100644 --- a/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json +++ b/src/legacy/core_plugins/console/server/api_server/spec/generated/update_by_query.json @@ -32,7 +32,6 @@ "dfs_query_then_fetch" ], "search_timeout": "", - "size": "", "max_docs": "all documents", "sort": [], "_source": [], @@ -55,8 +54,7 @@ "POST" ], "patterns": [ - "{indices}/_update_by_query", - "{indices}/{type}/_update_by_query" + "{indices}/_update_by_query" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/docs-update-by-query.html" } 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 5333aff8b87da..9e2478cb0704e 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 0000000000000..a5fac10e4693f --- /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 0000000000000..21ba32ec6a6fe --- /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 5dd5c05647789..84da576e8205c 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 dbff5096f2287..7ee3e375c0967 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 60828b4a2a202..2412541e8c5c8 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 4767b6d3a3ca7..2c58af9deaf49 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 bdeeb787c983d..9ce1b5f2e4a20 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 62f5ddbe9e2b0..1c877f4f14251 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 d3b5944127965..39d1296ddf8bc 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 76beb4ee56053..2059f61fde59e 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 5576427b1592a..31a17315db7dd 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(

{ ['className', { watchDepth: 'reference' }], ['pluginDataStart', { watchDepth: 'reference' }], ]); - }) - .directive('applyFiltersPopover', () => { - return { - restrict: 'E', - template: '', - compile: (elem: any) => { - const child = document.createElement('apply-filters-popover-helper'); - - // Copy attributes to the child directive - for (const attr of elem[0].attributes) { - child.setAttribute(attr.name, attr.value); - } - - // Add a key attribute that will force a full rerender every time that - // a filter changes. - child.setAttribute('key', 'key'); - - // Append helper directive - elem.append(child); - - const linkFn = ($scope: any, _: any, $attr: any) => { - // Watch only for filter changes to update key. - $scope.$watch( - () => { - return $scope.$eval($attr.filters) || []; - }, - (newVal: any) => { - $scope.key = Date.now(); - }, - true - ); - }; - - return linkFn; - }, - }; - }) - .directive('applyFiltersPopoverHelper', (reactDirective: any) => - reactDirective(wrapInI18nContext(ApplyFiltersPopover), [ - ['filters', { watchDepth: 'collection' }], - ['onCancel', { watchDepth: 'reference' }], - ['onSubmit', { watchDepth: 'reference' }], - ['indexPatterns', { watchDepth: 'collection' }], - - // Key is needed to trigger a full rerender of the component - 'key', - ]) - ); + }); uiModules.get('kibana/index_patterns').value('indexPatterns', indexPatterns); }); diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts index 8043e0fb6e3f9..45d5c07cd1b26 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/execute.ts @@ -17,7 +17,7 @@ * under the License. */ -import { fromExpression } from '@kbn/interpreter/target/common'; +import { fromExpression, toExpression } from '@kbn/interpreter/target/common'; import { DataAdapter, RequestAdapter, Adapters } from '../../../../../../plugins/inspector/public'; import { getInterpreter } from './services'; import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './types'; @@ -38,17 +38,18 @@ export class ExpressionDataHandler { private inspectorAdapters: Adapters; private promise: Promise; + public isPending: boolean = true; constructor(expression: string | ExpressionAST, params: IExpressionLoaderParams) { if (typeof expression === 'string') { this.expression = expression; this.ast = fromExpression(expression) as ExpressionAST; } else { this.ast = expression; - this.expression = ''; + this.expression = toExpression(this.ast); } this.abortController = new AbortController(); - this.inspectorAdapters = this.getActiveInspectorAdapters(); + this.inspectorAdapters = params.inspectorAdapters || this.getActiveInspectorAdapters(); const getInitialContext = () => ({ type: 'kibana_context', @@ -58,11 +59,21 @@ export class ExpressionDataHandler { const defaultContext = { type: 'null' }; const interpreter = getInterpreter(); - this.promise = interpreter.interpretAst(this.ast, params.context || defaultContext, { - getInitialContext, - inspectorAdapters: this.inspectorAdapters, - abortSignal: this.abortController.signal, - }); + this.promise = interpreter + .interpretAst(this.ast, params.context || defaultContext, { + getInitialContext, + inspectorAdapters: this.inspectorAdapters, + abortSignal: this.abortController.signal, + }) + .then( + (v: IInterpreterResult) => { + this.isPending = false; + return v; + }, + () => { + this.isPending = false; + } + ); } cancel = () => { diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts index a3caa1c47b150..4c3bc76af351d 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.test.ts @@ -67,7 +67,7 @@ describe('execute helper function', () => { }); describe('ExpressionLoader', () => { - const expressionString = ''; + const expressionString = 'demodata'; describe('constructor', () => { it('accepts expression string', () => { @@ -134,6 +134,8 @@ describe('ExpressionLoader', () => { (ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({ getData: () => true, cancel: cancelMock, + isPending: () => true, + inspect: () => {}, })); const expressionLoader = new ExpressionLoader(element, expressionString, {}); @@ -160,10 +162,15 @@ describe('ExpressionLoader', () => { (ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({ getData, cancel: cancelMock, + isPending: () => true, + inspect: () => {}, })); + (ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({ getData, cancel: cancelMock, + isPending: () => true, + inspect: () => {}, })); const expressionLoader = new ExpressionLoader(element, expressionString, {}); @@ -193,6 +200,8 @@ describe('ExpressionLoader', () => { (ExpressionDataHandler as jest.Mock).mockImplementationOnce(() => ({ getData, cancel: cancelMock, + isPending: () => true, + inspect: () => {}, })); const expressionLoader = new ExpressionLoader(element, expressionString, {}); diff --git a/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts index 709fbc78a9b52..2213cd30010b2 100644 --- a/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts +++ b/src/legacy/core_plugins/expressions/public/np_ready/public/loader.ts @@ -38,11 +38,12 @@ export class ExpressionLoader { private loadingSubject: Subject; private data: Data; private params: IExpressionLoaderParams = {}; + private ignoreNextResponse = false; constructor( element: HTMLElement, - expression: string | ExpressionAST, - params: IExpressionLoaderParams + expression?: string | ExpressionAST, + params?: IExpressionLoaderParams ) { this.dataSubject = new Subject(); this.data$ = this.dataSubject.asObservable().pipe(share()); @@ -65,7 +66,9 @@ export class ExpressionLoader { this.setParams(params); - this.loadData(expression, this.params); + if (expression) { + this.loadData(expression, this.params); + } } destroy() { @@ -117,9 +120,10 @@ export class ExpressionLoader { update(expression?: string | ExpressionAST, params?: IExpressionLoaderParams): void { this.setParams(params); + this.loadingSubject.next(); if (expression) { this.loadData(expression, this.params); - } else { + } else if (this.data) { this.render(this.data); } } @@ -128,18 +132,22 @@ export class ExpressionLoader { expression: string | ExpressionAST, params: IExpressionLoaderParams ): Promise => { - this.loadingSubject.next(); - if (this.dataHandler) { + if (this.dataHandler && this.dataHandler.isPending) { + this.ignoreNextResponse = true; this.dataHandler.cancel(); } this.setParams(params); this.dataHandler = new ExpressionDataHandler(expression, params); + if (!params.inspectorAdapters) params.inspectorAdapters = this.dataHandler.inspect(); const data = await this.dataHandler.getData(); + if (this.ignoreNextResponse) { + this.ignoreNextResponse = false; + return; + } this.dataSubject.next(data); }; private render(data: Data): void { - this.loadingSubject.next(); this.renderHandler.render(data, this.params.extraHandlers); } @@ -148,23 +156,16 @@ export class ExpressionLoader { return; } - if (params.searchContext && this.params.searchContext) { + if (params.searchContext) { this.params.searchContext = _.defaults( {}, params.searchContext, - this.params.searchContext + this.params.searchContext || {} ) as any; } if (params.extraHandlers && this.params) { this.params.extraHandlers = params.extraHandlers; } - - if (!Object.keys(this.params).length) { - this.params = { - ...params, - searchContext: { type: 'kibana_context', ...(params.searchContext || {}) }, - }; - } } } 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 96c0802d3772a..ea029af9e4890 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 39f5f6a50a5a6..8784f0e79ca8d 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/functions/esaggs.ts b/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts index d232a97c3c34c..bcb8d00663e01 100644 --- a/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts +++ b/src/legacy/core_plugins/interpreter/public/functions/esaggs.ts @@ -22,9 +22,15 @@ import { i18n } from '@kbn/i18n'; import { AggConfigs } from 'ui/agg_types/agg_configs'; import { createFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; import chrome from 'ui/chrome'; -import { TimeRange } from 'src/plugins/data/public'; + +import { Query, TimeRange, esFilters } from 'src/plugins/data/public'; import { SearchSource } from '../../../../ui/public/courier/search_source'; -import { FilterBarQueryFilterProvider } from '../../../../ui/public/filter_manager/query_filter'; +// @ts-ignore +import { + FilterBarQueryFilterProvider, + QueryFilter, +} from '../../../../ui/public/filter_manager/query_filter'; + import { buildTabularInspectorData } from '../../../../ui/public/inspector/build_tabular_inspector_data'; import { getRequestInspectorStats, @@ -32,15 +38,30 @@ import { } from '../../../../ui/public/courier/utils/courier_inspector_utils'; import { calculateObjectHash } from '../../../../ui/public/vis/lib/calculate_object_hash'; import { getTime } from '../../../../ui/public/timefilter'; -import { RequestHandlerParams } from '../../../../ui/public/visualize/loader/embedded_visualize_handler'; -import { KibanaContext, KibanaDatatable } from '../../common'; -import { ExpressionFunction, KibanaDatatableColumn } from '../../types'; -import { start as data } from '../../../data/public/legacy'; + +export interface RequestHandlerParams { + searchSource: SearchSource; + aggs: AggConfigs; + timeRange?: TimeRange; + query?: Query; + filters?: esFilters.Filter[]; + forceFetch: boolean; + queryFilter: QueryFilter; + uiState?: PersistedState; + partialRows?: boolean; + inspectorAdapters: Adapters; + metricsAtAllLevels?: boolean; + visParams?: any; + abortSignal?: AbortSignal; +} // @ts-ignore import { tabifyAggResponse } from '../../../../ui/public/agg_response/tabify/tabify'; -// @ts-ignore -import { SearchSourceProvider } from '../../../../ui/public/courier/search_source'; +import { KibanaContext, KibanaDatatable } from '../../common'; +import { ExpressionFunction, KibanaDatatableColumn } from '../../types'; +import { start as data } from '../../../data/public/legacy'; +import { PersistedState } from '../../../../ui/public/persisted_state'; +import { Adapters } from '../../../../../plugins/inspector/public'; const name = 'esaggs'; diff --git a/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts b/src/legacy/core_plugins/interpreter/public/renderers/visualization.tsx similarity index 72% rename from src/legacy/core_plugins/interpreter/public/renderers/visualization.ts rename to src/legacy/core_plugins/interpreter/public/renderers/visualization.tsx index bedba6bfacede..f15cdf23fe15b 100644 --- a/src/legacy/core_plugins/interpreter/public/renderers/visualization.ts +++ b/src/legacy/core_plugins/interpreter/public/renderers/visualization.tsx @@ -18,9 +18,11 @@ */ import chrome from 'ui/chrome'; -import { visualizationLoader } from 'ui/visualize/loader/visualization_loader'; +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; // @ts-ignore -import { VisProvider } from 'ui/visualize/loader/vis'; +import { Vis } from '../../../../ui/public/visualize/loader/vis'; +import { Visualization } from '../../../../ui/public/visualize/components'; export const visualization = () => ({ name: 'visualization', @@ -31,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 @@ -50,17 +50,27 @@ export const visualization = () => ({ type: visType, params: visConfig, }); - handlers.vis.eventsSubject = handlers.eventsSubject; } + handlers.vis.eventsSubject = { next: handlers.event }; + const uiState = handlers.uiState || handlers.vis.getUiState(); - handlers.onDestroy(() => visualizationLoader.destroy()); + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); - await visualizationLoader - .render(domNode, handlers.vis, visData, handlers.vis.params, uiState, params) - .then(() => { - if (handlers.done) handlers.done(); - }); + const listenOnChange = params ? params.listenOnChange : false; + render( + , + domNode + ); }, }); 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 c7ada18f9e1f2..2ca4ed1e2343d 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 24cd436912395..c7cda8aec0165 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.html b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html index 68c8131fa1a7b..f644f3811e3e0 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -42,13 +42,6 @@ index-patterns="indexPatterns" > - -

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 5fa3a938ed9df..656b54040ad99 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 adf0e1e084a64..d82b89339b0d0 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,15 +50,14 @@ 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 { extractTimeFilter, changeTimeFilter } from '../../../../../plugins/data/public'; +import { Query } from '../../../../../plugins/data/public'; import { start as data } from '../../../data/public/legacy'; -import { esFilters } from '../../../../../plugins/data/public'; import { DashboardContainer, @@ -417,31 +416,6 @@ export class DashboardAppController { queryFilter.setFilters(filters); }; - $scope.onCancelApplyFilters = () => { - $scope.appState.$newFilters = []; - }; - - $scope.onApplyFilters = filters => { - if (filters.length) { - // All filters originated from one visualization. - const indexPatternId = filters[0].meta.index; - const indexPattern = _.find( - $scope.indexPatterns, - (p: IndexPattern) => p.id === indexPatternId - ); - if (indexPattern && indexPattern.timeFieldName) { - const { timeRangeFilter, restOfFilters } = extractTimeFilter( - indexPattern.timeFieldName, - filters - ); - queryFilter.addFilters(restOfFilters); - if (timeRangeFilter) changeTimeFilter(timefilter, timeRangeFilter); - } - } - - $scope.appState.$newFilters = []; - }; - $scope.onQuerySaved = savedQuery => { $scope.savedQuery = savedQuery; }; @@ -514,12 +488,6 @@ export class DashboardAppController { } ); - $scope.$watch('appState.$newFilters', (filters: esFilters.Filter[] = []) => { - if (filters.length === 1) { - $scope.onApplyFilters(filters); - } - }); - $scope.indexPatterns = []; $scope.$watch('model.query', (newQuery: Query) => { 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 8ffabe5add1c3..1a42ed837a9de 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/listing/__snapshots__/dashboard_listing.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index e23102a0785fc..1ed05035f5f4c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`after fetch hideWriteControls 1`] = ` - `; exports[`after fetch initialFilter 1`] = ` - `; exports[`after fetch renders call to action when no dashboards exist 1`] = ` - `; exports[`after fetch renders table rows 1`] = ` - `; exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` - `; exports[`renders empty page in before initial fetch to avoid flickering 1`] = ` - `; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index d8216361562e2..c222fcd3c928c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -23,8 +23,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { npStart } from 'ui/new_platform'; -import { TableListView } from './../../table_list_view'; +import { TableListView } from '../../../../../../../src/plugins/kibana_react/public'; export const EMPTY_FILTER = ''; @@ -58,6 +59,8 @@ export class DashboardListing extends React.Component { tableListTitle={i18n.translate('kbn.dashboard.listing.dashboardsTitle', { defaultMessage: 'Dashboards', })} + toastNotifications={npStart.core.notifications.toasts} + uiSettings={npStart.core.uiSettings} /> ); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js b/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js index 57de395525e1b..be542c60bfe7a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/dashboard_listing.test.js @@ -42,6 +42,17 @@ jest.mock( { virtual: true } ); +jest.mock('ui/new_platform', () => { + return { + npStart: { + core: { + notifications: { toasts: { } }, + uiSettings: { get: jest.fn(() => 10) }, + }, + }, + }; +}); + import React from 'react'; import { shallow } from 'enzyme'; 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 8522495b9dedb..e82fc58670e39 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 5b860b0a2cc7c..5b24aa13f4f77 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 5aaca7b62094f..3c2c87a502da4 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 563b140fd2ead..2e88d2e1285e3 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 0000000000000..3945d8d8dc856 --- /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 25c7b945b9dfb..0000000000000 --- 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 e36e75f6837ab..0000000000000 --- 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 0000000000000..74708e36a98aa --- /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 6c076092c76d5..0000000000000 --- 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 0000000000000..ec9af1a6acd92 --- /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 f472ff9250eb5..b3d37083b37f7 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 b136b03bd500b..5a445a65939ed 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 9f7b180e8fe7d..10fe6c0e2eda1 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 ed5049aa912e0..8ee23bfb005a2 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 1f5db791469b9..0000000000000 --- 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 badfbb4b14a4c..5054f7b4bdad1 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 3d93487d9e6cc..d5f6b63d12199 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 732fb6d2e4e70..c575465a377e2 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 5473ec0e7b8b4..2d940ad8cba98 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 b78d05e68acad..d0eb115e32676 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/home/components/__snapshots__/home.test.js.snap b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap index 71c336b1d48d2..0bf8c808ae920 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/home.test.js.snap @@ -1072,10 +1072,8 @@ exports[`home welcome should show the normal home page if welcome screen is disa exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = ` `; diff --git a/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap new file mode 100644 index 0000000000000..5a6c6eba5c8db --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/components/__snapshots__/welcome.test.tsx.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render a Welcome screen with no telemetry disclaimer 1`] = ` + +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + + + + + +
+
+
+`; + +exports[`should render a Welcome screen with the telemetry disclaimer 1`] = ` + +
+
+
+ + + + + +

+ +

+
+ +

+ +

+
+ +
+
+
+ + + + + + + + + + + + + + + + + +
+
+
+`; diff --git a/src/legacy/core_plugins/kibana/public/home/components/home.js b/src/legacy/core_plugins/kibana/public/home/components/home.js index 7f67b7ea0f3e1..3266bbb79c625 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home.js @@ -51,6 +51,7 @@ export class Home extends Component { getServices().getInjected('disableWelcomeScreen') || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); + const showTelemetryDisclaimer = getServices().getInjected('allowChangingOptInStatus'); this.state = { // If welcome is enabled, we wait for loading to complete @@ -60,6 +61,7 @@ export class Home extends Component { isLoading: isWelcomeEnabled, isNewKibanaInstance: false, isWelcomeEnabled, + showTelemetryDisclaimer, }; } @@ -228,10 +230,7 @@ export class Home extends Component { ); } @@ -254,10 +253,6 @@ export class Home extends Component { Home.propTypes = { addBasePath: PropTypes.func.isRequired, - fetchTelemetry: PropTypes.func.isRequired, - getTelemetryBannerId: PropTypes.func.isRequired, - setOptIn: PropTypes.func.isRequired, - shouldShowTelemetryOptIn: PropTypes.bool, directories: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string.isRequired, diff --git a/src/legacy/core_plugins/kibana/public/home/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/components/home_app.js index e4a6753e0771a..f8476a0c09670 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/components/home_app.js @@ -31,8 +31,6 @@ import { getServices } from '../kibana_services'; export function HomeApp({ directories }) { const { - telemetryOptInProvider, - shouldShowTelemetryOptIn, getInjected, savedObjectsClient, getBasePath, @@ -85,10 +83,6 @@ export function HomeApp({ directories }) { find={savedObjectsClient.find} localStorage={localStorage} urlBasePath={getBasePath()} - shouldShowTelemetryOptIn={shouldShowTelemetryOptIn} - setOptIn={telemetryOptInProvider.setOptIn} - fetchTelemetry={telemetryOptInProvider.fetchExample} - getTelemetryBannerId={telemetryOptInProvider.getBannerId} /> diff --git a/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/telemetry_opt_in_card.tsx b/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/telemetry_opt_in_card.tsx deleted file mode 100644 index 572188d9c9b93..0000000000000 --- a/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/telemetry_opt_in_card.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - // @ts-ignore - EuiCard, - EuiButton, -} from '@elastic/eui'; -import { OptInMessage } from '../../../../../telemetry/public/components/opt_in_message'; - -export interface Props { - urlBasePath: string; - onConfirm: () => void; - onDecline: () => void; - fetchTelemetry: () => Promise; -} - -export function renderTelemetryOptInCard({ - urlBasePath, - fetchTelemetry, - onConfirm, - onDecline, -}: Props) { - return ( - - } - description={} - footer={ -
- - - - - - -
- } - /> - ); -} diff --git a/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx new file mode 100644 index 0000000000000..195a527707af6 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/components/welcome.test.tsx @@ -0,0 +1,52 @@ +/* + * 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 { shallow } from 'enzyme'; +import { Welcome } from './welcome'; + +jest.mock('../kibana_services', () => ({ + getServices: () => ({ + addBasePath: (path: string) => `root${path}`, + trackUiMetric: () => {}, + METRIC_TYPE: { + LOADED: 'loaded', + CLICK: 'click', + }, + }), +})); + +test('should render a Welcome screen with the telemetry disclaimer', () => { + const component = shallow( + // @ts-ignore + {}} showTelemetryDisclaimer={true} /> + ); + + expect(component).toMatchSnapshot(); +}); + +test('should render a Welcome screen with no telemetry disclaimer', () => { + // @ts-ignore + const component = shallow( + // @ts-ignore + {}} showTelemetryDisclaimer={false} /> + ); + + expect(component).toMatchSnapshot(); +}); diff --git a/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx index afe43a23e18cb..d919a4ecf239c 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/components/welcome.tsx @@ -25,6 +25,8 @@ import React from 'react'; import { + EuiLink, + EuiTextColor, EuiTitle, EuiSpacer, EuiFlexGroup, @@ -37,29 +39,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../kibana_services'; import { SampleDataCard } from './sample_data'; -import { TelemetryOptInCard } from './telemetry_opt_in'; interface Props { urlBasePath: string; - onSkip: () => {}; - fetchTelemetry: () => Promise; - setOptIn: (enabled: boolean) => Promise; - getTelemetryBannerId: () => string; - shouldShowTelemetryOptIn: boolean; -} - -interface State { - step: number; + onSkip: () => void; + showTelemetryDisclaimer: boolean; } /** * Shows a full-screen welcome page that gives helpful quick links to beginners. */ -export class Welcome extends React.PureComponent { +export class Welcome extends React.Component { private services = getServices(); - public readonly state: State = { - step: 0, - }; private hideOnEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') { @@ -72,19 +63,11 @@ export class Welcome extends React.PureComponent { window.location.href = path; } - private async handleTelemetrySelection(confirm: boolean) { - const metricName = `telemetryOptIn${confirm ? 'Confirm' : 'Decline'}`; - this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, metricName); - await this.props.setOptIn(confirm); - const bannerId = this.props.getTelemetryBannerId(); - this.services.banners.remove(bannerId); - this.setState(() => ({ step: 1 })); - } - private onSampleDataDecline = () => { this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, 'sampleDataDecline'); this.props.onSkip(); }; + private onSampleDataConfirm = () => { this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, 'sampleDataConfirm'); this.redirecToSampleData(); @@ -92,12 +75,6 @@ export class Welcome extends React.PureComponent { componentDidMount() { this.services.trackUiMetric(this.services.METRIC_TYPE.LOADED, 'welcomeScreenMount'); - if (this.props.shouldShowTelemetryOptIn) { - this.services.trackUiMetric( - this.services.METRIC_TYPE.COUNT, - 'welcomeScreenWithTelemetryOptIn' - ); - } document.addEventListener('keydown', this.hideOnEsc); } @@ -106,8 +83,7 @@ export class Welcome extends React.PureComponent { } render() { - const { urlBasePath, shouldShowTelemetryOptIn, fetchTelemetry } = this.props; - const { step } = this.state; + const { urlBasePath, showTelemetryDisclaimer } = this.props; return ( @@ -137,20 +113,39 @@ export class Welcome extends React.PureComponent {
- {shouldShowTelemetryOptIn && step === 0 && ( - - )} - {(!shouldShowTelemetryOptIn || step === 1) && ( - + + + {showTelemetryDisclaimer && ( + + + + + + + + + + )} diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index fe741a357cbfe..c5b9d86b57aae 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/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js index 39a9f7cd98a57..5956b6c306b0e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js @@ -106,10 +106,10 @@ const allSavedObjects = [ }, ]; -const $http = () => {}; +const $http = () => { }; $http.post = jest.fn().mockImplementation(() => []); const defaultProps = { - goInspectObject: () => {}, + goInspectObject: () => { }, confirmModalPromise: jest.fn(), savedObjectsClient: { find: jest.fn(), @@ -256,7 +256,6 @@ describe('ObjectsTable', () => { const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ _id: obj.id, - _type: obj._type, _source: {}, })); @@ -297,7 +296,6 @@ describe('ObjectsTable', () => { const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ _id: obj.id, - _type: obj._type, _source: {}, })); 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 a6ed2e36839f4..4ecc3583e76ce 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/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index f501161136801..58a0075e94b99 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -31,7 +31,7 @@ import editorTemplate from './editor.html'; import { DashboardConstants } from '../../dashboard/dashboard_constants'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; -import { extractTimeFilter, changeTimeFilter } from '../../../../../../plugins/data/public'; + import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { @@ -342,23 +342,6 @@ function VisEditor( queryFilter.setFilters(filters); }; - $scope.onCancelApplyFilters = () => { - $scope.state.$newFilters = []; - }; - - $scope.onApplyFilters = filters => { - const { timeRangeFilter, restOfFilters } = extractTimeFilter($scope.indexPattern.timeFieldName, filters); - queryFilter.addFilters(restOfFilters); - if (timeRangeFilter) changeTimeFilter(timefilter, timeRangeFilter); - $scope.state.$newFilters = []; - }; - - $scope.$watch('state.$newFilters', (filters = []) => { - if (filters.length === 1) { - $scope.onApplyFilters(filters); - } - }); - $scope.showSaveQuery = capabilities.visualize.saveQuery; $scope.$watch(() => capabilities.visualize.saveQuery, (newCapability) => { @@ -457,6 +440,12 @@ function VisEditor( next: $scope.fetch })); + subscriptions.add(subscribeWithScope($scope, timefilter.getAutoRefreshFetch$(), { + next: () => { + $scope.vis.forceReload(); + } + })); + $scope.$on('$destroy', function () { if ($scope._handler) { $scope._handler.destroy(); 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 318686b26f6f2..0b75c6ffa1ffb 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 @@ -17,39 +17,53 @@ * under the License. */ -import _ from 'lodash'; -import { EmbeddedVisualizeHandler } from 'ui/visualize/loader/embedded_visualize_handler'; +import _, { forEach } from 'lodash'; +import { StaticIndexPattern } from 'ui/index_patterns'; +import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; +import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; +import { SavedObject } from 'ui/saved_objects/saved_object'; +import { Vis } from 'ui/vis'; +import { SearchSource } from 'ui/courier'; +import { queryGeohashBounds } from 'ui/visualize/loader/utils'; +import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; +import { AppState } from 'ui/state_management/app_state'; +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 { TimeRange, + Query, onlyDisabledFiltersChanged, esFilters, + mapAndFlattenFilters, } from '../../../../../../plugins/data/public'; -import { Query } from '../../../../data/public'; -import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; - import { - AppState, - Container, - Embeddable, EmbeddableInput, EmbeddableOutput, - PersistedState, - StaticIndexPattern, - VisSavedObject, - VisualizeLoader, - VisualizeLoaderParams, - VisualizeUpdateParams, -} from '../kibana_services'; + Embeddable, + Container, + APPLY_FILTER_TRIGGER, +} from '../../../../../../plugins/embeddable/public'; +import { dispatchRenderComplete } from '../../../../../../plugins/kibana_utils/public'; const getKeys = (o: T): Array => Object.keys(o) as Array; +export interface VisSavedObject extends SavedObject { + vis: Vis; + description?: string; + searchSource: SearchSource; + title: string; + uiStateJSON?: string; + destroy: () => void; +} + export interface VisualizeEmbeddableConfiguration { savedVisualization: VisSavedObject; indexPatterns?: StaticIndexPattern[]; editUrl: string; - loader: VisualizeLoader; editable: boolean; appState?: AppState; uiState?: PersistedState; @@ -73,24 +87,28 @@ export interface VisualizeOutput extends EmbeddableOutput { visTypeName: string; } +type ExpressionLoader = InstanceType; + export class VisualizeEmbeddable extends Embeddable { + private handler?: ExpressionLoader; private savedVisualization: VisSavedObject; - private loader: VisualizeLoader; private appState: AppState | undefined; private uiState: PersistedState; - private handler?: EmbeddedVisualizeHandler; private timeRange?: TimeRange; private query?: Query; private title?: string; private filters?: esFilters.Filter[]; private visCustomizations: VisualizeInput['vis']; - private subscription: Subscription; + private subscriptions: Subscription[] = []; + private expression: string = ''; + private actions: any = {}; + private vis: Vis; + private domNode: any; public readonly type = VISUALIZE_EMBEDDABLE_TYPE; constructor( { savedVisualization, - loader, editUrl, indexPatterns, editable, @@ -112,8 +130,12 @@ export class VisualizeEmbeddable extends Embeddable { - this.handleChanges(); - }); + this.subscriptions.push( + Rx.merge(this.getOutput$(), this.getInput$()).subscribe(() => { + this.handleChanges(); + }) + ); } public getVisualizationDescription() { return this.savedVisualization.description; } - public getInspectorAdapters() { + public getInspectorAdapters = () => { if (!this.handler) { return undefined; } - return this.handler.inspectorAdapters; - } + return this.handler.inspect(); + }; + + public openInspector = () => { + if (this.handler) { + return this.handler.openInspector(this.getTitle() || ''); + } + }; /** * Transfers all changes in the containerState.customization into @@ -170,87 +202,148 @@ export class VisualizeEmbeddable extends Embeddable { + if (event.disabled || !eventName) { + return; + } else { + this.actions[eventName] = event.defaultAction; + } + }); + + // This is a hack to give maps visualizations access to data in the + // globalState, since they can no longer access it via searchSource. + // TODO: Remove this as a part of elastic/kibana#30593 + this.vis.API.getGeohashBounds = () => { + return queryGeohashBounds(this.savedVisualization.vis, { + filters: this.filters, + query: this.query, + searchSource: this.savedVisualization.searchSource, + }); }; + + // this is a hack to make editor still work, will be removed once we clean up editor + this.vis.hasInspector = () => { + const visTypesWithoutInspector = ['markdown', 'input_control_vis', 'metrics', 'vega']; + if (visTypesWithoutInspector.includes(this.vis.type.name)) { + return false; + } + return this.getInspectorAdapters(); + }; + + this.vis.openInspector = this.openInspector; + + const div = document.createElement('div'); + div.className = `visualize panel-content panel-content--fullWidth`; + domNode.appendChild(div); + this.domNode = div; + + this.handler = new expressions.ExpressionLoader(this.domNode); + + this.subscriptions.push( + this.handler.events$.subscribe(async event => { + if (this.actions[event.name]) { + event.data.aggConfigs = getTableAggs(this.vis); + const filters: esFilters.Filter[] = this.actions[event.name](event.data) || []; + const mappedFilters = mapAndFlattenFilters(filters); + const timeFieldName = this.vis.indexPattern.timeFieldName; + + npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + filters: mappedFilters, + timeFieldName, + }); + } + }) + ); + + div.setAttribute('data-title', this.output.title || ''); + if (this.savedVisualization.description) { - dataAttrs.description = this.savedVisualization.description; + div.setAttribute('data-description', this.savedVisualization.description); } - const handlerParams: VisualizeLoaderParams = { - appState: this.appState, - uiState: this.uiState, - // Append visualization to container instead of replacing its content - append: true, - timeRange: _.cloneDeep(this.input.timeRange), - query: this.query, - filters: this.filters, - cssClass: `panel-content panel-content--fullWidth`, - dataAttrs, - }; + div.setAttribute('data-test-subj', 'visualizationLoader'); + div.setAttribute('data-shared-item', ''); + div.setAttribute('data-rendering-count', '0'); + div.setAttribute('data-render-complete', 'false'); + + this.subscriptions.push( + this.handler.loading$.subscribe(() => { + div.setAttribute('data-render-complete', 'false'); + div.setAttribute('data-loading', ''); + }) + ); - this.handler = this.loader.embedVisualizationWithSavedObject( - domNode, - this.savedVisualization, - handlerParams + this.subscriptions.push( + this.handler.render$.subscribe(count => { + div.removeAttribute('data-loading'); + div.setAttribute('data-render-complete', 'true'); + div.setAttribute('data-rendering-count', count.toString()); + dispatchRenderComplete(div); + }) ); + + this.updateHandler(); } public destroy() { super.destroy(); - if (this.subscription) { - this.subscription.unsubscribe(); - } + this.subscriptions.forEach(s => s.unsubscribe()); this.uiState.off('change', this.uiStateChangeHandler); + this.savedVisualization.vis.removeListener('reload', this.reload); + this.savedVisualization.vis.removeListener('update', this.handleVisUpdate); this.savedVisualization.destroy(); if (this.handler) { this.handler.destroy(); @@ -258,12 +351,44 @@ export class VisualizeEmbeddable extends Embeddable { + this.handleVisUpdate(); + }; + + private async updateHandler() { + const expressionParams: IExpressionLoaderParams = { + searchContext: { + type: 'kibana_context', + timeRange: this.timeRange, + query: this.input.query, + filters: this.input.filters, + }, + extraHandlers: { + vis: this.vis, + uiState: this.uiState, + }, + }; + this.expression = await buildPipeline(this.vis, { + searchSource: this.savedVisualization.searchSource, + timeRange: this.timeRange, + }); + + this.vis.filters = { timeRange: this.timeRange }; + if (this.handler) { - this.handler.reload(); + this.handler.update(this.expression, expressionParams); } } + private handleVisUpdate = async () => { + if (this.appState) { + this.appState.vis = this.savedVisualization.vis.getState(); + this.appState.save(); + } + + this.updateHandler(); + }; + private uiStateChangeHandler = () => { this.updateInput({ ...this.uiState.toJSON(), diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx index c1ce4f67cfdb3..15ad9a33232ef 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable_factory.tsx @@ -36,7 +36,6 @@ import { EmbeddableFactory, EmbeddableOutput, ErrorEmbeddable, - getVisualizeLoader, VisSavedObject, } from '../kibana_services'; @@ -131,7 +130,6 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< const visId = savedObject.id as string; const editUrl = visId ? addBasePath(`/app/kibana${savedVisualizations.urlFor(visId)}`) : ''; - const loader = await getVisualizeLoader(); const isLabsEnabled = config.get('visualize:enableLabs'); if (!isLabsEnabled && savedObject.vis.type.stage === 'experimental') { @@ -143,7 +141,6 @@ export class VisualizeEmbeddableFactory extends EmbeddableFactory< return new VisualizeEmbeddable( { savedVisualization: savedObject, - loader, indexPatterns, editUrl, editable: this.isEditable(), diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index 7e8435bbdc65e..5c6d06b5eaeb6 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -42,7 +42,7 @@ import { timefilter } from 'ui/timefilter'; // Saved objects import { SavedObjectsClientProvider } from 'ui/saved_objects'; // @ts-ignore -import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; +import { SavedObject, SavedObjectProvider } from 'ui/saved_objects/saved_object'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; @@ -105,7 +105,6 @@ export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; export { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; export { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; -export { getVisualizeLoader } from 'ui/visualize/loader'; export { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; export { Container, @@ -121,12 +120,8 @@ export { METRIC_TYPE }; export { StaticIndexPattern } from 'ui/index_patterns'; export { AppState } from 'ui/state_management/app_state'; export { VisType } from 'ui/vis'; -export { VisualizeLoader } from 'ui/visualize/loader'; -export { - VisSavedObject, - VisualizeLoaderParams, - VisualizeUpdateParams, -} from 'ui/visualize/loader/types'; // export const export { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; + +export { VisSavedObject } from './embeddable/visualize_embeddable'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js index fbd70a0d8c0f7..efab03303aa80 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js +++ b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing_table.js @@ -21,13 +21,13 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { TableListView } from './../../table_list_view'; +import { TableListView } from '../../../../../../../src/plugins/kibana_react/public'; import { EuiIcon, EuiBetaBadge, EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { getServices } from '../kibana_services'; -const { capabilities } = getServices(); +const { capabilities, toastNotifications, uiSettings } = getServices(); class VisualizeListingTable extends Component { constructor(props) { @@ -57,6 +57,8 @@ class VisualizeListingTable extends Component { tableListTitle={i18n.translate('kbn.visualize.listing.table.listTitle', { defaultMessage: 'Visualizations', })} + toastNotifications={toastNotifications} + uiSettings={uiSettings} /> ); } 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 f8adaed0bf584..aec80b8d13551 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/kibana/server/lib/kql_usage_collector/fetch.test.js b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/fetch.test.js index e8e95fd92dd11..a5db4602872ee 100644 --- a/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/fetch.test.js +++ b/src/legacy/core_plugins/kibana/server/lib/kql_usage_collector/fetch.test.js @@ -32,7 +32,6 @@ function setupMockCallCluster(optCount, language) { if (optCount === null) { return Promise.resolve({ _index: '.kibana_1', - _type: 'doc', _id: 'kql-telemetry:kql-telemetry', found: false, }); diff --git a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js index a2cc63b4b8679..6926fa72b0ae2 100644 --- a/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js +++ b/src/legacy/core_plugins/kibana/server/routes/api/management/saved_objects/scroll.js @@ -53,10 +53,8 @@ export function registerScrollForExportRoute(server) { }); return objects.map(hit => { - const type = hit.type; return { _id: hit.id, - _type: type, _source: hit.attributes, _meta: { savedObjectVersion: 2 diff --git a/src/legacy/core_plugins/newsfeed/constants.ts b/src/legacy/core_plugins/newsfeed/constants.ts new file mode 100644 index 0000000000000..55a0c51c2ac65 --- /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 0000000000000..cf8852be09a1e --- /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 0000000000000..d4d753f32b0f9 --- /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 0000000000000..a77132379041c --- /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 0000000000000..e25dbd25daaf5 --- /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 0000000000000..c3c3e4144fca8 --- /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 9bfa413257967..b57fbd637f0b7 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 9749c7fa8e2f9..8306b3274a914 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 ab1397b2cc232..7b0c62276f290 100644 --- a/src/legacy/core_plugins/telemetry/common/constants.ts +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -51,7 +51,7 @@ export const LOCALSTORAGE_KEY = 'telemetry.data'; /** * Link to the Elastic Telemetry privacy statement. */ -export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/telemetry-privacy-statement`; +export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/privacy-statement`; /** * The type name used within the Monitoring index to publish localization stats. @@ -59,6 +59,12 @@ export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/telemetry-pri */ 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 4b6566415f3e1..9993f2dbf0b86 100644 --- a/src/legacy/core_plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -17,6 +17,7 @@ * under the License. */ +import * as Rx from 'rxjs'; import { resolve } from 'path'; import JoiNamespace from 'joi'; import { Server } from 'hapi'; @@ -26,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'; @@ -45,12 +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).default(true), + otherwise: 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( @@ -60,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: { @@ -80,31 +97,48 @@ const telemetry = (kibana: any) => { }, }, async replaceInjectedVars(originalInjectedVars: any, request: any) { - const currentKibanaVersion = getCurrentKibanaVersion(request.server); - const telemetryOptedIn = await getTelemetryOptIn({ request, currentKibanaVersion }); - - return { - ...originalInjectedVars, - telemetryOptedIn, - }; + const telemetryInjectedVars = await replaceTelemetryInjectedVars(request); + return Object.assign({}, originalInjectedVars, telemetryInjectedVars); }, injectDefaultVars(server: Server) { const config = server.config(); return { telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'), - telemetryBanner: getXpackConfigWithDeprecated(config, 'telemetry.banner'), - telemetryOptedIn: null, + telemetryBanner: + 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, }, + 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: { + create() { + const config = server.config(); + return Rx.of({ + enabled: config.get('telemetry.enabled'), + optIn: config.get('telemetry.optIn'), + config: config.get('telemetry.config'), + banner: config.get('telemetry.banner'), + url: config.get('telemetry.url'), + allowChangingOptInStatus: config.get('telemetry.allowChangingOptInStatus'), + }); }, }, } as PluginInitializerContext; @@ -115,8 +149,8 @@ const telemetry = (kibana: any) => { } as any) as 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)); @@ -126,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 1245ef88f5892..95c6ebfc7dc79 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__/opt_in_message.test.tsx.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap new file mode 100644 index 0000000000000..c80485332fa8a --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_message.test.tsx.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OptInMessage renders as expected 1`] = ` + + + + , + } + } + /> + +`; 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 c1ad6276aee25..a7f8d72e016f8 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 @@ -1,6 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TelemetryForm renders as expected 1`] = ` +exports[`TelemetryForm doesn't render form when not allowed to change optIn status 1`] = `""`; + +exports[`TelemetryForm renders as expected when allows to change optIn status 1`] = `

Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic. @@ -50,7 +53,7 @@ exports[`TelemetryForm renders as expected 1`] = `

{ const title = ( ); return ( @@ -45,12 +45,18 @@ export class OptInBanner extends React.PureComponent { this.props.optInClick(true)}> - + this.props.optInClick(false)}> - + diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx b/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx new file mode 100644 index 0000000000000..1a9fabceda907 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/components/opt_in_message.test.tsx @@ -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. + */ +import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { OptInMessage } from './opt_in_message'; + +describe('OptInMessage', () => { + it('renders as expected', () => { + expect( + shallowWithIntl( [])} />) + ).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx b/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx index 928bb1015b715..4221d78516e10 100644 --- a/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx +++ b/src/legacy/core_plugins/telemetry/public/components/opt_in_message.tsx @@ -21,8 +21,7 @@ import * as React from 'react'; import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getConfigTelemetryDesc, PRIVACY_STATEMENT_URL } from '../../common/constants'; -import { OptInExampleFlyout } from './opt_in_details_component'; +import { PRIVACY_STATEMENT_URL } from '../../common/constants'; interface Props { fetchTelemetry: () => Promise; @@ -46,60 +45,22 @@ export class OptInMessage extends React.PureComponent { }; render() { - const { showDetails, showExample } = this.state; - - const getDetails = () => ( - - - - ), - telemetryPrivacyStatementLink: ( - - - - ), - }} - /> - ); - - const getFlyoutDetails = () => ( - this.setState({ showExample: false })} - fetchTelemetry={this.props.fetchTelemetry} - /> - ); - - const getReadMore = () => ( - this.setState({ showDetails: true })}> - - - ); - return ( - {getConfigTelemetryDesc()} {!showDetails && getReadMore()} - {showDetails && ( - - {getDetails()} - {showExample && getFlyoutDetails()} - - )} + + + + ), + }} + /> ); } 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 c2dcd48ee57da..6c6ace71af4d0 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']; @@ -78,6 +79,10 @@ export class TelemetryForm extends Component { queryMatches, } = this.state; + if (!telemetryOptInProvider.canChangeOptInStatus()) { + return null; + } + if (queryMatches !== null && !queryMatches) { return null; } @@ -112,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/components/telemetry_form.test.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js index 4d2c1dec27176..836fbc5d914de 100644 --- a/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js +++ b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js @@ -17,7 +17,7 @@ * under the License. */ -import '../services/telemetry_opt_in.test.mocks'; +import { mockInjectedMetadata } from '../services/telemetry_opt_in.test.mocks'; import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { TelemetryForm } from './telemetry_form'; @@ -33,6 +33,8 @@ const buildTelemetryOptInProvider = () => { switch (key) { case '$http': return mockHttp; + case 'allowChangingOptInStatus': + return true; default: return null; } @@ -47,7 +49,23 @@ const buildTelemetryOptInProvider = () => { }; describe('TelemetryForm', () => { - it('renders as expected', () => { + it('renders as expected when allows to change optIn status', () => { + mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); + + expect(shallowWithIntl( + ) + ).toMatchSnapshot(); + }); + + it(`doesn't render form when not allowed to change optIn status`, () => { + mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: false }); + expect(shallowWithIntl( url }; - const provider = new TelemetryOptInProvider(injector, chrome); + const provider = new TelemetryOptInProvider(injector, chrome, false); if (simulateError) { provider.setOptIn = () => Promise.reject('unhandled error'); @@ -72,7 +72,7 @@ describe('click_banner', () => { const optIn = true; const bannerId = 'bruce-banner'; - mockInjectedMetadata({ telemetryOptedIn: optIn }); + mockInjectedMetadata({ telemetryOptedIn: optIn, allowChangingOptInStatus: true }); const telemetryOptInProvider = getTelemetryOptInProvider(); telemetryOptInProvider.setBannerId(bannerId); @@ -92,7 +92,7 @@ describe('click_banner', () => { remove: sinon.spy() }; const optIn = true; - mockInjectedMetadata({ telemetryOptedIn: null }); + mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); const telemetryOptInProvider = getTelemetryOptInProvider({ simulateFailure: true }); await clickBanner(telemetryOptInProvider, optIn, { _banners: banners, _toastNotifications: toastNotifications }); @@ -110,7 +110,7 @@ describe('click_banner', () => { remove: sinon.spy() }; const optIn = false; - mockInjectedMetadata({ telemetryOptedIn: null }); + mockInjectedMetadata({ telemetryOptedIn: null, allowChangingOptInStatus: true }); const telemetryOptInProvider = getTelemetryOptInProvider({ simulateError: true }); await clickBanner(telemetryOptInProvider, optIn, { _banners: banners, _toastNotifications: toastNotifications }); 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 31091e1952053..4f0f2983477e0 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 fd21a5122b594..d78a4a3e92362 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 @@ -38,7 +38,7 @@ const getTelemetryOptInProvider = (enabled, { simulateFailure = false } = {}) => const chrome = { addBasePath: url => url }; - mockInjectedMetadata({ telemetryOptedIn: enabled }); + mockInjectedMetadata({ telemetryOptedIn: enabled, allowChangingOptInStatus: true }); const $injector = { get: (key) => { @@ -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/hacks/welcome_banner/should_show_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js index 19e7ccbe61866..240c991a75b64 100644 --- a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js @@ -38,7 +38,7 @@ const getMockInjector = () => { }; const getTelemetryOptInProvider = ({ telemetryOptedIn = null } = {}) => { - mockInjectedMetadata({ telemetryOptedIn }); + mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus: true }); const injector = getMockInjector(); const chrome = { addBasePath: (url) => url 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 0034fa4438238..b0ebb9e7382f6 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 @@ -34,7 +34,7 @@ describe('TelemetryOptInProvider', () => { addBasePath: (url) => url }; - mockInjectedMetadata({ telemetryOptedIn: optedIn }); + mockInjectedMetadata({ telemetryOptedIn: optedIn, allowChangingOptInStatus: true }); const mockInjector = { get: (key) => { @@ -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.test.mocks.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js index f98f5e16e00c3..012f8de640042 100644 --- a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js @@ -24,10 +24,11 @@ import { } from '../../../../../core/public/mocks'; const injectedMetadataMock = injectedMetadataServiceMock.createStartContract(); -export function mockInjectedMetadata({ telemetryOptedIn }) { +export function mockInjectedMetadata({ telemetryOptedIn, allowChangingOptInStatus }) { const mockGetInjectedVar = jest.fn().mockImplementation((key) => { switch (key) { case 'telemetryOptedIn': return telemetryOptedIn; + case 'allowChangingOptInStatus': return allowChangingOptInStatus; default: throw new Error(`unexpected injectedVar ${key}`); } }); 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 f4462ffea7a33..9b32f88df1218 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,22 +26,61 @@ 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' + ) as boolean; setCanTrackUiMetrics(currentOptInStatus); const provider = { getBannerId: () => bannerId, getOptIn: () => currentOptInStatus, + canChangeOptInStatus: () => allowChangingOptInStatus, setBannerId(id: string) { 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 fef0a9b0f9f40..799d9f4ee9c8b 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 0bc1d50fab1be..f963ecec0477c 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/visualize/loader/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts similarity index 90% rename from src/legacy/ui/public/visualize/loader/index.ts rename to src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts index 0ebe8e3a2300f..e96c47741f79c 100644 --- a/src/legacy/ui/public/visualize/loader/index.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/telemetry_plugin/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './visualize_loader'; +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 0000000000000..a172ba7dc6955 --- /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 6594c7f8e7a6f..3b7a9355da746 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 0000000000000..9edd8457f2b89 --- /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 aa13fab9a5f81..02752ca773488 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 a5f0f1234799a..f2628090c08af 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; @@ -31,7 +30,7 @@ export class TelemetryPlugin { 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 2eb6bf95b4f45..66a7b2c97f3ae 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) { - registerOptInRoutes({ core, currentKibanaVersion }); - registerTelemetryDataRoutes(core); + 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 3a7194890b570..0000000000000 --- 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 0000000000000..596c5c17c353e --- /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 0000000000000..d3bf6dbb77d7a --- /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 8a91d24b34ed2..c14314ca4da24 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 9ca609cd88778..d60b330db7b5b 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 d0de9cc365a71..4cbdf18df4a74 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 a840c39812e2c..4abd95f0cf66d 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 67fc721306c21..e11c6b1277d5b 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 024272e0f805c..0000000000000 --- 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 f33727d82f44c..7f228dbc5e6f6 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 0000000000000..faf8e9de79194 --- /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 0000000000000..9fa4fbc5e0227 --- /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 67ad3aaae427d..efc4a020e0ff0 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 c8bd4a4b6dfbd..d83ffdf69b576 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 0000000000000..69868a97a931d --- /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 0000000000000..9e4ae14b6097c --- /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 0000000000000..ab30dac1c3666 --- /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 0000000000000..90d1f9cfdac65 --- /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 0000000000000..7cc177878de4d --- /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 0000000000000..91965ef201ecb --- /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 0000000000000..f3629abc1620c --- /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 4dc55194562e5..b66e01faaa6bc 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 751f92fe88215..0e3c4fdd9d355 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/tile_map/public/tile_map_visualization.js b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js index ca798b6bf2470..560a5c93c938c 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js @@ -79,6 +79,7 @@ export const createTileMapVisualization = ({ serviceSettings, $injector }) => { return; } if (precisionChange) { + updateGeohashAgg(); this.vis.updateState(); } else { //when we filter queries by collar 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 6239e4027c392..35dea4a0deb9b 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 6c957f23b5c40..964e3497accba 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 7aafc82cfe4c6..983434f09922b 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 b1e78b56d05d0..5c327234b1e7c 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 7310ee5b5f172..ee928b8a1d9ee 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 8a7950c46fa31..e2de23ea806e4 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 7f626df6a4ea3..384beb3764e2e 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 abebf8190dc9f..4153ce2da36a7 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 d22ff92c4d3f6..13e8a4fd9535a 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 e8359e37d6186..f4e3a8e36605c 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 69d08d8cb3f74..0cb903faac47c 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 842d3aa6c4ad7..2b42c22ad7c43 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_timeseries/public/components/vis_editor_visualization.js b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor_visualization.js index f873cf9c178f8..ae82dc41fa9bc 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor_visualization.js +++ b/src/legacy/core_plugins/vis_type_timeseries/public/components/vis_editor_visualization.js @@ -75,13 +75,13 @@ class VisEditorVisualizationUI extends Component { this._handler = await embeddables.getEmbeddableFactory('visualization').createFromObject(savedObj, { vis: {}, timeRange: timeRange, - filters: appState.filters || [], + filters: appState ? appState.filters || [] : [], }); - this._handler.render(this._visEl.current); + await this._handler.render(this._visEl.current); this._subscription = this._handler.handler.data$.subscribe(data => { - this.setPanelInterval(data.visData); - onDataChange(data); + this.setPanelInterval(data.value.visData); + onDataChange(data.value); }); } 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 46725a2c5d01f..191f35d2e03ea 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 b4c32f37eb90c..accc52c1e5a14 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 ad86c9ddb14c5..ca79f547890f9 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 c14daa37f5706..dfd36f2aa7b2b 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 27b390ed7e471..39303e94adc6f 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 fd8df904b600f..dabd66a22fcd2 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 09668c638d695..001132f97c95a 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 dcadeacb3d1fe..759a6b80d8faf 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 675d37d05c33c..7c0245f30a1fd 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 44a97abb7a1d7..a8d509d507c6b 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 5c599f16e09c2..effa49f0ade6b 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 f7cae60cce773..1b423e64c48ae 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 348fccdab3fe3..230675ab487ad 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 45f350ea6adc8..e6b5f3831e65d 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 66bc205cead13..c1f5528825bcc 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 f3882ca57161f..7461b5cf07ee7 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 1503f43b22dc3..c9f4bcc3862a0 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 ae09b5cd78977..3125026a52185 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/courier/search_strategy/default_search_strategy.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js index 7d9865c137e62..42a9b64136454 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.js @@ -66,10 +66,9 @@ function search({ searchRequests, es, config, esShardTimeout }) { const abortController = new AbortController(); const searchParams = getSearchParams(config, esShardTimeout); const promises = searchRequests.map(({ index, body }) => { - const searching = es.search({ index: index.title || index, body, ...searchParams }) - .catch(({ response }) => JSON.parse(response)); + const searching = es.search({ index: index.title || index, body, ...searchParams }); abortController.signal.addEventListener('abort', searching.abort); - return searching; + return searching.catch(({ response }) => JSON.parse(response)); }); return { searching: Promise.all(promises), diff --git a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js index 953ca4fe800f1..a1ea53e8b5b47 100644 --- a/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js +++ b/src/legacy/ui/public/courier/search_strategy/default_search_strategy.test.js @@ -27,16 +27,29 @@ function getConfigStub(config = {}) { }; } +const msearchMockResponse = Promise.resolve([]); +msearchMockResponse.abort = jest.fn(); +const msearchMock = jest.fn().mockReturnValue(msearchMockResponse); + +const searchMockResponse = Promise.resolve([]); +searchMockResponse.abort = jest.fn(); +const searchMock = jest.fn().mockReturnValue(searchMockResponse); + describe('defaultSearchStrategy', function () { describe('search', function () { let searchArgs; beforeEach(() => { - const msearchMock = jest.fn().mockReturnValue(Promise.resolve([])); - const searchMock = jest.fn().mockReturnValue(Promise.resolve([])); + msearchMockResponse.abort.mockClear(); + msearchMock.mockClear(); + + searchMockResponse.abort.mockClear(); + searchMock.mockClear(); searchArgs = { - searchRequests: [], + searchRequests: [{ + index: { title: 'foo' } + }], es: { msearch: msearchMock, search: searchMock, @@ -73,5 +86,21 @@ describe('defaultSearchStrategy', function () { await search(searchArgs); expect(searchArgs.es.msearch.mock.calls[0][0]).toHaveProperty('ignore_throttled', false); }); + + test('should properly call abort with msearch', () => { + searchArgs.config = getConfigStub({ + 'courier:batchSearches': true + }); + search(searchArgs).abort(); + expect(msearchMockResponse.abort).toHaveBeenCalled(); + }); + + test('should properly abort with search', async () => { + searchArgs.config = getConfigStub({ + 'courier:batchSearches': false + }); + search(searchArgs).abort(); + expect(searchMockResponse.abort).toHaveBeenCalled(); + }); }); }); 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 5b6455bf20847..0000000000000 --- 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 e11e0ff6653a7..0000000000000 --- 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 6adc4e0965ccd..ce99d4cac3017 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 85c07cb3b1df1..2dd3f370c6d6a 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 67c370cad82a5..3b4952ac81519 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 58b8422cb2f8a..788718e848430 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 0c7b28e7da3df..9ee5d8580a90b 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 06e819d6a64a2..8d9b50d5a66b2 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 46d7ed4601f29..2e2e0c31bdb8a 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 96e4aa943ed1d..3240bd56f1502 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 5a2f93ab35b50..1d5e2de6dafe3 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 7806b1c0f78fb..661ece5944fa3 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 8e8926f027cad..980889743c20d 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 5fb241714b2e8..eb6bef4887642 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 3b6aebe8c2b0c..53f74465e90a5 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 3928c0a62eeaa..c6772cc108762 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 4ebe7b0d835d7..e847a95ead478 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 fe72e8dddd68d..aa654d26a23fd 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 a5fc9682bd954..de675386d9100 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 6da32690912e7..932a4d19b495c 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/default.html b/src/legacy/ui/public/vis/editors/default/default.html index 3e7a94c77ac42..2a759815f57f2 100644 --- a/src/legacy/ui/public/vis/editors/default/default.html +++ b/src/legacy/ui/public/vis/editors/default/default.html @@ -11,11 +11,6 @@

diff --git a/src/legacy/ui/public/vis/editors/default/utils.tsx b/src/legacy/ui/public/vis/editors/default/utils.tsx index efc424488ec41..4f82298aaca41 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 5a2a7edb6329a..791ce2563e0f1 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 711b465a1e8a5..05cd030f7d100 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 7571547ecf0d4..a9a636ab9e4cf 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 be73cccf06a59..e16562641801e 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 c1fff1556e3ad..304289a5cfa07 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 18eef99f05a75..afb3fea15a430 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 a4f62e6ceb431..5692ae65af281 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/components/visualization_chart.test.js b/src/legacy/ui/public/visualize/components/visualization_chart.test.js index 280370cdfe995..09d24ab71097a 100644 --- a/src/legacy/ui/public/visualize/components/visualization_chart.test.js +++ b/src/legacy/ui/public/visualize/components/visualization_chart.test.js @@ -57,46 +57,10 @@ describe('', () => { expect(wrapper.text()).toBe('Test Visualization visualization, not yet accessible'); }); - it('should emit render start and render end', async () => { - const renderStart = jest.fn(); - const renderComplete = jest.fn(); - const domNode = document.createElement('div'); - domNode.addEventListener('renderStart', renderStart); - domNode.addEventListener('renderComplete', renderComplete); - - mount(, { - attachTo: domNode - }); - - jest.runAllTimers(); - await renderPromise; - expect(renderStart).toHaveBeenCalledTimes(1); - expect(renderComplete).toHaveBeenCalledTimes(1); - - }); - it('should render visualization', async () => { const wrapper = mount(); jest.runAllTimers(); await renderPromise; expect(wrapper.find('.visChart').text()).toMatch(/markdown/); }); - - it('should re-render on param change', async () => { - const renderComplete = jest.fn(); - const wrapper = mount(); - const domNode = wrapper.getDOMNode(); - domNode.addEventListener('renderComplete', renderComplete); - jest.runAllTimers(); - await renderPromise; - expect(renderComplete).toHaveBeenCalledTimes(1); - - vis.params.markdown = 'new text'; - wrapper.setProps({ vis }); - jest.runAllTimers(); - await renderPromise; - - expect(wrapper.find('.visChart').text()).toBe('new text'); - expect(renderComplete).toHaveBeenCalledTimes(2); - }); }); diff --git a/src/legacy/ui/public/visualize/components/visualization_chart.tsx b/src/legacy/ui/public/visualize/components/visualization_chart.tsx index 06e44a4fd6e1c..eb7f130ec1a54 100644 --- a/src/legacy/ui/public/visualize/components/visualization_chart.tsx +++ b/src/legacy/ui/public/visualize/components/visualization_chart.tsx @@ -19,13 +19,9 @@ import React from 'react'; import * as Rx from 'rxjs'; -import { debounceTime, filter, share, switchMap, tap } from 'rxjs/operators'; +import { debounceTime, filter, share, switchMap } from 'rxjs/operators'; import { PersistedState } from '../../persisted_state'; -import { - dispatchRenderComplete, - dispatchRenderStart, -} from '../../../../../plugins/kibana_utils/public'; import { ResizeChecker } from '../../resize_checker'; import { Vis, VisualizationController } from '../../vis'; import { getUpdateStatus } from '../../vis/update_status'; @@ -59,11 +55,6 @@ class VisualizationChart extends React.Component { const render$ = this.renderSubject.asObservable().pipe(share()); const success$ = render$.pipe( - tap(() => { - if (this.chartDiv.current) { - dispatchRenderStart(this.chartDiv.current); - } - }), filter( ({ vis, visData, container }) => vis && container && (!vis.type.requiresSearch || visData) ), @@ -85,8 +76,8 @@ class VisualizationChart extends React.Component { const requestError$ = render$.pipe(filter(({ vis }) => vis.requestError)); this.renderSubscription = Rx.merge(success$, requestError$).subscribe(() => { - if (this.chartDiv.current !== null) { - dispatchRenderComplete(this.chartDiv.current); + if (this.props.onInit) { + this.props.onInit(); } }); } @@ -111,19 +102,11 @@ class VisualizationChart extends React.Component { throw new Error('chartDiv and currentDiv reference should always be present.'); } - const { vis, onInit } = this.props; + const { vis } = this.props; const Visualization = vis.type.visualization; this.visualization = new Visualization(this.chartDiv.current, vis); - if (onInit) { - // In case the visualization implementation has an isLoaded function, we - // call that and wait for the result to resolve (in case it was a promise). - const visLoaded = - this.visualization && this.visualization.isLoaded && this.visualization.isLoaded(); - Promise.resolve(visLoaded).then(onInit); - } - // We know that containerDiv.current will never be null, since we will always // have rendered and the div is always rendered into the tree (i.e. not // inside any condition). diff --git a/src/legacy/ui/public/visualize/loader/__snapshots__/embedded_visualize_handler.test.ts.snap b/src/legacy/ui/public/visualize/loader/__snapshots__/embedded_visualize_handler.test.ts.snap deleted file mode 100644 index 6650731942e7e..0000000000000 --- a/src/legacy/ui/public/visualize/loader/__snapshots__/embedded_visualize_handler.test.ts.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EmbeddedVisualizeHandler data$ observable can be used to get response data in the correct format 1`] = ` -Object { - "params": Object {}, - "visConfig": Object {}, - "visData": Object {}, - "visType": "histogram", -} -`; - -exports[`EmbeddedVisualizeHandler update should add provided data- attributes to the html element 1`] = ` -
-`; - -exports[`EmbeddedVisualizeHandler update should remove null data- attributes from the html element 1`] = ` -
-`; diff --git a/src/legacy/ui/public/visualize/loader/__tests__/visualization_loader.js b/src/legacy/ui/public/visualize/loader/__tests__/visualization_loader.js deleted file mode 100644 index ffce391fc1a07..0000000000000 --- a/src/legacy/ui/public/visualize/loader/__tests__/visualization_loader.js +++ /dev/null @@ -1,56 +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 'jquery'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import { setupAndTeardownInjectorStub } from 'test_utils/stub_get_active_injector'; - -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -import { VisProvider } from '../../../vis'; -import { visualizationLoader } from '../visualization_loader'; - -describe('visualization loader', () => { - let vis; - - beforeEach(ngMock.module('kibana', 'kibana/directive')); - beforeEach(ngMock.inject((_$rootScope_, savedVisualizations, Private) => { - const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - // Create a new Vis object - const Vis = Private(VisProvider); - vis = new Vis(indexPattern, { - type: 'markdown', - params: { markdown: 'this is test' }, - }); - - })); - setupAndTeardownInjectorStub(); - - it('should render visualization', async () => { - const element = document.createElement('div'); - expect(visualizationLoader.render).to.be.a('function'); - visualizationLoader.render(element, vis, null, vis.params); - expect($(element).find('.visualization').length).to.be(1); - }); - - -}); diff --git a/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js b/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js deleted file mode 100644 index 3fff184ffd199..0000000000000 --- a/src/legacy/ui/public/visualize/loader/__tests__/visualize_loader.js +++ /dev/null @@ -1,478 +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 angular from 'angular'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import sinon from 'sinon'; -import { cloneDeep } from 'lodash'; - -import { setupAndTeardownInjectorStub } from 'test_utils/stub_get_active_injector'; - -import FixturesStubbedSearchSourceProvider from 'fixtures/stubbed_search_source'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -import { VisProvider } from '../../../vis'; -import { getVisualizeLoader } from '../visualize_loader'; -import { EmbeddedVisualizeHandler } from '../embedded_visualize_handler'; -import { Inspector } from '../../../inspector/inspector'; -import { dispatchRenderComplete } from '../../../../../../plugins/kibana_utils/public'; -import { PipelineDataLoader } from '../pipeline_data_loader'; -import { PersistedState } from '../../../persisted_state'; -import { DataAdapter, RequestAdapter } from '../../../inspector/adapters'; - -describe('visualize loader', () => { - - let DataLoader; - let searchSource; - let vis; - let $rootScope; - let loader; - let mockedSavedObject; - let sandbox; - - function createSavedObject() { - return { - vis, - searchSource, - }; - } - - async function timeout(delay = 0) { - return new Promise(resolve => { - setTimeout(resolve, delay); - }); - } - - function newContainer() { - return angular.element('
'); - } - - function embedWithParams(params) { - const container = newContainer(); - loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), params); - $rootScope.$digest(); - return container.find('[data-test-subj="visualizationLoader"]'); - } - - beforeEach(ngMock.module('kibana', 'kibana/directive')); - beforeEach(ngMock.inject((_$rootScope_, savedVisualizations, Private) => { - $rootScope = _$rootScope_; - searchSource = Private(FixturesStubbedSearchSourceProvider); - const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - DataLoader = PipelineDataLoader; - // Create a new Vis object - const Vis = Private(VisProvider); - vis = new Vis(indexPattern, { - type: 'pie', - title: 'testVis', - params: {}, - aggs: [ - { type: 'count', schema: 'metric' }, - { - type: 'range', - schema: 'bucket', - params: { - field: 'bytes', - ranges: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 } - ] - } - } - ] - }); - vis.type.requestHandler = 'courier'; - vis.type.responseHandler = 'none'; - vis.type.requiresSearch = false; - - // Setup savedObject - mockedSavedObject = createSavedObject(); - - sandbox = sinon.sandbox.create(); - // Mock savedVisualizations.get to return 'mockedSavedObject' when id is 'exists' - sandbox.stub(savedVisualizations, 'get').callsFake((id) => - id === 'exists' ? Promise.resolve(mockedSavedObject) : Promise.reject() - ); - })); - setupAndTeardownInjectorStub(); - beforeEach(async () => { - loader = await getVisualizeLoader(); - }); - - afterEach(() => { - if (sandbox) { - sandbox.restore(); - } - }); - - describe('getVisualizeLoader', () => { - - it('should return a promise', () => { - expect(getVisualizeLoader().then).to.be.a('function'); - }); - - it('should resolve to an object', async () => { - const visualizeLoader = await getVisualizeLoader(); - expect(visualizeLoader).to.be.an('object'); - }); - - }); - - describe('service', () => { - - describe('getVisualizationList', () => { - - it('should be a function', async () => { - expect(loader.getVisualizationList).to.be.a('function'); - }); - - }); - - describe('embedVisualizationWithSavedObject', () => { - - it('should be a function', () => { - expect(loader.embedVisualizationWithSavedObject).to.be.a('function'); - }); - - it('should render the visualize element', () => { - const container = newContainer(); - loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); - expect(container.find('[data-test-subj="visualizationLoader"]').length).to.be(1); - }); - - it('should not mutate vis.params', () => { - const container = newContainer(); - const savedObject = createSavedObject(); - const paramsBefore = cloneDeep(vis.params); - loader.embedVisualizationWithSavedObject(container[0], savedObject, {}); - const paramsAfter = cloneDeep(vis.params); - expect(paramsBefore).to.eql(paramsAfter); - }); - - it('should replace content of container by default', () => { - const container = angular.element('
'); - loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); - expect(container.find('#prevContent').length).to.be(0); - }); - - it('should append content to container when using append parameter', () => { - const container = angular.element('
'); - loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), { - append: true - }); - expect(container.children().length).to.be(2); - expect(container.find('#prevContent').length).to.be(1); - }); - - it('should apply css classes from parameters', () => { - const vis = embedWithParams({ cssClass: 'my-css-class another-class' }); - expect(vis.hasClass('my-css-class')).to.be(true); - expect(vis.hasClass('another-class')).to.be(true); - }); - - it('should apply data attributes from dataAttrs parameter', () => { - const vis = embedWithParams({ - dataAttrs: { - 'foo': '', - 'with-dash': 'value', - } - }); - expect(vis.attr('data-foo')).to.be(''); - expect(vis.attr('data-with-dash')).to.be('value'); - }); - }); - - describe('embedVisualizationWithId', () => { - - it('should be a function', async () => { - expect(loader.embedVisualizationWithId).to.be.a('function'); - }); - - it('should reject if the id was not found', () => { - const resolveSpy = sinon.spy(); - const rejectSpy = sinon.spy(); - const container = newContainer(); - return loader.embedVisualizationWithId(container[0], 'not-existing', {}) - .then(resolveSpy, rejectSpy) - .then(() => { - expect(resolveSpy.called).to.be(false); - expect(rejectSpy.calledOnce).to.be(true); - }); - }); - - it('should render a visualize element, if id was found', async () => { - const container = newContainer(); - await loader.embedVisualizationWithId(container[0], 'exists', {}); - expect(container.find('[data-test-subj="visualizationLoader"]').length).to.be(1); - }); - - }); - - describe('EmbeddedVisualizeHandler', () => { - it('should be returned from embedVisualizationWithId via a promise', async () => { - const handler = await loader.embedVisualizationWithId(newContainer()[0], 'exists', {}); - expect(handler instanceof EmbeddedVisualizeHandler).to.be(true); - }); - - it('should be returned from embedVisualizationWithSavedObject', async () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - expect(handler instanceof EmbeddedVisualizeHandler).to.be(true); - }); - - it('should give access to the visualize element', () => { - const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); - expect(handler.getElement()).to.be(container.find('[data-test-subj="visualizationLoader"]')[0]); - }); - - it('should allow opening the inspector of the visualization and return its session', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - sandbox.spy(Inspector, 'open'); - const inspectorSession = handler.openInspector(); - expect(Inspector.open.calledOnce).to.be(true); - expect(inspectorSession.close).to.be.a('function'); - inspectorSession.close(); - }); - - describe('inspector', () => { - - describe('hasInspector()', () => { - it('should forward to inspectors hasInspector', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - sinon.spy(Inspector, 'isAvailable'); - handler.hasInspector(); - expect(Inspector.isAvailable.calledOnce).to.be(true); - const adapters = Inspector.isAvailable.lastCall.args[0]; - expect(adapters.data).to.be.a(DataAdapter); - expect(adapters.requests).to.be.a(RequestAdapter); - }); - - it('should return hasInspectors result', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - const stub = sinon.stub(Inspector, 'isAvailable'); - stub.returns(true); - expect(handler.hasInspector()).to.be(true); - stub.returns(false); - expect(handler.hasInspector()).to.be(false); - }); - - afterEach(() => { - Inspector.isAvailable.restore(); - }); - }); - - describe('openInspector()', () => { - - beforeEach(() => { - sinon.stub(Inspector, 'open'); - }); - - it('should call openInspector with all attached inspectors', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - handler.openInspector(); - expect(Inspector.open.calledOnce).to.be(true); - const adapters = Inspector.open.lastCall.args[0]; - expect(adapters).to.be(handler.inspectorAdapters); - }); - - it('should pass the vis title to the openInspector call', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - handler.openInspector(); - expect(Inspector.open.calledOnce).to.be(true); - const params = Inspector.open.lastCall.args[1]; - expect(params.title).to.be('testVis'); - }); - - afterEach(() => { - Inspector.open.restore(); - }); - }); - - describe('inspectorAdapters', () => { - - it('should register none for none requestHandler', () => { - const savedObj = createSavedObject(); - savedObj.vis.type.requestHandler = 'none'; - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj, {}); - expect(handler.inspectorAdapters).to.eql({}); - }); - - it('should attach data and request handler for courier', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - expect(handler.inspectorAdapters.data).to.be.a(DataAdapter); - expect(handler.inspectorAdapters.requests).to.be.a(RequestAdapter); - }); - - it('should allow enabling data adapter manually', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - expect(handler.inspectorAdapters.data).to.be.a(DataAdapter); - }); - - it('should allow enabling requests adapter manually', () => { - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), {}); - expect(handler.inspectorAdapters.requests).to.be.a(RequestAdapter); - }); - - it('should allow adding custom inspector adapters via the custom key', () => { - const Foodapter = class { }; - const Bardapter = class { }; - const savedObj = createSavedObject(); - savedObj.vis.type.inspectorAdapters = { - custom: { foo: Foodapter, bar: Bardapter } - }; - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj, {}); - expect(handler.inspectorAdapters.foo).to.be.a(Foodapter); - expect(handler.inspectorAdapters.bar).to.be.a(Bardapter); - }); - - it('should not share adapter instances between vis instances', () => { - const Foodapter = class { }; - const savedObj1 = createSavedObject(); - const savedObj2 = createSavedObject(); - savedObj1.vis.type.inspectorAdapters = { custom: { foo: Foodapter } }; - savedObj2.vis.type.inspectorAdapters = { custom: { foo: Foodapter } }; - const handler1 = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj1, {}); - const handler2 = loader.embedVisualizationWithSavedObject(newContainer()[0], savedObj2, {}); - expect(handler1.inspectorAdapters.foo).to.be.a(Foodapter); - expect(handler2.inspectorAdapters.foo).to.be.a(Foodapter); - expect(handler1.inspectorAdapters.foo).not.to.be(handler2.inspectorAdapters.foo); - expect(handler1.inspectorAdapters.data).to.be.a(DataAdapter); - expect(handler2.inspectorAdapters.data).to.be.a(DataAdapter); - expect(handler1.inspectorAdapters.data).not.to.be(handler2.inspectorAdapters.data); - }); - }); - - }); - - it('should have whenFirstRenderComplete returns a promise resolving on first renderComplete event', async () => { - const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); - const spy = sinon.spy(); - handler.whenFirstRenderComplete().then(spy); - expect(spy.notCalled).to.be(true); - dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); - await timeout(); - expect(spy.calledOnce).to.be(true); - }); - - it('should add listeners via addRenderCompleteListener that triggers on renderComplete events', async () => { - const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); - const spy = sinon.spy(); - handler.addRenderCompleteListener(spy); - expect(spy.notCalled).to.be(true); - dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); - await timeout(); - expect(spy.calledOnce).to.be(true); - }); - - it('should call render complete listeners once per renderComplete event', async () => { - const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); - const spy = sinon.spy(); - handler.addRenderCompleteListener(spy); - expect(spy.notCalled).to.be(true); - dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); - dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); - dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); - expect(spy.callCount).to.be(3); - }); - - it('should successfully remove listeners from render complete', async () => { - const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), {}); - const spy = sinon.spy(); - handler.addRenderCompleteListener(spy); - expect(spy.notCalled).to.be(true); - dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); - expect(spy.calledOnce).to.be(true); - spy.resetHistory(); - handler.removeRenderCompleteListener(spy); - dispatchRenderComplete(container.find('[data-test-subj="visualizationLoader"]')[0]); - expect(spy.notCalled).to.be(true); - }); - - - it('should allow updating and deleting data attributes', () => { - const container = newContainer(); - const handler = loader.embedVisualizationWithSavedObject(container[0], createSavedObject(), { - dataAttrs: { - foo: 42 - } - }); - expect(container.find('[data-test-subj="visualizationLoader"]').attr('data-foo')).to.be('42'); - handler.update({ - dataAttrs: { - foo: null, - added: 'value', - } - }); - expect(container.find('[data-test-subj="visualizationLoader"]')[0].hasAttribute('data-foo')).to.be(false); - expect(container.find('[data-test-subj="visualizationLoader"]').attr('data-added')).to.be('value'); - }); - - it('should allow updating the time range of the visualization', async () => { - const spy = sandbox.spy(DataLoader.prototype, 'fetch'); - - const handler = loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), { - timeRange: { from: 'now-7d', to: 'now' } - }); - - // Wait for the initial fetch and render to happen - await timeout(150); - spy.resetHistory(); - - handler.update({ - timeRange: { from: 'now-10d/d', to: 'now' } - }); - - // Wait for fetch debounce to happen (as soon as we use lodash 4+ we could use fake timers here for the debounce) - await timeout(150); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWith(spy, sinon.match({ timeRange: { from: 'now-10d/d', to: 'now' } })); - }); - - it('should not set forceFetch on uiState change', async () => { - const spy = sandbox.spy(DataLoader.prototype, 'fetch'); - - const uiState = new PersistedState(); - loader.embedVisualizationWithSavedObject(newContainer()[0], createSavedObject(), { - timeRange: { from: 'now-7d', to: 'now' }, - uiState: uiState, - }); - - // Wait for the initial fetch and render to happen - await timeout(150); - spy.resetHistory(); - - uiState.set('property', 'value'); - - // Wait for fetch debounce to happen (as soon as we use lodash 4+ we could use fake timers here for the debounce) - await timeout(150); - - sinon.assert.calledOnce(spy); - sinon.assert.calledWith(spy, sinon.match({ forceFetch: false })); - }); - }); - - }); -}); diff --git a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.mocks.ts b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.mocks.ts deleted file mode 100644 index 4ca90d6c6b61b..0000000000000 --- a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.mocks.ts +++ /dev/null @@ -1,73 +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. - */ - -jest.useFakeTimers(); - -import { Subject } from 'rxjs'; - -jest.mock('ui/notify', () => ({ - toastNotifications: jest.fn(), -})); - -jest.mock('./utils', () => ({ - queryGeohashBounds: jest.fn(), -})); - -jest.mock('./pipeline_helpers/utilities', () => ({ - getFormat: jest.fn(), - getTableAggs: jest.fn(), -})); - -const autoRefreshFetchSub = new Subject(); - -export const timefilter = { - _triggerAutoRefresh: () => { - autoRefreshFetchSub.next(); - }, - getAutoRefreshFetch$: () => { - return autoRefreshFetchSub.asObservable(); - }, -}; -jest.doMock('../../timefilter', () => ({ timefilter })); - -jest.mock('../../inspector', () => ({ - Inspector: { - open: jest.fn(), - isAvailable: jest.fn(), - }, -})); - -export const mockDataLoaderFetch = jest.fn().mockReturnValue({ - as: 'visualization', - value: { - visType: 'histogram', - visData: {}, - visConfig: {}, - params: {}, - }, -}); -const MockDataLoader = class { - public async fetch(data: any) { - return await mockDataLoaderFetch(data); - } -}; - -jest.mock('./pipeline_data_loader', () => ({ - PipelineDataLoader: MockDataLoader, -})); diff --git a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts deleted file mode 100644 index c73f787457a03..0000000000000 --- a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.test.ts +++ /dev/null @@ -1,299 +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. - */ -jest.mock('ui/new_platform'); - -import { searchSourceMock } from '../../courier/search_source/mocks'; -import { mockDataLoaderFetch, timefilter } from './embedded_visualize_handler.test.mocks'; - -import _ from 'lodash'; -// @ts-ignore -import MockState from '../../../../../fixtures/mock_state'; -import { Vis } from '../../vis'; -import { VisResponseData } from './types'; -import { Inspector } from '../../inspector'; -import { EmbeddedVisualizeHandler, RequestHandlerParams } from './embedded_visualize_handler'; -import { AggConfigs } from 'ui/agg_types/agg_configs'; - -jest.mock('plugins/interpreter/interpreter', () => ({ - getInterpreter: () => { - return Promise.resolve(); - }, -})); - -jest.mock('../../../../core_plugins/interpreter/public/registries', () => ({ - registries: { - renderers: { - get: (name: string) => { - return { - render: async () => { - return {}; - }, - }; - }, - }, - }, -})); - -describe('EmbeddedVisualizeHandler', () => { - let handler: any; - let div: HTMLElement; - let dataLoaderParams: RequestHandlerParams; - const mockVis: Vis = { - title: 'My Vis', - // @ts-ignore - type: 'foo', - getAggConfig: () => [], - _setUiState: () => ({}), - getUiState: () => new MockState(), - on: () => ({}), - off: () => ({}), - removeListener: jest.fn(), - API: {}, - }; - - beforeEach(() => { - jest.clearAllMocks(); - - jest.spyOn(_, 'debounce').mockImplementation( - // @ts-ignore - (f: Function) => { - // @ts-ignore - f.cancel = () => {}; - return f; - } - ); - - dataLoaderParams = { - aggs: ([] as any) as AggConfigs, - filters: undefined, - forceFetch: false, - inspectorAdapters: {}, - query: undefined, - queryFilter: null, - searchSource: searchSourceMock, - timeRange: undefined, - uiState: undefined, - }; - - div = document.createElement('div'); - handler = new EmbeddedVisualizeHandler( - div, - { - vis: mockVis, - title: 'My Vis', - searchSource: searchSourceMock, - destroy: () => ({}), - copyOnSave: false, - save: () => Promise.resolve('123'), - }, - { - autoFetch: true, - Private: (provider: () => T) => provider(), - queryFilter: null, - } - ); - }); - - afterEach(() => { - handler.destroy(); - }); - - describe('autoFetch', () => { - it('should trigger a reload when autoFetch=true and auto refresh happens', () => { - const spy = jest.spyOn(handler, 'fetchAndRender'); - timefilter._triggerAutoRefresh(); - jest.runAllTimers(); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(true); - }); - - it('should not trigger a reload when autoFetch=false and auto refresh happens', () => { - handler = new EmbeddedVisualizeHandler( - div, - { - vis: mockVis, - title: 'My Vis', - searchSource: searchSourceMock, - destroy: () => ({}), - copyOnSave: false, - save: () => Promise.resolve('123'), - }, - { - autoFetch: false, - Private: (provider: () => T) => provider(), - queryFilter: null, - } - ); - const spy = jest.spyOn(handler, 'fetchAndRender'); - timefilter._triggerAutoRefresh(); - jest.runAllTimers(); - expect(spy).not.toHaveBeenCalled(); - }); - }); - - describe('getElement', () => { - it('should return the provided html element', () => { - expect(handler.getElement()).toBe(div); - }); - }); - - describe('update', () => { - it('should add provided data- attributes to the html element', () => { - const spy = jest.spyOn(handler, 'fetchAndRender'); - const params = { - dataAttrs: { foo: 'bar' }, - }; - handler.update(params); - expect(spy).not.toHaveBeenCalled(); - expect(handler.getElement()).toMatchSnapshot(); - }); - - it('should remove null data- attributes from the html element', () => { - const spy = jest.spyOn(handler, 'fetchAndRender'); - handler.update({ - dataAttrs: { foo: 'bar' }, - }); - const params = { - dataAttrs: { - foo: null, - baz: 'qux', - }, - }; - handler.update(params); - expect(spy).not.toHaveBeenCalled(); - expect(handler.getElement()).toMatchSnapshot(); - }); - - it('should call dataLoader.render with updated timeRange', () => { - const params = { timeRange: { foo: 'bar' } }; - handler.update(params); - expect(mockDataLoaderFetch).toHaveBeenCalled(); - const callIndex = mockDataLoaderFetch.mock.calls.length - 1; - const { abortSignal, ...otherParams } = mockDataLoaderFetch.mock.calls[callIndex][0]; - expect(abortSignal).toBeInstanceOf(AbortSignal); - expect(otherParams).toEqual({ ...dataLoaderParams, ...params }); - }); - - it('should call dataLoader.render with updated filters', () => { - const params = { filters: [{ meta: { disabled: false } }] }; - handler.update(params); - expect(mockDataLoaderFetch).toHaveBeenCalled(); - const callIndex = mockDataLoaderFetch.mock.calls.length - 1; - const { abortSignal, ...otherParams } = mockDataLoaderFetch.mock.calls[callIndex][0]; - expect(abortSignal).toBeInstanceOf(AbortSignal); - expect(otherParams).toEqual({ ...dataLoaderParams, ...params }); - }); - - it('should call dataLoader.render with updated query', () => { - const params = { query: { foo: 'bar' } }; - handler.update(params); - expect(mockDataLoaderFetch).toHaveBeenCalled(); - const callIndex = mockDataLoaderFetch.mock.calls.length - 1; - const { abortSignal, ...otherParams } = mockDataLoaderFetch.mock.calls[callIndex][0]; - expect(abortSignal).toBeInstanceOf(AbortSignal); - expect(otherParams).toEqual({ ...dataLoaderParams, ...params }); - }); - }); - - describe('destroy', () => { - it('should remove vis event listeners', () => { - const spy = jest.spyOn(mockVis, 'removeListener'); - handler.destroy(); - expect(spy).toHaveBeenCalledTimes(2); - expect(spy.mock.calls[0][0]).toBe('reload'); - expect(spy.mock.calls[1][0]).toBe('update'); - }); - - it('should remove element event listeners', () => { - const spy = jest.spyOn(handler.getElement(), 'removeEventListener'); - handler.destroy(); - expect(spy).toHaveBeenCalled(); - }); - - it('should prevent subsequent renders', () => { - const spy = jest.spyOn(handler, 'fetchAndRender'); - handler.destroy(); - expect(spy).not.toHaveBeenCalled(); - }); - - it('should cancel debounced fetchAndRender', () => { - const spy = jest.spyOn(handler.debouncedFetchAndRender, 'cancel'); - handler.destroy(); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('should call abort on controller', () => { - handler.abortController = new AbortController(); - const spy = jest.spyOn(handler.abortController, 'abort'); - handler.destroy(); - expect(spy).toHaveBeenCalled(); - }); - }); - - describe('openInspector', () => { - it('calls Inspector.open()', () => { - handler.openInspector(); - expect(Inspector.open).toHaveBeenCalledTimes(1); - expect(Inspector.open).toHaveBeenCalledWith({}, { title: 'My Vis' }); - }); - }); - - describe('hasInspector', () => { - it('calls Inspector.isAvailable()', () => { - handler.hasInspector(); - expect(Inspector.isAvailable).toHaveBeenCalledTimes(1); - expect(Inspector.isAvailable).toHaveBeenCalledWith({}); - }); - }); - - describe('reload', () => { - it('should force fetch and render', () => { - const spy = jest.spyOn(handler, 'fetchAndRender'); - handler.reload(); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith(true); - }); - }); - - describe('data$', () => { - it('observable can be used to get response data in the correct format', async () => { - let response; - handler.data$.subscribe((data: VisResponseData) => (response = data)); - await handler.fetch(true); - jest.runAllTimers(); - expect(response).toMatchSnapshot(); - }); - }); - - describe('render', () => { - // TODO - }); - - describe('whenFirstRenderComplete', () => { - // TODO - }); - - describe('addRenderCompleteListener', () => { - // TODO - }); - - describe('removeRenderCompleteListener', () => { - // TODO - }); -}); diff --git a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts b/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts deleted file mode 100644 index fb16e095b3418..0000000000000 --- a/src/legacy/ui/public/visualize/loader/embedded_visualize_handler.ts +++ /dev/null @@ -1,553 +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 { EventEmitter } from 'events'; -import { debounce, forEach, get, isEqual } from 'lodash'; -import * as Rx from 'rxjs'; -import { share } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; -// @ts-ignore untyped dependency -import { AggConfigs } from 'ui/agg_types/agg_configs'; -import { SearchSource } from 'ui/courier'; -import { QueryFilter } from 'ui/filter_manager/query_filter'; - -import { TimeRange, onlyDisabledFiltersChanged } from '../../../../../plugins/data/public'; -import { registries } from '../../../../core_plugins/interpreter/public/registries'; -import { Inspector } from '../../inspector'; -import { Adapters } from '../../inspector/types'; -import { PersistedState } from '../../persisted_state'; -import { IPrivate } from '../../private'; -import { RenderCompleteHelper } from '../../../../../plugins/kibana_utils/public'; -import { AppState } from '../../state_management/app_state'; -import { timefilter } from '../../timefilter'; -import { Vis } from '../../vis'; -// @ts-ignore untyped dependency -import { VisFiltersProvider } from '../../vis/vis_filters'; -import { PipelineDataLoader } from './pipeline_data_loader'; -import { visualizationLoader } from './visualization_loader'; -import { Query } from '../../../../core_plugins/data/public'; -import { esFilters } from '../../../../../plugins/data/public'; - -import { DataAdapter, RequestAdapter } from '../../inspector/adapters'; - -import { getTableAggs } from './pipeline_helpers/utilities'; -import { - VisResponseData, - VisSavedObject, - VisualizeLoaderParams, - VisualizeUpdateParams, -} from './types'; -import { queryGeohashBounds } from './utils'; - -interface EmbeddedVisualizeHandlerParams extends VisualizeLoaderParams { - Private: IPrivate; - queryFilter: any; - autoFetch?: boolean; -} - -export interface RequestHandlerParams { - searchSource: SearchSource; - aggs: AggConfigs; - timeRange?: TimeRange; - query?: Query; - filters?: esFilters.Filter[]; - forceFetch: boolean; - queryFilter: QueryFilter; - uiState?: PersistedState; - partialRows?: boolean; - inspectorAdapters: Adapters; - metricsAtAllLevels?: boolean; - visParams?: any; - abortSignal?: AbortSignal; -} - -const RENDER_COMPLETE_EVENT = 'render_complete'; -const DATA_SHARED_ITEM = 'data-shared-item'; -const LOADING_ATTRIBUTE = 'data-loading'; -const RENDERING_COUNT_ATTRIBUTE = 'data-rendering-count'; - -/** - * A handler to the embedded visualization. It offers several methods to interact - * with the visualization. - */ -export class EmbeddedVisualizeHandler { - /** - * This observable will emit every time new data is loaded for the - * visualization. The emitted value is the loaded data after it has - * been transformed by the visualization's response handler. - * This should not be used by any plugin. - * @ignore - */ - public readonly data$: Rx.Observable; - public readonly inspectorAdapters: Adapters = {}; - private vis: Vis; - private handlers: any; - private loaded: boolean = false; - private destroyed: boolean = false; - - private listeners = new EventEmitter(); - private firstRenderComplete: Promise; - private renderCompleteHelper: RenderCompleteHelper; - private shouldForceNextFetch: boolean = false; - private debouncedFetchAndRender = debounce(() => { - if (this.destroyed) { - return; - } - - const forceFetch = this.shouldForceNextFetch; - this.shouldForceNextFetch = false; - this.fetch(forceFetch).then(this.render); - }, 100); - - private dataLoaderParams: RequestHandlerParams; - private readonly appState?: AppState; - private uiState: PersistedState; - private dataLoader: PipelineDataLoader; - private dataSubject: Rx.Subject; - private actions: any = {}; - private events$: Rx.Observable; - private autoFetch: boolean; - private abortController?: AbortController; - private autoRefreshFetchSubscription: Rx.Subscription | undefined; - - constructor( - private readonly element: HTMLElement, - savedObject: VisSavedObject, - params: EmbeddedVisualizeHandlerParams - ) { - const { searchSource, vis } = savedObject; - - const { - appState, - uiState, - queryFilter, - timeRange, - filters, - query, - autoFetch = true, - Private, - } = params; - - this.dataLoaderParams = { - searchSource, - timeRange, - query, - queryFilter, - filters, - uiState, - aggs: vis.getAggConfig(), - forceFetch: false, - inspectorAdapters: this.inspectorAdapters, - }; - - // Listen to the first RENDER_COMPLETE_EVENT to resolve this promise - this.firstRenderComplete = new Promise(resolve => { - this.listeners.once(RENDER_COMPLETE_EVENT, resolve); - }); - - element.setAttribute(LOADING_ATTRIBUTE, ''); - element.setAttribute(DATA_SHARED_ITEM, ''); - element.setAttribute(RENDERING_COUNT_ATTRIBUTE, '0'); - - element.addEventListener('renderComplete', this.onRenderCompleteListener); - - this.autoFetch = autoFetch; - this.appState = appState; - this.vis = vis; - if (uiState) { - vis._setUiState(uiState); - } - this.uiState = this.vis.getUiState(); - - this.handlers = { - vis: this.vis, - uiState: this.uiState, - onDestroy: (fn: () => never) => (this.handlers.destroyFn = fn), - }; - - this.vis.on('update', this.handleVisUpdate); - this.vis.on('reload', this.reload); - this.uiState.on('change', this.onUiStateChange); - if (autoFetch) { - this.autoRefreshFetchSubscription = timefilter.getAutoRefreshFetch$().subscribe(this.reload); - } - - // This is a hack to give maps visualizations access to data in the - // globalState, since they can no longer access it via searchSource. - // TODO: Remove this as a part of elastic/kibana#30593 - this.vis.API.getGeohashBounds = () => { - return queryGeohashBounds(this.vis, { - filters: this.dataLoaderParams.filters, - query: this.dataLoaderParams.query, - }); - }; - - this.dataLoader = new PipelineDataLoader(vis); - const visFilters: any = Private(VisFiltersProvider); - this.renderCompleteHelper = new RenderCompleteHelper(element); - this.inspectorAdapters = this.getActiveInspectorAdapters(); - this.vis.openInspector = this.openInspector; - this.vis.hasInspector = this.hasInspector; - - // init default actions - forEach(this.vis.type.events, (event, eventName) => { - if (event.disabled || !eventName) { - return; - } else { - this.actions[eventName] = event.defaultAction; - } - }); - - this.handlers.eventsSubject = new Rx.Subject(); - this.vis.eventsSubject = this.handlers.eventsSubject; - this.events$ = this.handlers.eventsSubject.asObservable().pipe(share()); - this.events$.subscribe(event => { - if (this.actions[event.name]) { - event.data.aggConfigs = getTableAggs(this.vis); - const newFilters = this.actions[event.name](event.data) || []; - if (event.name === 'brush') { - const fieldName = newFilters[0].meta.key; - const $state = this.vis.API.getAppState(); - const existingFilter = $state.filters.find( - (filter: any) => filter.meta && filter.meta.key === fieldName - ); - if (existingFilter) { - Object.assign(existingFilter, newFilters[0]); - } - } - visFilters.pushFilters(newFilters); - } - }); - - this.dataSubject = new Rx.Subject(); - this.data$ = this.dataSubject.asObservable().pipe(share()); - - this.render(); - } - - /** - * Update properties of the embedded visualization. This method does not allow - * updating all initial parameters, but only a subset of the ones allowed - * in {@link VisualizeUpdateParams}. - * - * @param params The parameters that should be updated. - */ - public update(params: VisualizeUpdateParams = {}) { - // Apply data- attributes to the element if specified - const dataAttrs = params.dataAttrs; - if (dataAttrs) { - Object.keys(dataAttrs).forEach(key => { - if (dataAttrs[key] === null) { - this.element.removeAttribute(`data-${key}`); - return; - } - - this.element.setAttribute(`data-${key}`, dataAttrs[key]); - }); - } - - let fetchRequired = false; - if ( - params.hasOwnProperty('timeRange') && - !isEqual(this.dataLoaderParams.timeRange, params.timeRange) - ) { - fetchRequired = true; - this.dataLoaderParams.timeRange = params.timeRange; - } - if ( - params.hasOwnProperty('filters') && - !onlyDisabledFiltersChanged(this.dataLoaderParams.filters, params.filters) - ) { - fetchRequired = true; - this.dataLoaderParams.filters = params.filters; - } - if (params.hasOwnProperty('query') && !isEqual(this.dataLoaderParams.query, params.query)) { - fetchRequired = true; - this.dataLoaderParams.query = params.query; - } - - if (fetchRequired) { - this.fetchAndRender(); - } - } - - /** - * Destroy the underlying Angular scope of the visualization. This should be - * called whenever you remove the visualization. - */ - public destroy(): void { - this.destroyed = true; - this.cancel(); - this.debouncedFetchAndRender.cancel(); - if (this.autoFetch) { - if (this.autoRefreshFetchSubscription) this.autoRefreshFetchSubscription.unsubscribe(); - } - this.vis.removeListener('reload', this.reload); - this.vis.removeListener('update', this.handleVisUpdate); - this.element.removeEventListener('renderComplete', this.onRenderCompleteListener); - this.uiState.off('change', this.onUiStateChange); - visualizationLoader.destroy(this.element); - this.renderCompleteHelper.destroy(); - if (this.handlers.destroyFn) { - this.handlers.destroyFn(); - } - } - - /** - * Return the actual DOM element (wrapped in jQuery) of the rendered visualization. - * This is especially useful if you used `append: true` in the parameters where - * the visualization will be appended to the specified container. - */ - public getElement(): HTMLElement { - return this.element; - } - - /** - * renders visualization with provided data - * @param response: visualization data - */ - public render = (response: VisResponseData | null = null): void => { - const executeRenderer = this.rendererProvider(response); - if (!executeRenderer) { - return; - } - - // TODO: we have this weird situation when we need to render first, - // and then we call fetch and render... we need to get rid of that. - executeRenderer().then(() => { - if (!this.loaded) { - this.loaded = true; - if (this.autoFetch) { - this.fetchAndRender(); - } - } - }); - }; - - /** - * Opens the inspector for the embedded visualization. This will return an - * handler to the inspector to close and interact with it. - * @return An inspector session to interact with the opened inspector. - */ - public openInspector = () => { - return Inspector.open(this.inspectorAdapters, { - title: this.vis.title, - }); - }; - - public hasInspector = () => { - return Inspector.isAvailable(this.inspectorAdapters); - }; - - /** - * Returns a promise, that will resolve (without a value) once the first rendering of - * the visualization has finished. If you want to listen to consecutive rendering - * events, look into the `addRenderCompleteListener` method. - * - * @returns Promise, that resolves as soon as the visualization is done rendering - * for the first time. - */ - public whenFirstRenderComplete(): Promise { - return this.firstRenderComplete; - } - - /** - * Adds a listener to be called whenever the visualization finished rendering. - * This can be called multiple times, when the visualization rerenders, e.g. due - * to new data. - * - * @param {function} listener The listener to be notified about complete renders. - */ - public addRenderCompleteListener(listener: () => void) { - this.listeners.addListener(RENDER_COMPLETE_EVENT, listener); - } - - /** - * Removes a previously registered render complete listener from this handler. - * This listener will no longer be called when the visualization finished rendering. - * - * @param {function} listener The listener to remove from this handler. - */ - public removeRenderCompleteListener(listener: () => void) { - this.listeners.removeListener(RENDER_COMPLETE_EVENT, listener); - } - - /** - * Force the fetch of new data and renders the chart again. - */ - public reload = () => { - this.fetchAndRender(true); - }; - - private incrementRenderingCount = () => { - const renderingCount = Number(this.element.getAttribute(RENDERING_COUNT_ATTRIBUTE) || 0); - this.element.setAttribute(RENDERING_COUNT_ATTRIBUTE, `${renderingCount + 1}`); - }; - - private onRenderCompleteListener = () => { - this.listeners.emit(RENDER_COMPLETE_EVENT); - this.element.removeAttribute(LOADING_ATTRIBUTE); - this.incrementRenderingCount(); - }; - - private onUiStateChange = () => { - this.fetchAndRender(); - }; - - /** - * Returns an object of all inspectors for this vis object. - * This must only be called after this.type has properly be initialized, - * since we need to read out data from the the vis type to check which - * inspectors are available. - */ - private getActiveInspectorAdapters = (): Adapters => { - const adapters: Adapters = {}; - const { inspectorAdapters: typeAdapters } = this.vis.type; - - // Add the requests inspector adapters if the vis type explicitly requested it via - // inspectorAdapters.requests: true in its definition or if it's using the courier - // request handler, since that will automatically log its requests. - if ((typeAdapters && typeAdapters.requests) || this.vis.type.requestHandler === 'courier') { - adapters.requests = new RequestAdapter(); - } - - // Add the data inspector adapter if the vis type requested it or if the - // vis is using courier, since we know that courier supports logging - // its data. - if ((typeAdapters && typeAdapters.data) || this.vis.type.requestHandler === 'courier') { - adapters.data = new DataAdapter(); - } - - // Add all inspectors, that are explicitly registered with this vis type - if (typeAdapters && typeAdapters.custom) { - Object.entries(typeAdapters.custom).forEach(([key, Adapter]) => { - adapters[key] = new (Adapter as any)(); - }); - } - - return adapters; - }; - - /** - * Fetches new data and renders the chart. This will happen debounced for a couple - * of milliseconds, to bundle fast successive calls into one fetch and render, - * e.g. while resizing the window, this will be triggered constantly on the resize - * event. - * - * @param forceFetch=false Whether the request handler should be signaled to forceFetch - * (i.e. ignore caching in case it supports it). If at least one call to this - * passed `true` the debounced fetch and render will be a force fetch. - */ - private fetchAndRender = (forceFetch = false): void => { - this.shouldForceNextFetch = forceFetch || this.shouldForceNextFetch; - this.element.setAttribute(LOADING_ATTRIBUTE, ''); - this.debouncedFetchAndRender(); - }; - - private handleVisUpdate = () => { - if (this.appState) { - this.appState.vis = this.vis.getState(); - this.appState.save(); - } - - this.fetchAndRender(); - }; - - private cancel = () => { - if (this.abortController) this.abortController.abort(); - }; - - private fetch = (forceFetch: boolean = false) => { - this.cancel(); - this.abortController = new AbortController(); - this.dataLoaderParams.abortSignal = this.abortController.signal; - this.dataLoaderParams.aggs = this.vis.getAggConfig(); - this.dataLoaderParams.forceFetch = forceFetch; - this.dataLoaderParams.inspectorAdapters = this.inspectorAdapters; - - this.vis.filters = { timeRange: this.dataLoaderParams.timeRange }; - this.vis.requestError = undefined; - this.vis.showRequestError = false; - - return ( - this.dataLoader - // Don't pass in this.dataLoaderParams directly because it may be modified async in another - // call to fetch before the previous one has completed - .fetch({ ...this.dataLoaderParams }) - .then(data => { - // Pipeline responses never throw errors, so we need to check for - // `type: 'error'`, and then throw so it can be caught below. - // TODO: We should revisit this after we have fully migrated - // to the new expression pipeline infrastructure. - if (data && data.type === 'error') { - throw data.error; - } - - if (data && data.value) { - this.dataSubject.next(data.value); - } - return data; - }) - .catch(this.handleDataLoaderError) - ); - }; - - /** - * When dataLoader returns an error, we need to make sure it surfaces in the UI. - * - * TODO: Eventually we should add some custom error messages for issues that are - * frequently encountered by users. - */ - private handleDataLoaderError = (error: any): void => { - // If the data loader was aborted then no need to surface this error in the UI - if (error && error.name === 'AbortError') return; - - // Cancel execution of pipeline expressions - if (this.abortController) { - this.abortController.abort(); - } - - this.vis.requestError = error; - this.vis.showRequestError = - error.type && ['NO_OP_SEARCH_STRATEGY', 'UNSUPPORTED_QUERY'].includes(error.type); - - toastNotifications.addDanger({ - title: i18n.translate('common.ui.visualize.dataLoaderError', { - defaultMessage: 'Error in visualization', - }), - text: error.message, - }); - }; - - private rendererProvider = (response: VisResponseData | null) => { - const renderer = registries.renderers.get(get(response || {}, 'as', 'visualization')); - - if (!renderer) { - return null; - } - - return () => - renderer.render( - this.element, - get(response, 'value', { visType: this.vis.type.name }), - this.handlers - ); - }; -} diff --git a/src/legacy/ui/public/visualize/loader/pipeline_data_loader.ts b/src/legacy/ui/public/visualize/loader/pipeline_data_loader.ts deleted file mode 100644 index c1aa6903abe88..0000000000000 --- a/src/legacy/ui/public/visualize/loader/pipeline_data_loader.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Vis } from '../../vis'; -import { buildPipeline, runPipeline } from './pipeline_helpers'; -import { RequestHandlerParams } from './embedded_visualize_handler'; - -export class PipelineDataLoader { - constructor(private readonly vis: Vis) {} - - public async fetch(params: RequestHandlerParams): Promise { - this.vis.pipelineExpression = await buildPipeline(this.vis, params); - - return runPipeline( - this.vis.pipelineExpression, - { type: 'null' }, - { - getInitialContext: () => ({ - type: 'kibana_context', - query: params.query, - timeRange: params.timeRange, - filters: params.filters - ? params.filters.filter(filter => !filter.meta.disabled) - : undefined, - }), - inspectorAdapters: params.inspectorAdapters, - abortSignal: params.abortSignal, - } - ); - } -} 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 056f3e8cfc586..70e0c1f1382fa 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/pipeline_helpers/index.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts index 69c29339a8713..a1292c59ac61d 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/index.ts @@ -18,4 +18,3 @@ */ export { buildPipeline } from './build_pipeline'; -export { runPipeline } from './run_pipeline'; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts deleted file mode 100644 index 78a959b2b0f71..0000000000000 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// @ts-ignore -import { fromExpression } from '@kbn/interpreter/common'; -import { Adapters } from 'ui/inspector'; -import { getInterpreter } from '../../../../../core_plugins/interpreter/public/interpreter'; -import { KibanaContext } from '../../../../../core_plugins/interpreter/public'; - -type getInitialContextFunction = () => KibanaContext; - -export interface RunPipelineHandlers { - getInitialContext: getInitialContextFunction; - inspectorAdapters?: Adapters; - abortSignal?: AbortSignal; -} - -export const runPipeline = async ( - expression: string, - context: any, - handlers: RunPipelineHandlers -) => { - const ast = fromExpression(expression); - const { interpreter } = await getInterpreter(); - const pipelineResponse = await interpreter.interpretAst(ast, context, handlers as any); - return pipelineResponse; -}; diff --git a/src/legacy/ui/public/visualize/loader/types.ts b/src/legacy/ui/public/visualize/loader/types.ts deleted file mode 100644 index 525ec35834ecd..0000000000000 --- a/src/legacy/ui/public/visualize/loader/types.ts +++ /dev/null @@ -1,104 +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 { TimeRange } from 'src/plugins/data/public'; -import { Query } from 'src/legacy/core_plugins/data/public'; -import { SavedObject } from 'ui/saved_objects/saved_object'; -import { VisResponseValue } from 'src/plugins/visualizations/public'; -import { SearchSource } from '../../courier'; -import { PersistedState } from '../../persisted_state'; -import { AppState } from '../../state_management/app_state'; -import { Vis } from '../../vis'; -import { esFilters } from '../../../../../plugins/data/public'; - -export interface VisSavedObject extends SavedObject { - vis: Vis; - description?: string; - searchSource: SearchSource; - title: string; - uiStateJSON?: string; - destroy: () => void; -} - -export interface VisResponseData { - as: string; - value: VisResponseValue; -} - -/** - * The parameters accepted by the embedVisualize calls. - */ -export interface VisualizeLoaderParams { - /** - * An object with a from/to key, that must be either a date in ISO format, or a - * valid datetime Elasticsearch expression, e.g.: { from: 'now-7d/d', to: 'now' } - */ - timeRange?: TimeRange; - /** - * If set to true, the visualization will be appended to the passed element instead - * of replacing all its content. (default: false) - */ - append?: boolean; - /** - * If specified this CSS class (or classes with space separated) will be set to - * the root visualize element. - */ - cssClass?: string; - /** - * An object of key-value pairs, that will be set as data-{key}="{value}" attributes - * on the visualization element. - */ - dataAttrs?: { [key: string]: string }; - /** - * Specifies the filters that should be applied to that visualization. - */ - filters?: esFilters.Filter[]; - /** - * The query that should apply to that visualization. - */ - query?: Query; - /** - * The current uiState of the application. If you don't pass a uiState, the - * visualization will creates it's own uiState to store information like whether - * the legend is open or closed, but you don't have access to it from the outside. - * Pass one in if you need that access, e.g. for saving that state. - */ - uiState?: PersistedState; - /** - * The appState this visualization should use. If you don't specify it, the - * global AppState (that is decoded in the URL) will be used. Usually you don't - * need to overwrite this, unless you don't want the visualization to use the - * global AppState. - */ - appState?: AppState; - /** - * Whether or not the visualization should fetch its data automatically. If this is - * set to `false` the loader won't trigger a fetch on embedding or when an auto refresh - * cycle happens. Default value: `true` - */ - autoFetch?: boolean; -} - -/** - * The subset of properties allowed to update on an already embedded visualization. - */ -export type VisualizeUpdateParams = Pick< - VisualizeLoaderParams, - 'timeRange' | 'dataAttrs' | 'filters' | 'query' ->; 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 9f3aa190917d7..36759551a1723 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,14 +22,15 @@ 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 } from '../../../../../../plugins/data/public'; +import { esFilters, Query } from '../../../../../../plugins/data/public'; +import { SearchSource } from '../../../courier'; interface QueryGeohashBoundsParams { filters?: esFilters.Filter[]; query?: Query; + searchSource?: SearchSource; } /** @@ -47,7 +48,9 @@ export async function queryGeohashBounds(vis: Vis, params: QueryGeohashBoundsPar }); if (agg) { - const searchSource = vis.searchSource.createChild(); + const searchSource = params.searchSource + ? params.searchSource.createChild() + : new SearchSource(); searchSource.setField('size', 0); searchSource.setField('aggs', () => { const geoBoundsAgg = vis.getAggConfig().createAggConfig( diff --git a/src/legacy/ui/public/visualize/loader/vis.js b/src/legacy/ui/public/visualize/loader/vis.js index 85ab07528b846..29208563055d0 100644 --- a/src/legacy/ui/public/visualize/loader/vis.js +++ b/src/legacy/ui/public/visualize/loader/vis.js @@ -33,115 +33,109 @@ import { PersistedState } from '../../persisted_state'; import { start as visualizations } from '../../../../core_plugins/visualizations/public/np_ready/public/legacy'; +const visTypes = visualizations.types; -export function VisProvider(indexPatterns, 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 }); - }, +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/legacy/ui/public/visualize/loader/visualization_loader.tsx b/src/legacy/ui/public/visualize/loader/visualization_loader.tsx deleted file mode 100644 index 307ef0354f451..0000000000000 --- a/src/legacy/ui/public/visualize/loader/visualization_loader.tsx +++ /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 _ from 'lodash'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { PersistedState } from '../../persisted_state'; -import { Vis } from '../../vis'; -import { Visualization } from '../components/visualization'; - -interface VisualizationLoaderParams { - listenOnChange?: boolean; -} - -function renderVisualization( - element: HTMLElement, - vis: Vis, - visData: any, - visParams: any, - uiState: PersistedState, - params: VisualizationLoaderParams -) { - return new Promise(resolve => { - const listenOnChange = _.get(params, 'listenOnChange', false); - render( - , - element - ); - }); -} - -function destroy(element?: HTMLElement) { - if (element) { - unmountComponentAtNode(element); - } -} - -export const visualizationLoader = { - render: renderVisualization, - destroy, -}; diff --git a/src/legacy/ui/public/visualize/loader/visualize_loader.ts b/src/legacy/ui/public/visualize/loader/visualize_loader.ts deleted file mode 100644 index 086b16711a581..0000000000000 --- a/src/legacy/ui/public/visualize/loader/visualize_loader.ts +++ /dev/null @@ -1,159 +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. - */ - -/** - * IMPORTANT: If you make changes to this API, please make sure to check that - * the docs (docs/development/visualize/development-create-visualization.asciidoc) - * are up to date. - */ - -import chrome from '../../chrome'; -import { FilterBarQueryFilterProvider } from '../../filter_manager/query_filter'; -import { IPrivate } from '../../private'; -import { EmbeddedVisualizeHandler } from './embedded_visualize_handler'; -import { VisSavedObject, VisualizeLoaderParams } from './types'; - -export class VisualizeLoader { - constructor(private readonly savedVisualizations: any, private readonly Private: IPrivate) {} - - /** - * Renders a saved visualization specified by its id into a DOM element. - * - * @param element The DOM element to render the visualization into. - * You can alternatively pass a jQuery element instead. - * @param id The id of the saved visualization. This is the id of the - * saved object that is stored in the .kibana index. - * @param params A list of parameters that will influence rendering. - * - * @return A promise that resolves to the - * handler for this visualization as soon as the saved object could be found. - */ - public async embedVisualizationWithId( - element: HTMLElement, - savedVisualizationId: string, - params: VisualizeLoaderParams - ) { - return new Promise((resolve, reject) => { - this.savedVisualizations.get(savedVisualizationId).then((savedObj: VisSavedObject) => { - const handler = this.renderVis(element, savedObj, params); - resolve(handler); - }, reject); - }); - } - - /** - * Renders a saved visualization specified by its savedObject into a DOM element. - * In most of the cases you will need this method, since it allows you to specify - * filters, handlers, queries, etc. on the savedObject before rendering. - * - * We do not encourage you to use this method, since it will most likely be changed - * or removed in a future version of Kibana. Rather embed a visualization by its id - * via the {@link #embedVisualizationWithId} method. - * - * @deprecated You should rather embed by id, since this method will be removed in the future. - * @param element The DOM element to render the visualization into. - * You can alternatively pass a jQuery element instead. - * @param savedObj The savedObject as it could be retrieved by the - * `savedVisualizations` service. - * @param params A list of parameters that will influence rendering. - * - * @return The handler to the visualization. - */ - public embedVisualizationWithSavedObject( - el: HTMLElement, - savedObj: VisSavedObject, - params: VisualizeLoaderParams - ) { - return this.renderVis(el, savedObj, params); - } - - /** - * Returns a promise, that resolves to a list of all saved visualizations. - * - * @return Resolves with a list of all saved visualizations as - * returned by the `savedVisualizations` service in Kibana. - */ - public getVisualizationList(): Promise { - return this.savedVisualizations.find().then((result: any) => result.hits); - } - - private renderVis( - container: HTMLElement, - savedObj: VisSavedObject, - params: VisualizeLoaderParams - ) { - const { vis, description, searchSource } = savedObj; - - vis.description = description; - vis.searchSource = searchSource; - - if (!params.append) { - container.innerHTML = ''; - } - - const element = document.createElement('div'); - element.className = 'visualize'; - element.setAttribute('data-test-subj', 'visualizationLoader'); - container.appendChild(element); - // We need the container to have display: flex so visualization will render correctly - container.style.display = 'flex'; - - // If params specified cssClass, we will set this to the element. - if (params.cssClass) { - params.cssClass.split(' ').forEach(cssClass => { - element.classList.add(cssClass); - }); - } - - // Apply data- attributes to the element if specified - const dataAttrs = params.dataAttrs; - if (dataAttrs) { - Object.keys(dataAttrs).forEach(key => { - element.setAttribute(`data-${key}`, dataAttrs[key]); - }); - } - - const handlerParams = { - ...params, - // lets add query filter angular service to the params - queryFilter: this.Private(FilterBarQueryFilterProvider), - // lets add Private to the params, we'll need to pass it to visualize later - Private: this.Private, - }; - - return new EmbeddedVisualizeHandler(element, savedObj, handlerParams); - } -} - -function VisualizeLoaderProvider(savedVisualizations: any, Private: IPrivate) { - return new VisualizeLoader(savedVisualizations, Private); -} - -/** - * Returns a promise, that resolves with the visualize loader, once it's ready. - * @return A promise, that resolves to the visualize loader. - */ -function getVisualizeLoader(): Promise { - return chrome.dangerouslyGetActiveInjector().then($injector => { - const Private: IPrivate = $injector.get('Private'); - return Private(VisualizeLoaderProvider); - }); -} - -export { getVisualizeLoader, VisualizeLoaderProvider }; 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 02e5f45fae3bd..36efd0bcba676 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 8b258f3558438..6cefd11c912f1 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 eadf70a36416a..dbb5a06da9cd9 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 => ( { 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 15c5c9d4ad2e6..35110c924fe61 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 e4606695c0f6a..e207a3ff5961b 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 839e4f6359257..5a580db0c57b8 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 901dc724aa4e4..d2374162b195f 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 9008dc2a67294..017bb8e9cb7c5 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 d7931f191e52b..c2513a9dc0c5e 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 2814735061999..a242df4811c05 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 9eda75d8abd0b..ec8d8b006317f 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 1f2d8f914dde3..d838e54e9ead4 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 0000000000000..315894cd212c4 --- /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/ui/public/registry/dev_tools.js b/src/plugins/data/public/index_patterns/index_pattern.stub.ts similarity index 77% rename from src/legacy/ui/public/registry/dev_tools.js rename to src/plugins/data/public/index_patterns/index_pattern.stub.ts index 1741f39f86375..444e65cd0cd4b 100644 --- a/src/legacy/ui/public/registry/dev_tools.js +++ b/src/plugins/data/public/index_patterns/index_pattern.stub.ts @@ -17,11 +17,12 @@ * under the License. */ -import { uiRegistry } from './_registry'; - -export const DevToolsRegistryProvider = uiRegistry({ - name: 'devTools', - index: ['name'], - order: ['order'] -}); +import { IndexPattern } from '../../common'; +import { stubFields } from './field.stub'; +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 7955cdd825ee6..ce7a479151797 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 0000000000000..46cf0fd9c111e --- /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 0000000000000..5c4cdc2717338 --- /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 3afa3891a24bb..1847296016c73 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 9d7c2ffc56f70..224c2f3a04076 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 0000000000000..b74a1a07dc356 --- /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 15eebaa0b9fd6..fbb1726fc99ea 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 66424d9a1d6a3..67073a9078046 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 852f9fd269b32..19ac37fb59ae7 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 0000000000000..d13afa251ecb1 --- /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 1eb054ba40514..1fdb2d8ed03df 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 e0bc8f2c3525f..87c1ec29c1aee 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 deab9af4e3a01..fa7cdbcda3082 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 643ded120799e..d29f3b6882b26 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 0000000000000..27e6f9b48bbdd --- /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 0000000000000..200e5bacb7f18 --- /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/table_list_view/index.js b/src/plugins/data/public/stubs.ts similarity index 86% rename from src/legacy/core_plugins/kibana/public/table_list_view/index.js rename to src/plugins/data/public/stubs.ts index ae3e5d022c725..40a5e7d18f8d9 100644 --- a/src/legacy/core_plugins/kibana/public/table_list_view/index.js +++ b/src/plugins/data/public/stubs.ts @@ -17,5 +17,5 @@ * under the License. */ -export { TableListView } from './table_list_view'; - +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 eac380dde6a62..988b5fcd43fa8 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 13ccbbd9f3dde..7dc8ff0fe133d 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 03eaa5d9594d2..c769f64025b0e 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 0000000000000..1610411b9c98e --- /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 0000000000000..307035c7ec664 --- /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/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/filter.js b/src/plugins/dev_tools/public/index.ts similarity index 77% rename from src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/filter.js rename to src/plugins/dev_tools/public/index.ts index 1a2854ec15412..3a0d1455e2168 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/actions/filter.js +++ b/src/plugins/dev_tools/public/index.ts @@ -17,10 +17,11 @@ * under the License. */ -export function addFilter(field, values = [], operation, index, state, filterGen) { - if (!Array.isArray(values)) { - values = [values]; - } +import { PluginInitializerContext } from 'kibana/public'; +import { DevToolsPlugin } from './plugin'; - filterGen.add(field, values, operation, index); +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 0000000000000..8098308c0882b --- /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 b11bd167e15f2..70d7c99d3fb9d 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 417f3436a2c63..0c075c497a4d0 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 e288f61de8681..0bb89cc1af593 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 360182368ae63..3902b0615a33d 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 66c3e8d983f98..62867a0c07a6b 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/expression_types/number.ts b/src/plugins/expressions/public/expression_types/number.ts index 8434536f8f6b8..52b2bb1ff3194 100644 --- a/src/plugins/expressions/public/expression_types/number.ts +++ b/src/plugins/expressions/public/expression_types/number.ts @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { ExpressionType } from '../types'; import { Datatable } from './datatable'; import { Render } from './render'; @@ -28,7 +29,20 @@ export const number = (): ExpressionType => ({ from: { null: () => 0, boolean: b => Number(b), - string: n => Number(n), + string: n => { + const value = Number(n); + if (Number.isNaN(value)) { + throw new Error( + i18n.translate('expressions_np.types.number.fromStringConversionErrorMessage', { + defaultMessage: 'Can\'t typecast "{string}" string to number', + values: { + string: n, + }, + }) + ); + } + return value; + }, }, to: { render: (value: number): Render<{ text: string }> => { diff --git a/src/plugins/expressions/public/expression_types/tests/number.test.ts b/src/plugins/expressions/public/expression_types/tests/number.test.ts new file mode 100644 index 0000000000000..3336a1384ea79 --- /dev/null +++ b/src/plugins/expressions/public/expression_types/tests/number.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { number } from '../number'; + +describe('number', () => { + it('should fail when typecasting not numeric string to number', () => { + expect(() => number().from!.string('123test', {})).toThrowErrorMatchingInlineSnapshot( + `"Can't typecast \\"123test\\" string to number"` + ); + }); +}); diff --git a/src/plugins/expressions/public/interpreter_provider.ts b/src/plugins/expressions/public/interpreter_provider.ts index cb84370ad69c5..15d6b1c025f54 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/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 2d66216a9770b..87ef810682f60 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -91,6 +91,7 @@ export interface IExpressionLoaderParams { customFunctions?: []; customRenderers?: []; extraHandlers?: Record; + inspectorAdapters?: Adapters; } export interface IInterpreterHandlers { diff --git a/src/plugins/kibana_react/public/context/context.tsx b/src/plugins/kibana_react/public/context/context.tsx index cbae5c4638ca2..cbf2ad07b463e 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 cd2ae89b05b5d..2d82f646c827b 100644 --- a/src/plugins/kibana_react/public/index.ts +++ b/src/plugins/kibana_react/public/index.ts @@ -23,3 +23,5 @@ export * from './context'; 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 35c503d590b2c..4f64a2b95f512 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 28c1d5391d160..774f74863ee6f 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 e1e7f1c536342..bab710cdca595 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/legacy/ui/public/visualize/index.ts b/src/plugins/kibana_react/public/table_list_view/index.ts similarity index 95% rename from src/legacy/ui/public/visualize/index.ts rename to src/plugins/kibana_react/public/table_list_view/index.ts index 46a8968358294..d9a4db50ab7fb 100644 --- a/src/legacy/ui/public/visualize/index.ts +++ b/src/plugins/kibana_react/public/table_list_view/index.ts @@ -16,5 +16,4 @@ * specific language governing permissions and limitations * under the License. */ - -export * from './loader'; +export * from './table_list_view'; diff --git a/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx similarity index 66% rename from src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js rename to src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 3148a4a37c9c0..dde8efa7e1106 100644 --- a/src/legacy/core_plugins/kibana/public/table_list_view/table_list_view.js +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -18,13 +18,12 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import _ from 'lodash'; -import { toastNotifications } from 'ui/notify'; +import { debounce, indexBy, sortBy, uniq } from 'lodash'; import { EuiTitle, + // @ts-ignore EuiInMemoryTable, EuiPage, EuiPageBody, @@ -38,26 +37,67 @@ import { EuiConfirmModal, EuiCallOut, } from '@elastic/eui'; - -import { npStart } from 'ui/new_platform'; +import { ToastsStart, UiSettingsClientContract } from 'kibana/public'; +import { toMountPoint } from '../util'; export const EMPTY_FILTER = ''; +interface Column { + name: string; + width?: string; + actions?: object[]; +} + +interface Item { + id?: string; +} + +export interface TableListViewProps { + createItem?(): void; + deleteItems?(items: object[]): Promise; + editItem?(item: object): void; + entityName: string; + entityNamePlural: string; + findItems(query: string): Promise<{ total: number; hits: object[] }>; + listingLimit: number; + initialFilter: string; + noItemsFragment: JSX.Element; + // update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI + tableColumns: Column[]; + tableListTitle: string; + toastNotifications: ToastsStart; + uiSettings: UiSettingsClientContract; +} + +export interface TableListViewState { + items: object[]; + hasInitialFetchReturned: boolean; + isFetchingItems: boolean; + isDeletingItems: boolean; + showDeleteModal: boolean; + showLimitError: boolean; + filter: string; + selectedIds: string[]; + totalItems: number; +} + // saved object client does not support sorting by title because title is only mapped as analyzed // the legacy implementation got around this by pulling `listingLimit` items and doing client side sorting // and not supporting server-side paging. // This component does not try to tackle these problems (yet) and is just feature matching the legacy component // TODO support server side sorting/paging once title and description are sortable on the server. -class TableListViewUi extends React.Component { +class TableListView extends React.Component { + private pagination = {}; + private _isMounted = false; - constructor(props) { + constructor(props: TableListViewProps) { super(props); - const initialPageSize = npStart.core.uiSettings.get('savedObjects:perPage'); + const initialPageSize = props.uiSettings.get('savedObjects:perPage'); this.pagination = { initialPageIndex: 0, initialPageSize, - pageSizeOptions: _.uniq([10, 20, 50, initialPageSize]).sort(), + pageSizeOptions: uniq([10, 20, 50, initialPageSize]).sort(), }; this.state = { items: [], @@ -67,10 +107,9 @@ class TableListViewUi extends React.Component { isDeletingItems: false, showDeleteModal: false, showLimitError: false, - filter: this.props.initialFilter, + filter: props.initialFilter, selectedIds: [], }; - } componentWillMount() { @@ -86,7 +125,7 @@ class TableListViewUi extends React.Component { this.fetchItems(); } - debouncedFetch = _.debounce(async (filter) => { + debouncedFetch = debounce(async (filter: string) => { const response = await this.props.findItems(filter); if (!this._isMounted) { @@ -100,7 +139,7 @@ class TableListViewUi extends React.Component { this.setState({ hasInitialFetchReturned: true, isFetchingItems: false, - items: (!filter ? _.sortBy(response.hits, 'title') : response.hits), + items: !filter ? sortBy(response.hits, 'title') : response.hits, totalItems: response.total, showLimitError: response.total > this.props.listingLimit, }); @@ -108,26 +147,29 @@ class TableListViewUi extends React.Component { }, 300); fetchItems = () => { - this.setState({ - isFetchingItems: true, - }, this.debouncedFetch.bind(null, this.state.filter)); - } + this.setState( + { + isFetchingItems: true, + }, + this.debouncedFetch.bind(null, this.state.filter) + ); + }; deleteSelectedItems = async () => { - if (this.state.isDeletingItems) { + if (this.state.isDeletingItems || !this.props.deleteItems) { return; } this.setState({ - isDeletingItems: true + isDeletingItems: true, }); try { - const itemsById = _.indexBy(this.state.items, 'id'); + const itemsById = indexBy(this.state.items, 'id'); await this.props.deleteItems(this.state.selectedIds.map(id => itemsById[id])); } catch (error) { - toastNotifications.addDanger({ - title: ( + this.props.toastNotifications.addDanger({ + title: toMountPoint( @@ -138,25 +180,28 @@ class TableListViewUi extends React.Component { this.fetchItems(); this.setState({ isDeletingItems: false, - selectedIds: [] + selectedIds: [], }); this.closeDeleteModal(); - } + }; closeDeleteModal = () => { this.setState({ showDeleteModal: false }); - } + }; openDeleteModal = () => { this.setState({ showDeleteModal: true }); - } + }; - setFilter(filter) { + setFilter({ queryText }: { queryText: string }) { // If the user is searching, we want to clear the sort order so that // results are ordered by Elasticsearch's relevance. - this.setState({ - filter: filter.queryText, - }, this.fetchItems); + this.setState( + { + filter: queryText, + }, + this.fetchItems + ); } hasNoItems() { @@ -170,14 +215,14 @@ class TableListViewUi extends React.Component { renderConfirmDeleteModal() { let deleteButton = ( ); if (this.state.isDeletingItems) { deleteButton = ( ); @@ -188,11 +233,14 @@ class TableListViewUi extends React.Component { } @@ -201,7 +249,7 @@ class TableListViewUi extends React.Component { onConfirm={this.deleteSelectedItems} cancelButtonText={ } @@ -210,7 +258,7 @@ class TableListViewUi extends React.Component { >

@@ -227,7 +275,7 @@ class TableListViewUi extends React.Component { } @@ -236,26 +284,22 @@ class TableListViewUi extends React.Component { >

- listingLimit - - ), + listingLimitText: listingLimit, advancedSettingsLink: ( - ) + ), }} />

@@ -268,18 +312,15 @@ class TableListViewUi extends React.Component { renderNoItemsMessage() { if (this.props.noItemsFragment) { - return ( - this.props.noItemsFragment - ); + return this.props.noItemsFragment; } else { return ( ); - } } @@ -302,11 +343,12 @@ class TableListViewUi extends React.Component { data-test-subj="deleteSelectedItems" > @@ -314,25 +356,34 @@ class TableListViewUi extends React.Component { } renderTable() { - const selection = this.props.deleteItems ? { - onSelectionChange: (selection) => { - this.setState({ - selectedIds: selection.map(item => { return item.id; }) - }); - } - } : null; - - const actions = [{ - name: i18n.translate('kbn.table_list_view.listing.table.editActionName', { - defaultMessage: 'Edit' - }), - description: i18n.translate('kbn.table_list_view.listing.table.editActionDescription', { - defaultMessage: 'Edit' - }), - icon: 'pencil', - type: 'icon', - onClick: this.props.editItem - }]; + const selection = this.props.deleteItems + ? { + onSelectionChange: (obj: Item[]) => { + this.setState({ + selectedIds: obj + .map(item => item.id) + .filter((id: undefined | string): id is string => Boolean(id)), + }); + }, + } + : null; + + const actions = [ + { + name: i18n.translate('kibana-react.tableListView.listing.table.editActionName', { + defaultMessage: 'Edit', + }), + description: i18n.translate( + 'kibana-react.tableListView.listing.table.editActionDescription', + { + defaultMessage: 'Edit', + } + ), + icon: 'pencil', + type: 'icon', + onClick: this.props.editItem, + }, + ]; const search = { onChange: this.setFilter.bind(this), @@ -346,17 +397,17 @@ class TableListViewUi extends React.Component { const columns = this.props.tableColumns.slice(); if (this.props.editItem) { columns.push({ - name: i18n.translate('kbn.table_list_view.listing.table.actionTitle', { - defaultMessage: 'Actions' + name: i18n.translate('kibana-react.tableListView.listing.table.actionTitle', { + defaultMessage: 'Actions', }), width: '100px', - actions + actions, }); } const noItemsMessage = ( @@ -397,7 +448,7 @@ class TableListViewUi extends React.Component { fill > @@ -412,14 +463,11 @@ class TableListViewUi extends React.Component { -

- {this.props.tableListTitle} -

+

{this.props.tableListTitle}

{createButton} -
@@ -450,34 +498,10 @@ class TableListViewUi extends React.Component { className="itemListing__page" restrictWidth > - - {this.renderPageContent()} - + {this.renderPageContent()} ); } } -TableListViewUi.propTypes = { - tableColumns: PropTypes.array.isRequired, - - noItemsFragment: PropTypes.object, - - findItems: PropTypes.func.isRequired, - deleteItems: PropTypes.func, - createItem: PropTypes.func, - editItem: PropTypes.func, - - listingLimit: PropTypes.number, - initialFilter: PropTypes.string, - - entityName: PropTypes.string.isRequired, - entityNamePlural: PropTypes.string.isRequired, - tableListTitle: PropTypes.string.isRequired, -}; - -TableListViewUi.defaultProps = { - initialFilter: EMPTY_FILTER, -}; - -export const TableListView = injectI18n(TableListViewUi); +export { TableListView }; diff --git a/src/plugins/kibana_react/public/util/index.ts b/src/plugins/kibana_react/public/util/index.ts index a9219196e84a9..1053ca01603e3 100644 --- a/src/plugins/kibana_react/public/util/index.ts +++ b/src/plugins/kibana_react/public/util/index.ts @@ -19,3 +19,4 @@ export * from './use_observable'; export * from './use_unmount'; +export * from './react_mount'; diff --git a/src/plugins/kibana_react/public/util/react_mount.tsx b/src/plugins/kibana_react/public/util/react_mount.tsx new file mode 100644 index 0000000000000..e3f16e0e3bfa1 --- /dev/null +++ b/src/plugins/kibana_react/public/util/react_mount.tsx @@ -0,0 +1,40 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { MountPoint } from 'kibana/public'; + +/** + * MountPoint converter for react nodes. + * + * @param node to get a mount point for + */ +export const toMountPoint = (node: React.ReactNode): MountPoint => { + 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 0000000000000..45ad4cb407175 --- /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/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/index.ts b/src/plugins/newsfeed/constants.ts similarity index 81% rename from src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/index.ts rename to src/plugins/newsfeed/constants.ts index 63636433bc00b..ddcbbb6cb1dbe 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/index.ts +++ b/src/plugins/newsfeed/constants.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { renderTelemetryOptInCard, Props } from './telemetry_opt_in_card'; -export const TelemetryOptInCard = (props: Props) => { - return renderTelemetryOptInCard(props); -}; +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 0000000000000..9d49b42424a06 --- /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 0000000000000..8764b7664d449 --- /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 0000000000000..2e88b0053535e --- /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 0000000000000..33702df00a583 --- /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 0000000000000..cec18e0bdec43 --- /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 0000000000000..8a99abe18a75f --- /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 0000000000000..ca449b8ee879e --- /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 0000000000000..fcbc7970377d4 --- /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 0000000000000..da042f0fce7b6 --- /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 0000000000000..1217de60d9638 --- /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 0000000000000..4383b9e0f7dab --- /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 0000000000000..6920dd9b2bccc --- /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 0000000000000..5ea5e5b324717 --- /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 0000000000000..78485c6ee4f59 --- /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 31656df2cb644..f5a1e63617dfa 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/index_patterns/es_errors/lib/get_es_errors.js b/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js index f303bca0da574..df221a09a661c 100644 --- a/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js +++ b/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js @@ -36,7 +36,6 @@ export async function getDocNotFoundError(es) { try { await es.get({ index: 'basic_index', - type: 'type', id: '1234' }); } catch (err) { diff --git a/test/api_integration/apis/ui_metric/ui_metric.js b/test/api_integration/apis/ui_metric/ui_metric.js index efa6be47b50c9..f0c86f2904638 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 44e4bef99bf62..58161e545bd06 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 0000000000000..beee9bb5c6069 --- /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 0000000000000..2a7ea3793324d --- /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 0000000000000..5291b1031b0a9 --- /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 f0d34207a87a2..0b2b4f14f126d 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 0000000000000..35d7ac8adefa5 --- /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 17c93680088cb..f3f564fbd2919 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/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.js index ce5f968a37115..217e6d4c1a8d3 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.js @@ -56,7 +56,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.setScriptedFieldScript(`doc['bytes'].value`); const response = await es.update({ index: '.kibana', - type: '_doc', id: 'index-pattern:logstash-*', body: { 'doc': { 'index-pattern': { 'fieldFormatMap': '{"geo.src":{"id":"number"}}' } } @@ -83,7 +82,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.setFieldFormat('url'); const response = await es.update({ index: '.kibana', - type: '_doc', id: 'index-pattern:logstash-*', body: { 'doc': { 'index-pattern': { 'fieldFormatMap': '{"geo.dest":{"id":"number"}}' } } diff --git a/test/functional/apps/visualize/_tile_map.js b/test/functional/apps/visualize/_tile_map.js index 7719ecca56a65..0e580f6a7ab3f 100644 --- a/test/functional/apps/visualize/_tile_map.js +++ b/test/functional/apps/visualize/_tile_map.js @@ -221,7 +221,7 @@ export default function ({ getService, getPageObjects }) { it('when not checked does not add filters to aggregation', async () => { await PageObjects.visualize.toggleOpenEditor(2); - await PageObjects.visualize.toggleIsFilteredByCollarCheckbox(); + await PageObjects.visualize.setIsFilteredByCollarCheckbox(false); await PageObjects.visualize.clickGo(); await inspector.open(); await inspector.expectTableHeaders(['geohash_grid', 'Count', 'Geo Centroid']); @@ -229,7 +229,7 @@ export default function ({ getService, getPageObjects }) { }); after(async () => { - await PageObjects.visualize.toggleIsFilteredByCollarCheckbox(); + await PageObjects.visualize.setIsFilteredByCollarCheckbox(true); await PageObjects.visualize.clickGo(); }); }); 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 b659d29b158b7..4088ab6193a59 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 ca141114f976d..af3a15e9b3015 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 1e8c454f42cfe..84562990191d1 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 0000000000000..24ff21f0b47de --- /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 570511bee4bc5..4b65de57f12d8 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 67494f201adae..81d26a4b69478 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`); @@ -1007,6 +1029,16 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli await testSubjects.click('isFilteredByCollarCheckbox'); } + async setIsFilteredByCollarCheckbox(value = true) { + await retry.try(async () => { + const isChecked = await this.isSwitchChecked('isFilteredByCollarCheckbox'); + if (isChecked !== value) { + await testSubjects.click('isFilteredByCollarCheckbox'); + throw new Error('isFilteredByCollar not set correctly'); + } + }); + } + async getMarkdownData() { const markdown = await retry.try(async () => find.byCssSelector('visualize')); return await markdown.getVisibleText(); diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index 97e02958f3787..a8ce4270d4205 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 164ea999fa279..df3aac67f22a1 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 b6b68cc0d3cf9..71e2711906fce 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 f134fde028e09..d6de0be0c172e 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 766e6168002c2..da1bb597f5730 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/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js index bd58184cd1185..b0db26c0c6743 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/app.js @@ -24,9 +24,6 @@ import { uiModules } from 'ui/modules'; import chrome from 'ui/chrome'; import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; -import { runPipeline } from 'ui/visualize/loader/pipeline_helpers'; -import { visualizationLoader } from 'ui/visualize/loader/visualization_loader'; - import { registries } from 'plugins/interpreter/registries'; // This is required so some default styles and required scripts/Angular modules are loaded, @@ -58,6 +55,17 @@ app.config(stateManagementConfigProvider => stateManagementConfigProvider.disable() ); +import { fromExpression } from '@kbn/interpreter/common'; +import { getInterpreter } from '../../../../../src/legacy/core_plugins/interpreter/public/interpreter'; + +const runPipeline = async (expression, context, handlers) => { + const ast = fromExpression(expression); + const { interpreter } = await getInterpreter(); + const pipelineResponse = await interpreter.interpretAst(ast, context, handlers); + return pipelineResponse; +}; + + function RootController($scope, $element) { const domNode = $element[0]; @@ -67,7 +75,6 @@ function RootController($scope, $element) { DataAdapter={DataAdapter} runPipeline={runPipeline} registries={registries} - visualizationLoader={visualizationLoader} />, domNode); // unmount react on controller destroy diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js index 3b1744457c25a..62ba8dd16fef4 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/components/main.js @@ -64,7 +64,6 @@ class Main extends React.Component { this.setState({ expression: 'Renderer was not found in registry!\n\n' + JSON.stringify(context) }); return resolve(); } - props.visualizationLoader.destroy(this.chartDiv); const renderCompleteHandler = () => { resolve('render complete'); this.chartDiv.removeEventListener('renderComplete', renderCompleteHandler); diff --git a/test/interpreter_functional/test_suites/run_pipeline/index.js b/test/interpreter_functional/test_suites/run_pipeline/index.js index 3c1ce2314f55f..ebc0568ebb955 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/index.js +++ b/test/interpreter_functional/test_suites/run_pipeline/index.js @@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects, loadTestFile }) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'header']); - describe('runPipeline', function () { + describe.skip('runPipeline', function () { this.tags(['skipFirefox']); before(async () => { diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index e5ad767349358..a6316c607a7c7 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -32,7 +32,6 @@ export default async function ({ readConfigFile }) { testFiles: [ require.resolve('./test_suites/app_plugins'), require.resolve('./test_suites/custom_visualizations'), - require.resolve('./test_suites/embedding_visualizations'), require.resolve('./test_suites/panel_actions'), require.resolve('./test_suites/search'), 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 0000000000000..a8a5616627726 --- /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 0000000000000..eff6c1e1f142a --- /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 0000000000000..556a9ca140715 --- /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/src/legacy/core_plugins/console/public/quarantined/hacks/register.js b/test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts similarity index 70% rename from src/legacy/core_plugins/console/public/quarantined/hacks/register.js rename to test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts index b5df1c1af99c5..6e9959ecbdf9e 100644 --- a/src/legacy/core_plugins/console/public/quarantined/hacks/register.js +++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts @@ -17,14 +17,14 @@ * under the License. */ -import { DevToolsRegistryProvider } from 'ui/registry/dev_tools'; -import { i18n } from '@kbn/i18n'; +import { PluginInitializer } from 'kibana/public'; +import { + CorePluginChromelessPlugin, + CorePluginChromelessPluginSetup, + CorePluginChromelessPluginStart, +} from './plugin'; -DevToolsRegistryProvider.register(() => ({ - order: 1, - name: 'console', - display: i18n.translate('console.consoleDisplayName', { - defaultMessage: 'Console', - }), - url: '#/dev_tools/console', -})); +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 c7cb877be676b..03870410fb334 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 0000000000000..5fcaeafbb0d85 --- /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 377163251010c..298eaaaf420e0 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 acb75b15196d6..d3f2360add6c0 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 7c5b6f6be58af..4d0444265825a 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 ef472b4026957..196e64af39985 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 277bb09ac745c..33e60128d0806 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/plugins/kbn_tp_visualize_embedding/index.js b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/index.js deleted file mode 100644 index 1ec4ea2b9e096..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/index.js +++ /dev/null @@ -1,39 +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. - */ - -export default function (kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - title: 'Embedding Vis', - description: 'This is a sample plugin to test embedding of visualizations', - main: 'plugins/kbn_tp_visualize_embedding/app', - } - }, - - init(server) { - // The following lines copy over some configuration variables from Kibana - // to this plugin. This will be needed when embedding visualizations, so that e.g. - // region map is able to get its configuration. - server.injectUiAppVars('kbn_tp_visualize_embedding', async () => { - return await server.getInjectedUiAppVars('kibana'); - }); - } - }); -} diff --git a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json deleted file mode 100644 index f248a7e4d1f2d..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "kbn_tp_visualize_embedding", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0", - "dependencies": { - "@elastic/eui": "14.8.0", - "react": "^16.8.0", - "react-dom": "^16.8.0" - } -} diff --git a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/app.js b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/app.js deleted file mode 100644 index 4463feac27513..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/app.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; - -// This is required so some default styles and required scripts/Angular modules are loaded, -// or the timezone setting is correctly applied. -import 'ui/autoload/all'; - -// These are all the required uiExports you need to import in case you want to embed visualizations. -import 'uiExports/visTypes'; -import 'uiExports/visResponseHandlers'; -import 'uiExports/visRequestHandlers'; -import 'uiExports/visEditorTypes'; -import 'uiExports/visualize'; -import 'uiExports/savedObjectTypes'; -import 'uiExports/fieldFormats'; -import 'uiExports/search'; - -import { Main } from './components/main'; - -const app = uiModules.get('apps/firewallDemoPlugin', ['kibana']); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -function RootController($scope, $element) { - const domNode = $element[0]; - - // render react to DOM - render(
, domNode); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('firewallDemoPlugin', RootController); diff --git a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/components/main.js b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/components/main.js deleted file mode 100644 index 677708dfe6e97..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/components/main.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiLoadingChart, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentBody, - EuiPageContentHeader, - EuiSelect, -} from '@elastic/eui'; - -import { embeddingSamples } from '../embedding'; - -const VISUALIZATION_OPTIONS = [ - { value: '', text: '' }, - { value: 'timebased', text: 'Time based' }, - { value: 'timebased_with-filters', text: 'Time based (with filters)' }, - { value: 'timebased_no-datehistogram', text: 'Time based data without date histogram' } -]; - -class Main extends React.Component { - - chartDiv = React.createRef(); - state = { - loading: false, - selectedParams: null, - selectedVis: null, - }; - - embedVisualization = async () => { - if (this.handler) { - // Whenever a visualization is about to be removed from DOM that you embedded, - // you need to call `destroy` on the handler to make sure the visualization is - // teared down correctly. - this.handler.destroy(); - this.chartDiv.current.innerHTML = ''; - } - - const { selectedParams, selectedVis } = this.state; - if (selectedParams && selectedVis) { - this.setState({ loading: true }); - const sample = embeddingSamples.find(el => el.id === selectedParams); - this.handler = await sample.run(this.chartDiv.current, selectedVis); - // handler.whenFirstRenderComplete() will return a promise that resolves once the first - // rendering after embedding has finished. - await this.handler.whenFirstRenderComplete(); - this.setState({ loading: false }); - } - } - - onChangeVisualization = async (ev) => { - this.setState({ - selectedVis: ev.target.value, - }, this.embedVisualization); - }; - - onSelectSample = async (ev) => { - this.setState({ - selectedParams: ev.target.value, - }, this.embedVisualization); - }; - - render() { - const samples = [ - { value: '', text: '' }, - ...embeddingSamples.map(({ id, title }) => ({ - value: id, - text: title, - })) - ]; - - return ( - - - - - - - - - - - - - - - - { this.state.loading && - - - - } - - - - {/* - The element you want to render into should have its dimension set (via a fixed height, flexbox, absolute positioning, etc.), - since the visualization will render with exactly the size of that element, i.e. the container size determines the - visualization size. - */} -
- - - - - ); - } -} - -export { Main }; diff --git a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/embedding.js b/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/embedding.js deleted file mode 100644 index 190e6331837b9..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_visualize_embedding/public/embedding.js +++ /dev/null @@ -1,187 +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. - */ - -/** - * This files shows a couple of examples how to use the visualize loader API - * to embed visualizations. - */ - -import { getVisualizeLoader } from 'ui/visualize'; -import chrome from 'ui/chrome'; - -export const embeddingSamples = [ - - { - id: 'none', - title: 'No parameters', - async run(domNode, id) { - // You always need to retrieve the visualize loader for embedding visualizations. - const loader = await getVisualizeLoader(); - // Use the embedVisualizationWithId method to embed a visualization by its id. The id is the id of the - // saved object in the .kibana index (you can find the id via Management -> Saved Objects). - // - // Pass in a DOM node that you want to embed that visualization into. Note: the loader will - // use the size of that DOM node. - // - // The call will return a handler for the visualization with methods to interact with it. - // Check the components/main.js file to see how this handler is used. Most important: you need to call - // `destroy` on the handler once you are about to remove the visualization from the DOM. - // - // Note: If the visualization you want to embed contains date histograms with an auto interval, you need - // to specify the timeRange parameter (see below). - return loader.embedVisualizationWithId(domNode, id, {}); - } - }, { - id: 'timerange', - title: 'timeRange', - async run(domNode, id) { - const loader = await getVisualizeLoader(); - // If you want to filter down the data to a specific time range, you can specify a - // timeRange in the parameters to the embedding call. - // You can either use an absolute time range as seen below. You can also specify - // a datemath string, like "now-7d", "now-1w/w" for the from or to key. - // You can also directly assign a moment JS or regular JavaScript Date object. - return loader.embedVisualizationWithId(domNode, id, { - timeRange: { - from: '2015-09-20 20:00:00.000', - to: '2015-09-21 20:00:00.000', - } - }); - } - }, { - id: 'query', - title: 'query', - async run(domNode, id) { - const loader = await getVisualizeLoader(); - // You can specify a query that should filter down the data via the query parameter. - // It must have a language key which must be one of the supported query languages of Kibana, - // which are at the moment: 'lucene' or 'kquery'. - // The query key must then hold the actual query in the specified language for filtering. - return loader.embedVisualizationWithId(domNode, id, { - query: { - language: 'lucene', - query: 'extension.raw:jpg', - } - }); - } - }, { - id: 'filters', - title: 'filters', - async run(domNode, id) { - const loader = await getVisualizeLoader(); - // You can specify an array of filters that should apply to the query. - // The format of a filter must match the format the filter bar is using internally. - // This has a query key, which holds the query part of an Elasticsearch query - // and a meta key allowing to set some meta values, most important for this API - // the `negate` option to negate the filter. - return loader.embedVisualizationWithId(domNode, id, { - filters: [ - { - query: { - bool: { - should: [ - { match_phrase: { 'extension.raw': 'jpg' } }, - { match_phrase: { 'extension.raw': 'png' } }, - ] - } - }, - meta: { - negate: true - } - } - ] - }); - } - }, { - id: 'filters_query_timerange', - title: 'filters & query & timeRange', - async run(domNode, id) { - const loader = await getVisualizeLoader(); - // You an of course combine timeRange, query and filters options all together - // to filter the data in the embedded visualization. - return loader.embedVisualizationWithId(domNode, id, { - timeRange: { - from: '2015-09-20 20:00:00.000', - to: '2015-09-21 20:00:00.000', - }, - query: { - language: 'lucene', - query: 'bytes:>2000' - }, - filters: [ - { - query: { - bool: { - should: [ - { match_phrase: { 'extension.raw': 'jpg' } }, - { match_phrase: { 'extension.raw': 'png' } }, - ] - } - }, - meta: { - negate: true - } - } - ] - }); - } - }, { - id: 'savedobject_filter_query_timerange', - title: 'filters & query & time (use saved object)', - async run(domNode, id) { - const loader = await getVisualizeLoader(); - // Besides embedding via the id of the visualizataion, the API offers the possibility to - // embed via the saved visualization object. - // - // WE ADVISE YOU NOT TO USE THIS INSIDE ANY PLUGIN! - // - // Since the format of the saved visualization object will change in the future and because - // this still requires you to talk to old Angular code, we do not encourage you to use this - // way of embedding in any plugin. It's likely it will be removed or changed in a future version. - const $injector = await chrome.dangerouslyGetActiveInjector(); - const savedVisualizations = $injector.get('savedVisualizations'); - const savedVis = await savedVisualizations.get(id); - return loader.embedVisualizationWithSavedObject(domNode, savedVis, { - timeRange: { - from: '2015-09-20 20:00:00.000', - to: '2015-09-21 20:00:00.000', - }, - query: { - language: 'lucene', - query: 'bytes:>2000' - }, - filters: [ - { - query: { - bool: { - should: [ - { match_phrase: { 'extension.raw': 'jpg' } }, - { match_phrase: { 'extension.raw': 'png' } }, - ] - } - }, - meta: { - negate: true - } - } - ] - }); - } - } -]; diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts index eec2ec019a515..c16847dab9dc2 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/test/plugin_functional/test_suites/embedding_visualizations/embed_by_id.js b/test/plugin_functional/test_suites/embedding_visualizations/embed_by_id.js deleted file mode 100644 index c877ec2e5e025..0000000000000 --- a/test/plugin_functional/test_suites/embedding_visualizations/embed_by_id.js +++ /dev/null @@ -1,189 +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 { delay } from 'bluebird'; - -export default function ({ getService }) { - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const table = getService('table'); - const retry = getService('retry'); - - async function selectVis(id) { - await testSubjects.click('visSelect'); - await find.clickByCssSelector(`option[value="${id}"]`); - } - - async function selectParams(id) { - await testSubjects.click('embeddingParamsSelect'); - await find.clickByCssSelector(`option[value="${id}"]`); - await retry.try(async () => { - await testSubjects.waitForDeleted('visLoadingIndicator'); - }); - await delay(1000); - } - - async function getTableData() { - const data = await table.getDataFromTestSubj('paginated-table-body'); - // Strip away empty rows (at the bottom) - return data.filter(row => !row.every(cell => !cell.trim())); - } - - describe('embed by id', function describeIndexTests() { - describe('vis on timebased data without date histogram', () => { - before(async () => { - await selectVis('timebased_no-datehistogram'); - }); - - it('should correctly embed', async () => { - await selectParams('none'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['jpg', '9,109'], - ['css', '2,159'], - ['png', '1,373'], - ['gif', '918'], - ['php', '445'], - ]); - }); - - it('should correctly embed specifying a timeRange', async () => { - await selectParams('timerange'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['jpg', '3,005'], - ['css', '720'], - ['png', '455'], - ['gif', '300'], - ['php', '142'], - ]); - }); - - it('should correctly embed specifying a query', async () => { - await selectParams('query'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['jpg', '9,109'], - ]); - }); - - it('should correctly embed specifying filters', async () => { - await selectParams('filters'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['css', '2,159'], - ['gif', '918'], - ['php', '445'], - ]); - }); - - it('should correctly embed specifying filters and query and timeRange', async () => { - await selectParams('filters_query_timerange'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['css', '678'], - ['php', '110'], - ]); - }); - }); - - describe('vis on timebased data with date histogram with interval auto', () => { - before(async () => { - await selectVis('timebased'); - }); - - it('should correctly embed specifying a timeRange', async () => { - await selectParams('timerange'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['2015-09-20 20:00', '45.159KB', '5.65KB'], - ['2015-09-21 00:00', '42.428KB', '5.345KB'], - ['2015-09-21 04:00', '43.717KB', '5.35KB'], - ['2015-09-21 08:00', '43.228KB', '5.538KB'], - ['2015-09-21 12:00', '42.83KB', '5.669KB'], - ['2015-09-21 16:00', '44.908KB', '5.673KB'], - ]); - }); - - it('should correctly embed specifying filters and query and timeRange', async () => { - await selectParams('filters_query_timerange'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['2015-09-20 20:00', '45.391KB', '5.692KB'], - ['2015-09-21 00:00', '46.57KB', '5.953KB'], - ['2015-09-21 04:00', '47.339KB', '6.636KB'], - ['2015-09-21 08:00', '40.5KB', '6.133KB'], - ['2015-09-21 12:00', '41.31KB', '5.84KB'], - ['2015-09-21 16:00', '48.012KB', '6.003KB'], - ]); - }); - }); - - describe('vis on timebased data with date histogram with interval auto and saved filters', () => { - before(async () => { - await selectVis('timebased_with-filters'); - }); - - it('should correctly embed specifying a timeRange', async () => { - await selectParams('timerange'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['2015-09-20 20:00', '21.221KB', '2.66KB'], - ['2015-09-21 00:00', '22.054KB', '2.63KB'], - ['2015-09-21 04:00', '15.592KB', '2.547KB'], - ['2015-09-21 08:00', '4.656KB', '2.328KB'], - ['2015-09-21 12:00', '17.887KB', '2.694KB'], - ['2015-09-21 16:00', '20.533KB', '2.529KB'], - ]); - }); - - it('should correctly embed specifying filters and query and timeRange', async () => { - await selectParams('filters_query_timerange'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['2015-09-20 20:00', '24.567KB', '3.498KB'], - ['2015-09-21 00:00', '25.984KB', '3.589KB'], - ['2015-09-21 04:00', '2.543KB', '2.543KB'], - ['2015-09-21 12:00', '5.783KB', '2.927KB'], - ['2015-09-21 16:00', '21.107KB', '3.44KB'], - ]); - }); - }); - - describe('vis visa saved object on timebased data with date histogram with interval auto and saved filters', () => { - before(async () => { - await selectVis('timebased_with-filters'); - }); - - it('should correctly embed specifying filters and query and timeRange', async () => { - await selectParams('savedobject_filter_query_timerange'); - const data = await getTableData(); - expect(data).to.be.eql([ - ['2015-09-20 20:00', '24.567KB', '3.498KB'], - ['2015-09-21 00:00', '25.984KB', '3.589KB'], - ['2015-09-21 04:00', '2.543KB', '2.543KB'], - ['2015-09-21 12:00', '5.783KB', '2.927KB'], - ['2015-09-21 16:00', '21.107KB', '3.44KB'], - ]); - }); - }); - }); - -} diff --git a/test/plugin_functional/test_suites/embedding_visualizations/index.js b/test/plugin_functional/test_suites/embedding_visualizations/index.js deleted file mode 100644 index b54a500fcd1f2..0000000000000 --- a/test/plugin_functional/test_suites/embedding_visualizations/index.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default function ({ getService, getPageObjects, loadTestFile }) { - const browser = getService('browser'); - const appsMenu = getService('appsMenu'); - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['common', 'header']); - - describe('embedding visualizations', function () { - before(async () => { - await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/logstash_functional'); - await esArchiver.load('../functional/fixtures/es_archiver/visualize_embedding'); - await kibanaServer.uiSettings.replace({ - 'dateFormat:tz': 'Australia/North', - 'defaultIndex': 'logstash-*', - 'format:bytes:defaultPattern': '0,0.[000]b' - }); - await browser.setWindowSize(1300, 900); - await PageObjects.common.navigateToApp('settings'); - await appsMenu.clickLink('Embedding Vis'); - }); - - loadTestFile(require.resolve('./embed_by_id')); - }); -} 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 19a00632ca192..1f5f770e8447c 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 0000000000000..2aa81f34028a0 --- /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 0fba7d2cefbd5..9d601e680cf87 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 a85da7a69534c..322966b3c982e 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 8e5fba31ac5a4..8becf6892ff92 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 3e71725713070..a5bf42bc2cc01 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/actions/server/shim.ts b/x-pack/legacy/plugins/actions/server/shim.ts index 0da6b84f2cc69..1af62d276f10b 100644 --- a/x-pack/legacy/plugins/actions/server/shim.ts +++ b/x-pack/legacy/plugins/actions/server/shim.ts @@ -42,7 +42,7 @@ export interface KibanaConfig { */ export type TaskManagerStartContract = Pick; export type XPackMainPluginSetupContract = Pick; -export type SecurityPluginSetupContract = Pick; +export type SecurityPluginSetupContract = Pick; export type SecurityPluginStartContract = Pick; export type TaskManagerSetupContract = Pick< TaskManager, diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index d86eab2038095..0ee1ef843d7d0 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -41,7 +41,7 @@ export interface Server extends Legacy.Server { * Shim what we're thinking setup and start contracts will look like */ export type TaskManagerStartContract = Pick; -export type SecurityPluginSetupContract = Pick; +export type SecurityPluginSetupContract = Pick; export type SecurityPluginStartContract = Pick; export type XPackMainPluginSetupContract = Pick; export type TaskManagerSetupContract = Pick< 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 0000000000000..ac43b700117c6 --- /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 9a8f11c6493c5..522f6d39ac71a 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/apm/common/transaction_types.ts b/x-pack/legacy/plugins/apm/common/transaction_types.ts new file mode 100644 index 0000000000000..4dd59af63047d --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/transaction_types.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const 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 556bce9d37bb5..bfbfb4bb99c6a 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 0b31798242fad..02296606b1c01 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 53f1893a168ac..69f0cf61af242 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 291208b2d9032..d52c869b95872 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/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts index f9c160f4031df..1a15be1f65dec 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts @@ -33,7 +33,6 @@ export const esResponse = { hits: [ { _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _type: 'doc', _id: 'qH7C_WIBcmGuKeCHJvvT', _score: null, _source: { @@ -61,7 +60,6 @@ export const esResponse = { hits: [ { _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _type: 'doc', _id: '_3_D_WIBcmGuKeCHFwOW', _score: null, _source: { @@ -93,7 +91,6 @@ export const esResponse = { hits: [ { _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _type: 'doc', _id: 'dn_D_WIBcmGuKeCHQgXJ', _score: null, _source: { @@ -125,7 +122,6 @@ export const esResponse = { hits: [ { _index: 'apm-7.0.0-alpha1-error-2018.04.25', - _type: 'doc', _id: 'dX_D_WIBcmGuKeCHQgXJ', _score: null, _source: { 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 276d309cbb3e3..8005fc17f2a20 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 b69076b3a1f70..a118871a5e268 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 d03e70fc99cc6..0702e092a714f 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 db0ddb56e7088..fc86f4bb78afb 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 d81b7417570a5..f016052df56a2 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 7c0b6f24f87a7..9918f162a01f4 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 6f67b2458ea10..0aaeae3e4ce44 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 bdf895f423913..4398c129aa7b8 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 7e68b2f84eead..4378c7fdeee0c 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 ca14be237d22b..b7963b5c75a92 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 52afdffcb0839..d765a57a56a18 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 7b9586634c7d0..50c94fe88e6ad 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 12872fd64a3c4..adcce161c7ac1 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 40c6150149eb5..6176397170797 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 132067a6c32b7..94f30a8a2325a 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 b794332d4aa63..6b3fa1f0d98f7 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 ba74b0175ff71..bc6382841be3f 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 80a1b96efb3d6..2b0263f69db8f 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 a18882120fe75..a224df9e59e58 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 6db6e8848ef07..26cae303542a4 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 54106cce10bac..0000000000000 --- 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 754666b0a9fa2..640072d6ec4d8 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 8a91bd8781fe7..0000000000000 --- 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 b7081c43465bf..5bbd6be14a708 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 ee41599454dd6..9c111910f16f9 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 3af1d8c706f46..c685ffdd801dc 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 81dd8b34c8847..f164ca39d51c0 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 57de438be7f2a..f43b9ea11487a 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 3ec519d5e71b5..dcc034287863a 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 0b9407b288b1d..1aff1d772c5c2 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 861732ee03923..434eda8c0f46e 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 25a4f5141498f..23faa4b74cf8f 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 896363c054ba7..ed20a58b271e1 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 d5aa389cea335..52efc2b50305b 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 283f30b51441d..dd4d019ef7263 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 e5349edb67f30..b7b9c21172140 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.mocks.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts index 982077e2e6665..1ed7f56e0b10d 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts @@ -22,7 +22,6 @@ export const searchMocks = { hits: [ { _index: '.apm-agent-configuration', - _type: '_doc', _id: '-aQHsm0BxZLczArvNQYW', _score: 0.9808292, _source: { @@ -39,7 +38,6 @@ export const searchMocks = { }, { _index: '.apm-agent-configuration', - _type: '_doc', _id: '-KQHsm0BxZLczArvNAb0', _score: 0.18232156, _source: { @@ -56,7 +54,6 @@ export const searchMocks = { }, { _index: '.apm-agent-configuration', - _type: '_doc', _id: '96QHsm0BxZLczArvNAbD', _score: 0.0, _source: { 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 400bd0207771a..dcf7329b229d8 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 35d76d745cf4f..969bbc542f8a6 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 cd237a5264099..e942a26da373e 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 8de47c5c44144..e57e64942ab89 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 99553690359cf..ca10183bb259e 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 e092942a25ba6..ed6bdf203f2d4 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 67816d67a29a2..2648851789c66 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 a21c4f38ac30c..3d425415de832 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 cddc66e52cf70..3b9e80c901fe9 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_anomaly_data/mock-responses/mlBucketSpanResponse.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlBucketSpanResponse.ts index a4e54d240f204..4c329fa9d1035 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlBucketSpanResponse.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/charts/get_anomaly_data/mock-responses/mlBucketSpanResponse.ts @@ -19,7 +19,6 @@ export const mlBucketSpanResponse = { hits: [ { _index: '.ml-anomalies-shared', - _type: 'doc', _id: 'opbeans-go-request-high_mean_response_time_model_plot_1542636000000_900_0_29791_0', _score: 1.0, 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 5056a100de3ce..0345b0815679f 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 5d10a4ae27060..a0149bec728c5 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 0458c8e4fedf0..e1cb1774469f2 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 b0461f5cb3b68..18fe547a34cf0 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 f969e4d6024ca..66f28a9bf6d44 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 100df4dc238fe..92e1284f3ed74 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 85d53925db86e..4b955c7a6e981 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 d25ad949d6dde..2867cef28d952 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 3c82a35ec7903..4afcf135a1a76 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 cde9fd1dd4ca9..0b5c29fc29857 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 a0ddffe044c15..77d96d3677494 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 9d36946d29cf6..36508e53acce7 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 9f17b0197a5b2..b694e9526e2b8 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 56cd0ff23a3fb..eff39838bd957 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 92137bdf5cc6e..8972084018d98 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 0d5c67d09da8a..bc77abc9815be 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 479be32ce1e12..3ac2ff72c0116 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/breadcrumb.tsx b/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx index 6e74fac3af49b..7948501f5f873 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Component } from 'react'; -import { RouteProps } from 'react-router'; +import { RouteProps } from 'react-router-dom'; import { BASE_PATH } from '../../../../common/constants'; import { BreadcrumbConsumer } from './consumer'; import { Breadcrumb as BreadcrumbData, BreadcrumbContext } from './types'; 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 8736663dd08af..f15e08c2ca230 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 a93000a3e80f0..6f03f884563e1 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/frontend_types.d.ts b/x-pack/legacy/plugins/beats_management/public/frontend_types.d.ts index bcaac2b3781aa..21996d10be231 100644 --- a/x-pack/legacy/plugins/beats_management/public/frontend_types.d.ts +++ b/x-pack/legacy/plugins/beats_management/public/frontend_types.d.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { RouteComponentProps } from 'react-router'; +import { RouteComponentProps } from 'react-router-dom'; import { BeatsContainer } from './containers/beats'; import { TagsContainer } from './containers/tags'; import { URLStateProps } from './containers/with_url_state'; 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 4a7f768bd4740..b2e11461007fd 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 31c55b9272193..190c9e265463d 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 8274764e759ab..92cfcc935ad9b 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 4718abc1b7153..83129384a77df 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 cfcc2c4eda397..df9c65034115b 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 bb0376ab87d29..156304443431d 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 c80c37bb1ed56..e592c6d22ef73 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 bfc3212fa234c..a5b65e6bdc62b 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 f44d37abe166f..74c13be3abd7b 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 c180b6186ee92..718f2d8b11e1a 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 5889864df3c1d..5d5c3c1a735a0 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 9229def146f8b..042cbe83fb10f 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 bf68d217f18ab..0b9358714e71c 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 eb32881bc1f6d..068854866dc1b 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 6a7a89231f250..4bb68973e80ea 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 16ca644160c0a..d40c5f787e44f 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 8ecde2cc721e3..f26b3510c7682 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 8789577c92f63..f0a5325baf3c0 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 b3aa9c393096b..65b811fe68134 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 69c4463e7d210..6c05eec0c3c09 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 2c7820df26ca9..7fb73b1672eb1 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 e85ce8c10da64..89c0b5b21c581 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 26a0d92655362..dabf06a24aeb6 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 20d19d2fcbd82..8faaf278a07de 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 8a040c0c90c83..612406c30f88e 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 6ae4ef984264f..4bcdb547186d1 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 32e9cbbd1d91d..774214cf68cec 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 a0692584d4986..a51e6b4b5d987 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 06507e4eb9a71..ef5de3feb575c 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 51d856b0fc7a6..430d6a5343662 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 825ea8a4f2494..9de5f81b440ba 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 f60aad1fa3e72..ac282962afb54 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 723b30aac37fa..fca663f8532d8 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 2b966e7bd6bb8..c6dddab3b5dd1 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 b13740f177a23..aa3bbdce97863 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/custom_interval.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx index 0bcb0c89ba1cc..ab34f332dc126 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx @@ -54,7 +54,7 @@ export const CustomInterval = ({ gutterSize, buttonSize, onSubmit, defaultValue - + { 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 6d3c9a0640ba2..d2fece567a8ad 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 718fec9f59f77..53c053811a273 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 406d6b54729b9..b22a9d35aa793 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 34c3d446c5ca7..731656dd4e095 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 56ea35a6887ec..4ee3a65172a2e 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 893caba1465b8..46a97f7c15d74 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 4b99a5ef298d8..d3da1b5553958 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 1005cc60e50ba..7b7e87b027af5 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/api/__tests__/__snapshots__/shareable.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap index 01a8337d2d313..782d5364de821 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with default propertie "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" @@ -18,7 +18,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" @@ -30,7 +30,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2` "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" @@ -42,7 +42,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" @@ -54,7 +54,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2 "
markdown mock
markdown mock
markdown mock
My Canvas Workpad
" diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot index 1b351e9ac0f6a..c3352b52c591d 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot @@ -88,9 +88,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -126,9 +126,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -165,9 +165,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -205,9 +205,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -249,9 +249,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -290,9 +290,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -332,9 +332,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -368,9 +368,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -418,9 +418,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -471,9 +471,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -509,9 +509,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -545,9 +545,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -582,9 +582,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -620,9 +620,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -658,9 +658,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -698,9 +698,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -735,9 +735,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -772,9 +772,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -811,9 +811,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -862,9 +862,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -905,9 +905,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -996,9 +996,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1065,9 +1065,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1105,9 +1105,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1146,9 +1146,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1188,9 +1188,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1228,9 +1228,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1268,9 +1268,9 @@ exports[`Storyshots shareables/Canvas component 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1312,6 +1312,11 @@ exports[`Storyshots shareables/Canvas component 1`] = ` >
@@ -1620,9 +1625,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1659,9 +1664,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1699,9 +1704,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1743,9 +1748,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1784,9 +1789,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1826,9 +1831,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1862,9 +1867,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1912,9 +1917,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1965,9 +1970,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2003,9 +2008,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2039,9 +2044,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2076,9 +2081,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2114,9 +2119,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2152,9 +2157,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2192,9 +2197,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2229,9 +2234,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2266,9 +2271,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2305,9 +2310,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2356,9 +2361,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2399,9 +2404,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2490,9 +2495,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2559,9 +2564,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2599,9 +2604,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2640,9 +2645,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2682,9 +2687,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2722,9 +2727,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2762,9 +2767,9 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -2806,6 +2811,11 @@ exports[`Storyshots shareables/Canvas contextual: austin 1`] = ` >
@@ -3116,6 +3126,11 @@ exports[`Storyshots shareables/Canvas contextual: hello 1`] = ` >
App renders properly 1`] = ` "
markdown mock
" 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 db0aac34336ea..9cf2ddc3a22e3 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/canvas.module.scss b/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss index 88619c150c1f5..f7e47d8ddeb2b 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/canvas.module.scss @@ -18,5 +18,7 @@ :global .kbnCanvas :local .page { position: absolute; - transform-origin: center center; + transform-origin: left top; + top: 0; + left: 0; } diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot index bad95ca4fb5b5..6570016336d9e 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot @@ -41,9 +41,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -79,9 +79,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -118,9 +118,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -158,9 +158,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -202,9 +202,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -243,9 +243,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -285,9 +285,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -321,9 +321,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -371,9 +371,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -424,9 +424,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -462,9 +462,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -498,9 +498,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -535,9 +535,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -573,9 +573,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -611,9 +611,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -651,9 +651,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -688,9 +688,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -725,9 +725,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -764,9 +764,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -815,9 +815,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -858,9 +858,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -949,9 +949,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1018,9 +1018,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1058,9 +1058,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1099,9 +1099,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1141,9 +1141,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1181,9 +1181,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1221,9 +1221,9 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1265,6 +1265,11 @@ exports[`Storyshots shareables/Footer contextual: austin 1`] = ` >
@@ -1529,6 +1534,11 @@ exports[`Storyshots shareables/Footer contextual: hello 1`] = ` >
@@ -92,9 +92,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -130,9 +130,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -169,9 +169,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -209,9 +209,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -253,9 +253,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -294,9 +294,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -336,9 +336,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -372,9 +372,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -422,9 +422,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -475,9 +475,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -513,9 +513,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -549,9 +549,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -586,9 +586,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -624,9 +624,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -662,9 +662,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -702,9 +702,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -739,9 +739,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -776,9 +776,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -815,9 +815,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -866,9 +866,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -909,9 +909,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1000,9 +1000,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1069,9 +1069,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1109,9 +1109,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1150,9 +1150,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1192,9 +1192,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1232,9 +1232,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1272,9 +1272,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: austin 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 177.77777777777777, + "width": 1280, } } > @@ -1333,9 +1333,9 @@ exports[`Storyshots shareables/Footer/Scrubber contextual: hello 1`] = ` className="preview" style={ Object { - "height": 100, + "height": 720, "transform": "scale3d(0.1388888888888889, 0.1388888888888889, 1)", - "width": 150, + "width": 1080, } } > diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot index 6bb60b8e574a2..8d0ced56d1474 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot @@ -19,6 +19,11 @@ exports[`Storyshots shareables/Footer/Title component 1`] = ` >
= ({ workpadWidth, }) => { const scale = height / workpadHeight; - const style = { - height: workpadHeight * scale, - width: workpadWidth * scale, - }; const transform = { - ...style, + height: workpadHeight, + width: workpadWidth, transform: `scale3d(${scale}, ${scale}, 1)`, }; @@ -73,7 +70,10 @@ export const PagePreviewComponent: FC = ({ className={css.root} onClick={() => onClick(index)} onKeyPress={() => onClick(index)} - style={style} + style={{ + height: workpadHeight * scale, + width: workpadWidth * scale, + }} >
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 7c4395820776e..1e66e19b3c0e1 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,
- - - - - + + + + - -

-
- -
@@ -200,49 +193,52 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings component: on, 5
- - - - - + + + + - -

-
- -
@@ -375,49 +361,52 @@ exports[`Storyshots shareables/Footer/Settings/AutoplaySettings contextual 1`] =
- - - - - + + + + - -

-
- -
diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.examples.storyshot b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.examples.storyshot index a40f699ddecd8..cdc7323902b82 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.examples.storyshot @@ -31,53 +31,55 @@ exports[`Storyshots shareables/Footer/Settings/ToolbarSettings component: off 1`
- - - - - + + + + - -
- - - - - + + + + - -
- - - - - + + + + - -
can navigate Autoplay Settings 1`] = ` data-focus-lock-disabled="disabled" >