diff --git a/.ci/jobs.yml b/.ci/jobs.yml index fe28ae79268de..3f1b5302f87b0 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -1,21 +1,34 @@ JOB: - - intake - - firefoxSmoke + - kibana-intake + - x-pack-intake + - kibana-firefoxSmoke - kibana-ciGroup1 - kibana-ciGroup2 - kibana-ciGroup3 - kibana-ciGroup4 - kibana-ciGroup5 - kibana-ciGroup6 - # - kibana-visualRegression + - kibana-ciGroup7 + - kibana-ciGroup8 + - kibana-ciGroup9 + - kibana-ciGroup10 + - kibana-ciGroup11 + - kibana-ciGroup12 + - kibana-visualRegression # make sure all x-pack-ciGroups are listed in test/scripts/jenkins_xpack_ci_group.sh + - x-pack-firefoxSmoke - x-pack-ciGroup1 - x-pack-ciGroup2 - x-pack-ciGroup3 - x-pack-ciGroup4 - x-pack-ciGroup5 - # - x-pack-visualRegression + - x-pack-ciGroup6 + - x-pack-ciGroup7 + - x-pack-ciGroup8 + - x-pack-ciGroup9 + - x-pack-ciGroup10 + - x-pack-visualRegression # `~` is yaml for `null` exclude: ~ \ No newline at end of file diff --git a/.ci/run.sh b/.ci/run.sh index e5c26c48546f7..88ce0bd9986a1 100755 --- a/.ci/run.sh +++ b/.ci/run.sh @@ -11,7 +11,7 @@ source src/dev/ci_setup/setup.sh source src/dev/ci_setup/checkout_sibling_es.sh case "$JOB" in -intake) +kibana-intake) ./test/scripts/jenkins_unit.sh ;; kibana-ciGroup*) @@ -21,9 +21,12 @@ kibana-ciGroup*) kibana-visualRegression*) ./test/scripts/jenkins_visual_regression.sh ;; -firefoxSmoke*) +kibana-firefoxSmoke*) ./test/scripts/jenkins_firefox_smoke.sh ;; +x-pack-intake) + ./test/scripts/jenkins_xpack.sh + ;; x-pack-ciGroup*) export CI_GROUP="${JOB##x-pack-ciGroup}" ./test/scripts/jenkins_xpack_ci_group.sh @@ -31,6 +34,9 @@ x-pack-ciGroup*) x-pack-visualRegression*) ./test/scripts/jenkins_xpack_visual_regression.sh ;; +x-pack-firefoxSmoke*) + ./test/scripts/jenkins_xpack_firefox_smoke.sh + ;; *) echo "JOB '$JOB' is not implemented." exit 1 diff --git a/.eslintrc.js b/.eslintrc.js index 8a641af8856c4..63f30b427e162 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,6 +31,8 @@ const ELASTIC_LICENSE_HEADER = ` `; module.exports = { + root: true, + extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], overrides: [ diff --git a/docs/development/core/public/kibana-plugin-public.app.md b/docs/development/core/public/kibana-plugin-public.app.md new file mode 100644 index 0000000000000..60cac357d1fe0 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.app.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [App](./kibana-plugin-public.app.md) + +## App interface + +Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. + +Signature: + +```typescript +export interface App extends AppBase +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [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.app.mount.md b/docs/development/core/public/kibana-plugin-public.app.mount.md new file mode 100644 index 0000000000000..dda06b035db4a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.app.mount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [App](./kibana-plugin-public.app.md) > [mount](./kibana-plugin-public.app.mount.md) + +## App.mount property + +A mount function called when the user navigates to this app's route. + +Signature: + +```typescript +mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.capabilities.md b/docs/development/core/public/kibana-plugin-public.appbase.capabilities.md new file mode 100644 index 0000000000000..450972e41bb29 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.capabilities.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [capabilities](./kibana-plugin-public.appbase.capabilities.md) + +## AppBase.capabilities property + +Custom capabilities defined by the app. + +Signature: + +```typescript +capabilities?: Partial; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.euiicontype.md b/docs/development/core/public/kibana-plugin-public.appbase.euiicontype.md new file mode 100644 index 0000000000000..99c7e852ff905 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.euiicontype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) + +## AppBase.euiIconType property + +A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property. + +Signature: + +```typescript +euiIconType?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.icon.md b/docs/development/core/public/kibana-plugin-public.appbase.icon.md new file mode 100644 index 0000000000000..d94d0897bc5b7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.icon.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [icon](./kibana-plugin-public.appbase.icon.md) + +## AppBase.icon property + +A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided. + +Signature: + +```typescript +icon?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.id.md b/docs/development/core/public/kibana-plugin-public.appbase.id.md new file mode 100644 index 0000000000000..57daa0c94bdf6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [id](./kibana-plugin-public.appbase.id.md) + +## AppBase.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.md b/docs/development/core/public/kibana-plugin-public.appbase.md new file mode 100644 index 0000000000000..338d30e780aaf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) + +## AppBase interface + + +Signature: + +```typescript +export interface AppBase +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [capabilities](./kibana-plugin-public.appbase.capabilities.md) | Partial<Capabilities> | Custom capabilities defined by the app. | +| [euiIconType](./kibana-plugin-public.appbase.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | +| [icon](./kibana-plugin-public.appbase.icon.md) | string | A URL to an image file used as an icon. Used as a fallback if euiIconType is not provided. | +| [id](./kibana-plugin-public.appbase.id.md) | string | | +| [order](./kibana-plugin-public.appbase.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [title](./kibana-plugin-public.appbase.title.md) | string | The title of the application. | +| [tooltip$](./kibana-plugin-public.appbase.tooltip$.md) | Observable<string> | An observable for a tooltip shown when hovering over app link. | + diff --git a/docs/development/core/public/kibana-plugin-public.appbase.order.md b/docs/development/core/public/kibana-plugin-public.appbase.order.md new file mode 100644 index 0000000000000..dc0ea14a7b860 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.order.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [order](./kibana-plugin-public.appbase.order.md) + +## AppBase.order property + +An ordinal used to sort nav links relative to one another for display. + +Signature: + +```typescript +order?: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.title.md b/docs/development/core/public/kibana-plugin-public.appbase.title.md new file mode 100644 index 0000000000000..4d0fb0c18e814 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.title.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [title](./kibana-plugin-public.appbase.title.md) + +## AppBase.title property + +The title of the application. + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appbase.tooltip$.md b/docs/development/core/public/kibana-plugin-public.appbase.tooltip$.md new file mode 100644 index 0000000000000..1b8ca490825f9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appbase.tooltip$.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppBase](./kibana-plugin-public.appbase.md) > [tooltip$](./kibana-plugin-public.appbase.tooltip$.md) + +## AppBase.tooltip$ property + +An observable for a tooltip shown when hovering over app link. + +Signature: + +```typescript +tooltip$?: Observable; +``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.md index a3ab77e43446c..b53873bc0fb8a 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.md @@ -15,5 +15,6 @@ export interface ApplicationSetup | Method | Description | | --- | --- | -| [registerApp(app)](./kibana-plugin-public.applicationsetup.registerapp.md) | Register an mountable application to the system. Apps will be mounted based on their rootRoute. | +| [register(app)](./kibana-plugin-public.applicationsetup.register.md) | Register an mountable application to the system. | +| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. | diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md similarity index 52% rename from docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md rename to docs/development/core/public/kibana-plugin-public.applicationsetup.register.md index f2532ae71ca2f..b4ccb6a01c600 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.registerapp.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md @@ -1,22 +1,22 @@ -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerApp](./kibana-plugin-public.applicationsetup.registerapp.md) +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [register](./kibana-plugin-public.applicationsetup.register.md) -## ApplicationSetup.registerApp() method +## ApplicationSetup.register() method -Register an mountable application to the system. Apps will be mounted based on their `rootRoute`. +Register an mountable application to the system. Signature: ```typescript -registerApp(app: App): void; +register(app: App): void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| app | App | | +| app | App | an [App](./kibana-plugin-public.app.md) | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md new file mode 100644 index 0000000000000..0b5bd8eeb36ec --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) > [registerMountContext](./kibana-plugin-public.applicationsetup.registermountcontext.md) + +## ApplicationSetup.registerMountContext() method + +Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. + +Signature: + +```typescript +registerMountContext(contextName: T, provider: IContextProvider): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | +| provider | IContextProvider<AppMountContext, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md b/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md deleted file mode 100644 index 8bbd1dfcd31fa..0000000000000 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.availableapps.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) - -## ApplicationStart.availableApps property - -Apps available based on the current capabilities. Should be used to show navigation links and make routing decisions. - -Signature: - -```typescript -availableApps: readonly App[]; -``` diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md new file mode 100644 index 0000000000000..422fbdf7418c2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.geturlforapp.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [getUrlForApp](./kibana-plugin-public.applicationstart.geturlforapp.md) + +## ApplicationStart.getUrlForApp() method + +Returns a relative URL to a given app, including the global base path. + +Signature: + +```typescript +getUrlForApp(appId: string, options?: { + path?: string; + }): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appId | string | | +| options | {
path?: string;
} | | + +Returns: + +`string` + diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 5854a7c65714e..2a60ff449e44e 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -15,6 +15,13 @@ export interface ApplicationStart | Property | Type | Description | | --- | --- | --- | -| [availableApps](./kibana-plugin-public.applicationstart.availableapps.md) | readonly App[] | Apps available based on the current capabilities. Should be used to show navigation links and make routing decisions. | | [capabilities](./kibana-plugin-public.applicationstart.capabilities.md) | RecursiveReadonly<Capabilities> | Gets the read-only capabilities. | +## Methods + +| Method | Description | +| --- | --- | +| [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. | +| [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigiate to a given app | +| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. | + diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md b/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md new file mode 100644 index 0000000000000..eef31fe661f54 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.navigatetoapp.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [navigateToApp](./kibana-plugin-public.applicationstart.navigatetoapp.md) + +## ApplicationStart.navigateToApp() method + +Navigiate to a given app + +Signature: + +```typescript +navigateToApp(appId: string, options?: { + path?: string; + state?: any; + }): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appId | string | | +| options | {
path?: string;
state?: any;
} | | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md new file mode 100644 index 0000000000000..fc86aaf658b68 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ApplicationStart](./kibana-plugin-public.applicationstart.md) > [registerMountContext](./kibana-plugin-public.applicationstart.registermountcontext.md) + +## ApplicationStart.registerMountContext() method + +Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. + +Signature: + +```typescript +registerMountContext(contextName: T, provider: IContextProvider): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | +| provider | IContextProvider<AppMountContext, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | + +Returns: + +`void` + diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md new file mode 100644 index 0000000000000..63b3ead814f00 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.core.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountContext](./kibana-plugin-public.appmountcontext.md) > [core](./kibana-plugin-public.appmountcontext.core.md) + +## AppMountContext.core property + +Core service APIs available to mounted applications. + +Signature: + +```typescript +core: { + application: Pick; + chrome: ChromeStart; + docLinks: DocLinksStart; + http: HttpStart; + i18n: I18nStart; + notifications: NotificationsStart; + overlays: OverlayStart; + uiSettings: UiSettingsClientContract; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.md new file mode 100644 index 0000000000000..c6541e3eca392 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountContext](./kibana-plugin-public.appmountcontext.md) + +## AppMountContext interface + +The context object received when applications are mounted to the DOM. + +Signature: + +```typescript +export interface AppMountContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [core](./kibana-plugin-public.appmountcontext.core.md) | {
application: Pick<ApplicationStart, 'capabilities' | 'navigateToApp'>;
chrome: ChromeStart;
docLinks: DocLinksStart;
http: HttpStart;
i18n: I18nStart;
notifications: NotificationsStart;
overlays: OverlayStart;
uiSettings: UiSettingsClientContract;
} | Core service APIs available to mounted applications. | + diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md new file mode 100644 index 0000000000000..16c8ffe07fc15 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md @@ -0,0 +1,53 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) > [appBasePath](./kibana-plugin-public.appmountparameters.appbasepath.md) + +## AppMountParameters.appBasePath property + +The base path for configuring the application's router. + +Signature: + +```typescript +appBasePath: string; +``` + +## Example + +How to configure react-router with a base path: + +```ts +// inside your plugin's setup function +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); + }, + }); +} + +``` + +```ts +// application.tsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter, Route } from 'react-router-dom'; + +export renderApp = (context, { appBasePath, element }) => { + ReactDOM.render( + // pass `appBasePath` to `basename` + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +} + +``` + diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.element.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.element.md new file mode 100644 index 0000000000000..dbe496c01c215 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.element.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) > [element](./kibana-plugin-public.appmountparameters.element.md) + +## AppMountParameters.element property + +The container element to render the application into. + +Signature: + +```typescript +element: HTMLElement; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.md new file mode 100644 index 0000000000000..8733f9cd4915d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountParameters](./kibana-plugin-public.appmountparameters.md) + +## AppMountParameters interface + + +Signature: + +```typescript +export interface AppMountParameters +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [appBasePath](./kibana-plugin-public.appmountparameters.appbasepath.md) | string | The base path for configuring the application's router. | +| [element](./kibana-plugin-public.appmountparameters.element.md) | HTMLElement | The container element to render the application into. | + diff --git a/docs/development/core/public/kibana-plugin-public.appunmount.md b/docs/development/core/public/kibana-plugin-public.appunmount.md new file mode 100644 index 0000000000000..61782d19ca8c5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appunmount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppUnmount](./kibana-plugin-public.appunmount.md) + +## AppUnmount type + +A function called when an application should be unmounted from the page. This function should be synchronous. + +Signature: + +```typescript +export declare type AppUnmount = () => void; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md index e4e2ad2c7a3a7..1fef9fc1dc359 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.order.md @@ -9,5 +9,5 @@ An ordinal used to sort nav links relative to one another for display. Signature: ```typescript -readonly order: number; +readonly order?: number; ``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.application.md b/docs/development/core/public/kibana-plugin-public.coresetup.application.md new file mode 100644 index 0000000000000..4b39b2c76802b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.coresetup.application.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [application](./kibana-plugin-public.coresetup.application.md) + +## CoreSetup.application property + +[ApplicationSetup](./kibana-plugin-public.applicationsetup.md) + +Signature: + +```typescript +application: ApplicationSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index a4b5b88df36dc..9b94e2db52831 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -16,6 +16,7 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | +| [application](./kibana-plugin-public.coresetup.application.md) | ApplicationSetup | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | [context](./kibana-plugin-public.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-public.contextsetup.md) | | [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | FatalErrorsSetup | [FatalErrorsSetup](./kibana-plugin-public.fatalerrorssetup.md) | | [http](./kibana-plugin-public.coresetup.http.md) | HttpSetup | [HttpSetup](./kibana-plugin-public.httpsetup.md) | diff --git a/docs/development/core/public/kibana-plugin-public.corestart.application.md b/docs/development/core/public/kibana-plugin-public.corestart.application.md index 1dd05ff947aeb..c26701ca80529 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.application.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.application.md @@ -9,5 +9,5 @@ Signature: ```typescript -application: Pick; +application: ApplicationStart; ``` diff --git a/docs/development/core/public/kibana-plugin-public.corestart.md b/docs/development/core/public/kibana-plugin-public.corestart.md index 446e458735214..5c1626958c4df 100644 --- a/docs/development/core/public/kibana-plugin-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-public.corestart.md @@ -16,7 +16,7 @@ export interface CoreStart | Property | Type | Description | | --- | --- | --- | -| [application](./kibana-plugin-public.corestart.application.md) | Pick<ApplicationStart, 'capabilities'> | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | +| [application](./kibana-plugin-public.corestart.application.md) | ApplicationStart | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | [chrome](./kibana-plugin-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-public.chromestart.md) | | [docLinks](./kibana-plugin-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-public.doclinksstart.md) | | [http](./kibana-plugin-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-public.httpstart.md) | diff --git a/docs/development/core/public/kibana-plugin-public.legacycoresetup.injectedmetadata.md b/docs/development/core/public/kibana-plugin-public.legacycoresetup.injectedmetadata.md new file mode 100644 index 0000000000000..f71277e64ff17 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacycoresetup.injectedmetadata.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) > [injectedMetadata](./kibana-plugin-public.legacycoresetup.injectedmetadata.md) + +## LegacyCoreSetup.injectedMetadata property + +> Warning: This API is now obsolete. +> +> + +Signature: + +```typescript +injectedMetadata: InjectedMetadataSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.legacycoresetup.md b/docs/development/core/public/kibana-plugin-public.legacycoresetup.md new file mode 100644 index 0000000000000..f704bc65d12a5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacycoresetup.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) + +## LegacyCoreSetup interface + +> Warning: This API is now obsolete. +> +> + +Setup interface exposed to the legacy platform via the `ui/new_platform` module. + +Signature: + +```typescript +export interface LegacyCoreSetup extends CoreSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [injectedMetadata](./kibana-plugin-public.legacycoresetup.injectedmetadata.md) | InjectedMetadataSetup | | + +## Remarks + +Some methods are not supported in the legacy platform and while present to make this type compatibile with [CoreSetup](./kibana-plugin-public.coresetup.md), unsupported methods will throw exceptions when called. + diff --git a/docs/development/core/public/kibana-plugin-public.legacycorestart.injectedmetadata.md b/docs/development/core/public/kibana-plugin-public.legacycorestart.injectedmetadata.md new file mode 100644 index 0000000000000..cd818c3f5adc7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacycorestart.injectedmetadata.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) > [injectedMetadata](./kibana-plugin-public.legacycorestart.injectedmetadata.md) + +## LegacyCoreStart.injectedMetadata property + +> Warning: This API is now obsolete. +> +> + +Signature: + +```typescript +injectedMetadata: InjectedMetadataStart; +``` diff --git a/docs/development/core/public/kibana-plugin-public.legacycorestart.md b/docs/development/core/public/kibana-plugin-public.legacycorestart.md new file mode 100644 index 0000000000000..775c3fb1ffe3d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.legacycorestart.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) + +## LegacyCoreStart interface + +> Warning: This API is now obsolete. +> +> + +Start interface exposed to the legacy platform via the `ui/new_platform` module. + +Signature: + +```typescript +export interface LegacyCoreStart extends CoreStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [injectedMetadata](./kibana-plugin-public.legacycorestart.injectedmetadata.md) | InjectedMetadataStart | | + +## Remarks + +Some methods are not supported in the legacy platform and while present to make this type compatibile with [CoreStart](./kibana-plugin-public.corestart.md), unsupported methods will throw exceptions when called. + diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 5fda9f9159306..ccabdc62c5e7a 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -23,8 +23,12 @@ The plugin integrates with the core system via lifecycle events: `setup` | Interface | Description | | --- | --- | +| [App](./kibana-plugin-public.app.md) | Extension of [common app properties](./kibana-plugin-public.appbase.md) with the mount function. | +| [AppBase](./kibana-plugin-public.appbase.md) | | | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | +| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. | +| [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | | | [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | @@ -54,6 +58,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | +| [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | +| [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyNavLink](./kibana-plugin-public.legacynavlink.md) | | | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | | [NotificationsStart](./kibana-plugin-public.notificationsstart.md) | | @@ -80,6 +86,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | +| [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [ChromeHelpExtension](./kibana-plugin-public.chromehelpextension.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-public.chromenavlinkupdateablefields.md) | | | [HttpBody](./kibana-plugin-public.httpbody.md) | | diff --git a/docs/discover/images/saved-query-management-component-all-privileges.png b/docs/discover/images/saved-query-management-component-all-privileges.png new file mode 100644 index 0000000000000..d3746248f6e27 Binary files /dev/null and b/docs/discover/images/saved-query-management-component-all-privileges.png differ diff --git a/docs/discover/images/saved-query-management-component-delete-query-button.png b/docs/discover/images/saved-query-management-component-delete-query-button.png new file mode 100644 index 0000000000000..23af8022069b6 Binary files /dev/null and b/docs/discover/images/saved-query-management-component-delete-query-button.png differ diff --git a/docs/discover/images/saved-query-management-component-save-as-new-query.png b/docs/discover/images/saved-query-management-component-save-as-new-query.png new file mode 100644 index 0000000000000..4ba4d1d660268 Binary files /dev/null and b/docs/discover/images/saved-query-management-component-save-as-new-query.png differ diff --git a/docs/discover/images/saved-query-management-component-save-as-new.png b/docs/discover/images/saved-query-management-component-save-as-new.png new file mode 100644 index 0000000000000..0899df5c608f5 Binary files /dev/null and b/docs/discover/images/saved-query-management-component-save-as-new.png differ diff --git a/docs/discover/images/saved-query-save-form-default-filters.png b/docs/discover/images/saved-query-save-form-default-filters.png new file mode 100644 index 0000000000000..948e474251a37 Binary files /dev/null and b/docs/discover/images/saved-query-save-form-default-filters.png differ diff --git a/docs/discover/search.asciidoc b/docs/discover/search.asciidoc index 9286b3ce31ba5..790cc669927c1 100644 --- a/docs/discover/search.asciidoc +++ b/docs/discover/search.asciidoc @@ -55,10 +55,10 @@ query language you can also submit queries using the {ref}/query-dsl.html[Elasti [[save-open-search]] -=== Saving and Opening Searches -Saving searches enables you to reload them into Discover and use them as the basis -for <>. Saving a search saves both the search query string -and the currently selected index pattern. +=== Saving searches +A saved search persists your current view of Discover for later retrieval and reuse. You can reload a saved search into Discover, add it to a dashboard, and use it as the basis for a <>. + +A saved search includes the query text, filters, and optionally, the time filter. A saved search also includes the selected columns in the document table, the sort order, and the current index pattern. [role="xpack"] [[discover-read-only-access]] @@ -88,6 +88,78 @@ If the saved search is associated with a different index pattern than is current selected, opening the saved search changes the selected index pattern. The query language used for the saved search will also be automatically selected. +[[save-load-delete-query]] +=== Saving queries +A saved query is a portable collection of query text and filters that you can reuse in <>, <>, and <>. Save a query when you want to: + +* Retrieve results from the same query at a later time without having to reenter the query text, add the filters or set the time filter +* View the results of the same query in multiple apps +* Share your query + +Saved queries don't include information specific to Discover, such as the currently selected columns in the document table, the sort order, and the index pattern. If you want to save your current view of Discover for later retrieval and reuse, create a <> instead. + +[role="xpack"] +==== Read only access +If you have insufficient privileges to save queries, the *Save current query* button isn't visible in the saved query management popover. For more information, see <> + +==== Saving a query +To save the current query text, filters, and time filter: + +. Click *#* in the search bar, next to the query text input. +. Click *Save current query* in the popover. ++ +[role="screenshot"] +image::discover/images/saved-query-management-component-all-privileges.png["Example of the saved query management popover with a list of saved queries with write access",width="80%"] ++ +. Enter a name, a description, and then select the filter options that you want to include. By default, filters are automatically included, but the time filter is not. ++ +[role="screenshot"] +image::discover/images/saved-query-save-form-default-filters.png["Example of the saved query management save form with the filters option included and the time filter option excluded",width="80%"] +. Click *Save*. + +==== Loading a query +To load a saved query into Discover, Dashboard, or Visualize: + +. Click *#* in the search bar, next to the query text input. +. Select the query you want to load. You might need to scroll down to find the query you are looking for. + +==== Saving changes to a query +If you load a query and then make changes to the query text, the filters, or the time filter, you can save the changes as a new query or update the existing query. + +To save the changes as a new query: + +. Click *#* in the search bar, next to the query text input. +. Click *Save as new* in the popover. +. Enter a name and a description, and then select the filter options that you want to include. +. Click *Save*. ++ +[role="screenshot"] +image::discover/images/saved-query-management-component-save-as-new-query.png["Example of the saved query management popover when a query is loaded and we have made changes to the query",width="80%"] + +To save the changes to the current query: + +. Click *#* in the search bar. +. Click *Save changes* in the popover. +. Enter a description, and then select the filter options that you want to include. +. Click *Save*. + +==== Clearing a query +To clear a query that is currently loaded in an application: + +. Click *#* in the search bar. +. Click *Clear* in the popover. + +==== Deleting a query +To completely delete a query: + +. Click *#* in the search bar, next to the query text input. +. Hover over the query you want to delete. +. Click the trash can icon. ++ +[role="screenshot"] +image::discover/images/saved-query-management-component-delete-query-button.png["Example of the saved query management popover when a query is hovered over and we are about to delete a query",width="80%"] + +You can import, export, and delete saved queries from <>. [[select-pattern]] === Changing Which Indices You're Searching diff --git a/docs/infrastructure/images/infra-time-selector.png b/docs/infrastructure/images/infra-time-selector.png new file mode 100644 index 0000000000000..181fac4c7b39b Binary files /dev/null and b/docs/infrastructure/images/infra-time-selector.png differ diff --git a/docs/infrastructure/images/infrastructure-configure-source-dialog.png b/docs/infrastructure/images/infrastructure-configure-source-dialog.png deleted file mode 100644 index 31f496aaf56c7..0000000000000 Binary files a/docs/infrastructure/images/infrastructure-configure-source-dialog.png and /dev/null differ diff --git a/docs/infrastructure/images/infrastructure-configure-source-gear-icon.png b/docs/infrastructure/images/infrastructure-configure-source-gear-icon.png deleted file mode 100644 index 8b062c627143a..0000000000000 Binary files a/docs/infrastructure/images/infrastructure-configure-source-gear-icon.png and /dev/null differ diff --git a/docs/infrastructure/images/infrastructure-configure-source.png b/docs/infrastructure/images/infrastructure-configure-source.png deleted file mode 100644 index d1e9a2fd59246..0000000000000 Binary files a/docs/infrastructure/images/infrastructure-configure-source.png and /dev/null differ diff --git a/docs/infrastructure/images/infrastructure-time-selector.png b/docs/infrastructure/images/infrastructure-time-selector.png deleted file mode 100644 index ecf3c6ac34462..0000000000000 Binary files a/docs/infrastructure/images/infrastructure-time-selector.png and /dev/null differ diff --git a/docs/infrastructure/images/read-only-badge.png b/docs/infrastructure/images/read-only-badge.png deleted file mode 100644 index 7911a21e4985f..0000000000000 Binary files a/docs/infrastructure/images/read-only-badge.png and /dev/null differ diff --git a/docs/infrastructure/infra-ui.asciidoc b/docs/infrastructure/infra-ui.asciidoc index 0e1a10807df6e..be8fc29b98ab2 100644 --- a/docs/infrastructure/infra-ui.asciidoc +++ b/docs/infrastructure/infra-ui.asciidoc @@ -49,7 +49,7 @@ For example, enter `host.hostname : "host1"` to see only the information for `ho [[infra-date]] === Specify the time and date -Click the time selector image:logs/images/logs-time-selector.png[time selector icon] to choose the timeframe for the metrics. +Click the time selector image:infrastructure/images/infra-time-selector.png[time selector icon] to choose the timeframe for the metrics. The values shown are the values for the last minute at the specified time and date. [float] diff --git a/docs/logs/images/log-details-actions.png b/docs/logs/images/log-details-actions.png deleted file mode 100644 index 49192d0e3135c..0000000000000 Binary files a/docs/logs/images/log-details-actions.png and /dev/null differ diff --git a/docs/logs/images/log-details-filter.png b/docs/logs/images/log-details-filter.png deleted file mode 100644 index 5037c848d83fe..0000000000000 Binary files a/docs/logs/images/log-details-filter.png and /dev/null differ diff --git a/docs/logs/images/log-details-flyover.png b/docs/logs/images/log-details-flyover.png deleted file mode 100644 index b28040d0e1425..0000000000000 Binary files a/docs/logs/images/log-details-flyover.png and /dev/null differ diff --git a/docs/logs/images/logs-configure-source-dialog-add-column-button.png b/docs/logs/images/logs-configure-source-dialog-add-column-button.png deleted file mode 100644 index 2d4c1986d1336..0000000000000 Binary files a/docs/logs/images/logs-configure-source-dialog-add-column-button.png and /dev/null differ diff --git a/docs/logs/images/logs-configure-source-dialog-add-column-popover.png b/docs/logs/images/logs-configure-source-dialog-add-column-popover.png deleted file mode 100644 index b273ab6911b27..0000000000000 Binary files a/docs/logs/images/logs-configure-source-dialog-add-column-popover.png and /dev/null differ diff --git a/docs/logs/images/logs-configure-source-dialog-indices-tab.png b/docs/logs/images/logs-configure-source-dialog-indices-tab.png deleted file mode 100644 index 4f2460debf128..0000000000000 Binary files a/docs/logs/images/logs-configure-source-dialog-indices-tab.png and /dev/null differ diff --git a/docs/logs/images/logs-configure-source-dialog-log-columns-tab.png b/docs/logs/images/logs-configure-source-dialog-log-columns-tab.png deleted file mode 100644 index c89da36d9d9b2..0000000000000 Binary files a/docs/logs/images/logs-configure-source-dialog-log-columns-tab.png and /dev/null differ diff --git a/docs/logs/images/logs-configure-source-gear-icon.png b/docs/logs/images/logs-configure-source-gear-icon.png deleted file mode 100644 index 8b062c627143a..0000000000000 Binary files a/docs/logs/images/logs-configure-source-gear-icon.png and /dev/null differ diff --git a/docs/logs/images/logs-configure-source.png b/docs/logs/images/logs-configure-source.png deleted file mode 100644 index dec6e0ffe11fa..0000000000000 Binary files a/docs/logs/images/logs-configure-source.png and /dev/null differ diff --git a/docs/logs/images/logs-stream-click-entry.png b/docs/logs/images/logs-stream-click-entry.png deleted file mode 100644 index 12705493dce06..0000000000000 Binary files a/docs/logs/images/logs-stream-click-entry.png and /dev/null differ diff --git a/docs/logs/images/logs-stream-filtered-by-value.png b/docs/logs/images/logs-stream-filtered-by-value.png deleted file mode 100644 index af78e1633bf19..0000000000000 Binary files a/docs/logs/images/logs-stream-filtered-by-value.png and /dev/null differ diff --git a/docs/logs/images/logs-stream-highlight-box.png b/docs/logs/images/logs-stream-highlight-box.png deleted file mode 100644 index 1a6cb2b59dd59..0000000000000 Binary files a/docs/logs/images/logs-stream-highlight-box.png and /dev/null differ diff --git a/docs/logs/images/logs-stream-highlight-entries.png b/docs/logs/images/logs-stream-highlight-entries.png deleted file mode 100644 index d23c7c9853304..0000000000000 Binary files a/docs/logs/images/logs-stream-highlight-entries.png and /dev/null differ diff --git a/docs/logs/images/logs-time-selector.png b/docs/logs/images/logs-time-selector.png index ecf3c6ac34462..5e6a9b7222c54 100644 Binary files a/docs/logs/images/logs-time-selector.png and b/docs/logs/images/logs-time-selector.png differ diff --git a/docs/logs/images/logs-usage-column-headers.png b/docs/logs/images/logs-usage-column-headers.png deleted file mode 100644 index 7040d5a5c8d00..0000000000000 Binary files a/docs/logs/images/logs-usage-column-headers.png and /dev/null differ diff --git a/docs/logs/images/logs-usage-customize.png b/docs/logs/images/logs-usage-customize.png deleted file mode 100644 index 12089987c6b6e..0000000000000 Binary files a/docs/logs/images/logs-usage-customize.png and /dev/null differ diff --git a/docs/logs/images/logs-usage-query-bar.png b/docs/logs/images/logs-usage-query-bar.png deleted file mode 100644 index 45ba5e4647e18..0000000000000 Binary files a/docs/logs/images/logs-usage-query-bar.png and /dev/null differ diff --git a/docs/logs/images/logs-usage-start-streaming.png b/docs/logs/images/logs-usage-start-streaming.png deleted file mode 100644 index 72221f919b563..0000000000000 Binary files a/docs/logs/images/logs-usage-start-streaming.png and /dev/null differ diff --git a/docs/logs/images/logs-usage-stop-streaming.png b/docs/logs/images/logs-usage-stop-streaming.png deleted file mode 100644 index 77cf9aac596a5..0000000000000 Binary files a/docs/logs/images/logs-usage-stop-streaming.png and /dev/null differ diff --git a/docs/logs/images/logs-usage-streaming-indicator.png b/docs/logs/images/logs-usage-streaming-indicator.png deleted file mode 100644 index 807c0053699ef..0000000000000 Binary files a/docs/logs/images/logs-usage-streaming-indicator.png and /dev/null differ diff --git a/docs/logs/images/logs-usage-time-picker.png b/docs/logs/images/logs-usage-time-picker.png deleted file mode 100644 index 42a0e4d946bb3..0000000000000 Binary files a/docs/logs/images/logs-usage-time-picker.png and /dev/null differ diff --git a/docs/logs/images/logs-usage-timeline.png b/docs/logs/images/logs-usage-timeline.png deleted file mode 100644 index 4ad49f45b3392..0000000000000 Binary files a/docs/logs/images/logs-usage-timeline.png and /dev/null differ diff --git a/docs/logs/images/read-only-badge.png b/docs/logs/images/read-only-badge.png deleted file mode 100644 index ab7cc296477dc..0000000000000 Binary files a/docs/logs/images/read-only-badge.png and /dev/null differ diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc index 0b70712337182..cdc990ff1caa0 100644 --- a/docs/logs/using.asciidoc +++ b/docs/logs/using.asciidoc @@ -47,12 +47,6 @@ Here, you can set the scale to use for the minimap timeline, choose whether to w === Stream or pause logs Click *Stream live* to start streaming live log data, or click *Stop streaming* to focus on historical data. -[role="screenshot"] -image::logs/images/logs-usage-start-streaming.png[Logs start streaming] - -[role="screenshot"] -image::logs/images/logs-usage-stop-streaming.png[Logs stop streaming] - When you are viewing historical data, you can scroll back through the entries as far as there is data available. When you are streaming live data, the most recent log appears at the bottom of the page. @@ -75,12 +69,11 @@ To highlight a word or phrase in the logs stream, click *Highlights* and enter y To inspect a log event, hover over it, then click the *View details* icon image:logs/images/logs-view-event.png[View event icon] beside the event. This opens the *Log event document details* fly-out that shows the fields associated with the log event. -To quickly filter the logs stream by one of the field values shown here, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field. +To quickly filter the logs stream by one of the field values, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field. This automatically adds a search filter to the logs stream to filter the entries by this field and value. In the log event details, click *Actions* to see the other actions related to the event. Depending on the event and the features you have installed and configured, you may also be able to: -* Select *View monitor status* to <> in the *Uptime* app. -// ++ Is "monitor" the best choice of word here? +* Select *View status in Uptime* to <> in the *Uptime* app. * Select *View in APM* to <> in the *APM* app. diff --git a/docs/maps/connect-to-ems.asciidoc b/docs/maps/connect-to-ems.asciidoc index 75018e6dd4d97..6b4eb9928cea0 100644 --- a/docs/maps/connect-to-ems.asciidoc +++ b/docs/maps/connect-to-ems.asciidoc @@ -4,9 +4,9 @@ https://www.elastic.co/elastic-maps-service[Elastic Maps Service (EMS)] is a service that hosts tile layers and vector shapes of administrative boundaries. -If you are using Kibana's out-of-the-box settings, the **Maps** application is already configured to use EMS. +If you are using Kibana's out-of-the-box settings, **Elastic Maps** is already configured to use EMS. -The **Maps** application makes requests directly from the browser to EMS. +**Elastic Maps** makes requests directly from the browser to EMS. To proxy EMS requests through the Kibana server, set `map.proxyElasticMapsServiceInMaps` to `true` in your <> file. @@ -19,5 +19,5 @@ behind a firewall. If this happens, you can disable the EMS connection to avoid To disable EMS, change your <> file. . Set `map.includeElasticMapsService` to `false` to turn off the EMS connection. -. Set `map.tilemap.url` to the URL of your tile server. This configures the **Maps** default tile layer. +. Set `map.tilemap.url` to the URL of your tile server. This configures the default tile layer of **Elastic Maps**. . (Optional) Set `map.regionmap` to the vector shapes of the administrative boundaries that you want to use. diff --git a/docs/maps/index.asciidoc b/docs/maps/index.asciidoc index cb7ddd62d6c47..bb569a11ed874 100644 --- a/docs/maps/index.asciidoc +++ b/docs/maps/index.asciidoc @@ -1,11 +1,23 @@ [role="xpack"] [[maps]] -= Maps += Elastic Maps [partintro] -- -The **Maps** application enables you to parse through your geographical data at scale, with speed, and in real time. With features like multiple layers and indices in a map, plotting of raw documents, dynamic client-side styling, and global search across multiple layers, you can understand and monitor your data with ease. +*Elastic Maps* enables you to parse through your geographical data at scale, with speed, and in real time. With features like multiple layers and indices in a map, plotting of raw documents, dynamic client-side styling, and global search across multiple layers, you can understand and monitor your data with ease. + +With *Elastic Maps*, you can: + +* Create maps with multiple layers and indices. +* Upload GeoJSON files into Elasticsearch. +* Embed your map in Dashboards. +* Plot individual documents or use aggregations to plot any data set, no matter how large. +* Create choropleth maps. +* Use data driven styling to symbolize features from property values. +* Focus the data you want to display with searches. + +Start your tour of *Elastic Maps* with the <>. [role="screenshot"] image::maps/images/sample_data_ecommerce.png[] diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 68163f5e88a0e..f6db2f0fff219 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[maps-getting-started]] -== Getting started with Maps +== Getting started with Elastic Maps -You work with *Maps* by adding layers. The data for a layer can come from +You work with *Elastic Maps* by adding layers. The data for a layer can come from sources such as {es} documents, vector sources, tile map services, web map services, and more. You can symbolize the data in different ways. For example, you might show which airports have the longest flight @@ -14,7 +14,7 @@ light to dark. image::maps/images/sample_data_web_logs.png[] [[maps-read-only-access]] -NOTE: If you have insufficient privileges to create or save maps, a read-only icon +NOTE: If you have insufficient privileges to create or save maps, a read-only icon appears in the application header. The buttons to create new maps or edit existing maps won't be visible. For more information on granting access to Kibana see <>. @@ -25,7 +25,7 @@ image::maps/images/read-only-badge.png[Example of Maps' read only access indicat [float] === Prerequisites Before you start this tutorial, <>. Each -sample data set includes a map to go along with the data. Once you've added the data, open *Maps* and +sample data set includes a map to go along with the data. Once you've added the data, open *Elastic Maps* and explore the different layers of the *[Logs] Total Requests and Bytes* map. You'll re-create this map in this tutorial. @@ -44,7 +44,7 @@ In this tutorial, you'll learn to: The first thing to do is to create a new map. -. If you haven't already, open *Maps*. +. If you haven't already, open *Elastic Maps*. . On the maps list page, click *Create map*. . Set the time range to *Last 7 days*. + diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index 33a8ba15113d8..3830ecdcbc1f0 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -2,7 +2,7 @@ [[maps-search]] == Searching your data -The **Maps** application embeds the query bar for real-time ad hoc search. +**Elastic Maps** embeds the query bar for real-time ad hoc search. Only layers requesting data from {es} are filtered when you submit a search request. You can create a layer that requests data from {es} from the following: diff --git a/docs/maps/tile-layer.asciidoc b/docs/maps/tile-layer.asciidoc index 1681cd7dcca85..059dd527f4810 100644 --- a/docs/maps/tile-layer.asciidoc +++ b/docs/maps/tile-layer.asciidoc @@ -9,11 +9,11 @@ image::maps/images/tile_layer.png[] You can create a tile layer from the following data sources: -*Custom Tile Map Service*:: Map tiles configured in kibana.yml. +*Configured Tile Map Service*:: Tile map service configured in kibana.yml. See map.tilemap.url in <> for details. -*Tiles*:: Map tiles from https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. +*EMS Basemaps*:: Tile map service from https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. -*Tile Map Service from URL*:: Map tiles from a URL that includes the XYZ coordinates. +*Tile Map Service*:: Tile map service configured in interface. *Web Map Service*:: Maps from OGC Standard WMS. diff --git a/docs/maps/trouble-shooting.asciidoc b/docs/maps/trouble-shooting.asciidoc index d23bed546a6dc..542138828530b 100644 --- a/docs/maps/trouble-shooting.asciidoc +++ b/docs/maps/trouble-shooting.asciidoc @@ -1,13 +1,13 @@ [role="xpack"] [[maps-troubleshooting]] -== Maps troubleshooting +== Elastic Maps troubleshooting Use the information in this section to inspect Elasticsearch requests and find solutions to common problems. [float] === Inspect Elasticsearch requests -The Maps application uses the {ref}/search-search.html[{es} search API] to get documents and aggregation results from {es}. To troubleshoot these requests, open the Inspector, which shows the most recent requests for each layer. You can switch between different requests using the *Request* dropdown. +*Elastic Maps* uses the {ref}/search-search.html[{es} search API] to get documents and aggregation results from {es}. To troubleshoot these requests, open the Inspector, which shows the most recent requests for each layer. You can switch between different requests using the *Request* dropdown. [role="screenshot"] image::maps/images/inspector.png[] diff --git a/docs/maps/vector-layer.asciidoc b/docs/maps/vector-layer.asciidoc index c790107cbd874..8dade1adec6f8 100644 --- a/docs/maps/vector-layer.asciidoc +++ b/docs/maps/vector-layer.asciidoc @@ -9,10 +9,10 @@ image::maps/images/vector_layer.png[] You can create a vector layer from the following sources: -*Custom vector shapes*:: Vector shapes from static files configured in kibana.yml. +*Configured GeoJSON*:: Vector data from hosted GeoJSON configured in kibana.yml. See map.regionmap.* in <> for details. -*Documents*:: Geospatial data from a Kibana index pattern. +*Documents*:: Vector data from a Kibana index pattern. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. NOTE: Document results are limited to the first 10000 matching documents. @@ -22,7 +22,7 @@ Use <> to plot large data sets. Set *Show as* to *grid rectangles* or *points*. The index must contain at least one field mapped as {ref}/geo-point.html[geo_point]. -*Vector shapes*:: Vector shapes of administrative boundaries from https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. +*EMS Boundaries*:: Administrative boundaries from https://www.elastic.co/elastic-maps-service[Elastic Maps Service]. include::vector-style.asciidoc[] include::vector-style-properties.asciidoc[] diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 4798dd8678d0a..eac1971b22b17 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -83,6 +83,10 @@ Defaults to `120000` (two minutes). Reporting works by capturing screenshots from Kibana. The following settings control the capturing process. +`xpack.reporting.capture.maxAttempts`:: +If capturing a report fails for any reason, Kibana will re-attempt othe reporting +job, as many times as this setting. Defaults to `3`. + `xpack.reporting.capture.loadDelay`:: When visualizations are not evented, this is the amount of time before taking a screenshot. All visualizations that ship with Kibana are evented, so this diff --git a/package.json b/package.json index 3f095c8b82248..3dadd53a88ae7 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,7 @@ "custom-event-polyfill": "^0.3.0", "d3": "3.5.17", "d3-cloud": "1.2.5", - "del": "^4.0.0", + "del": "^4.1.1", "elasticsearch": "^16.2.0", "elasticsearch-browser": "^16.2.0", "encode-uri-query": "1.0.1", @@ -166,10 +166,11 @@ "handlebars": "4.1.2", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", + "history": "^4.9.0", "hjson": "3.1.2", "hoek": "^5.0.4", "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1", + "https-proxy-agent": "^2.2.2", "inert": "^5.1.0", "inline-style": "^2.0.0", "joi": "^13.5.2", @@ -197,7 +198,7 @@ "mustache": "2.3.2", "ngreact": "0.5.1", "node-fetch": "1.7.3", - "opn": "^5.4.0", + "opn": "^5.5.0", "oppsy": "^2.0.0", "pegjs": "0.10.0", "postcss-loader": "3.0.0", @@ -304,6 +305,7 @@ "@types/hapi": "^17.0.18", "@types/hapi-auth-cookie": "^9.1.0", "@types/has-ansi": "^3.0.0", + "@types/history": "^4.7.3", "@types/hoek": "^4.1.3", "@types/humps": "^1.1.2", "@types/jest": "^24.0.9", @@ -317,7 +319,7 @@ "@types/lru-cache": "^5.1.0", "@types/markdown-it": "^0.0.7", "@types/minimatch": "^2.0.29", - "@types/mocha": "^5.2.6", + "@types/mocha": "^5.2.7", "@types/moment-timezone": "^0.5.8", "@types/mustache": "^0.8.31", "@types/node": "^10.12.27", @@ -335,7 +337,7 @@ "@types/rimraf": "^2.0.2", "@types/selenium-webdriver": "^3.0.16", "@types/semver": "^5.5.0", - "@types/sinon": "^7.0.0", + "@types/sinon": "^7.0.13", "@types/strip-ansi": "^3.0.0", "@types/styled-components": "^3.0.2", "@types/supertest": "^2.0.5", @@ -393,7 +395,7 @@ "has-ansi": "^3.0.0", "image-diff": "1.6.3", "intl-messageformat-parser": "^1.4.0", - "is-path-inside": "^2.0.0", + "is-path-inside": "^2.1.0", "istanbul-instrumenter-loader": "3.0.1", "jest": "^24.8.0", "jest-cli": "^24.8.0", @@ -431,7 +433,7 @@ "sass-lint": "^1.12.1", "selenium-webdriver": "^4.0.0-alpha.4", "simple-git": "1.116.0", - "sinon": "^7.2.2", + "sinon": "^7.4.2", "strip-ansi": "^3.0.1", "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index bea8ed11e87ac..6d3914eb56218 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -21,5 +21,5 @@ export { withProcRunner } from './proc_runner'; export { ToolingLog, ToolingLogTextWriter, pickLevelFromFlags } from './tooling_log'; export { createAbsolutePathSerializer } from './serializers'; export { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from './certs'; -export { run, createFailError, createFlagError, combineErrors, isFailError } from './run'; +export { run, createFailError, createFlagError, combineErrors, isFailError, Flags } from './run'; export { REPO_ROOT } from './constants'; diff --git a/packages/kbn-dev-utils/src/run/index.ts b/packages/kbn-dev-utils/src/run/index.ts index 9d81ff0c9af3b..5e1a42deefffb 100644 --- a/packages/kbn-dev-utils/src/run/index.ts +++ b/packages/kbn-dev-utils/src/run/index.ts @@ -18,4 +18,5 @@ */ export { run } from './run'; +export { Flags } from './flags'; export { createFailError, createFlagError, combineErrors, isFailError } from './fail'; diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json index b16c45bb53dca..5ef1b6e125413 100644 --- a/packages/kbn-es-query/package.json +++ b/packages/kbn-es-query/package.json @@ -21,7 +21,7 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/expect": "1.0.0", - "del": "^4.0.0", + "del": "^4.1.1", "getopts": "^2.2.4", "supports-color": "^7.0.0" } diff --git a/packages/kbn-es/package.json b/packages/kbn-es/package.json index 7e8e2e227eb3c..1064d719af837 100644 --- a/packages/kbn-es/package.json +++ b/packages/kbn-es/package.json @@ -10,7 +10,7 @@ "abort-controller": "^2.0.3", "chalk": "^2.4.2", "dedent": "^0.7.0", - "del": "^4.0.0", + "del": "^4.1.1", "execa": "^1.0.0", "getopts": "^2.2.4", "glob": "^7.1.2", diff --git a/packages/kbn-i18n/package.json b/packages/kbn-i18n/package.json index 80162c9c6bafa..dff8a3f2f4b78 100644 --- a/packages/kbn-i18n/package.json +++ b/packages/kbn-i18n/package.json @@ -18,7 +18,7 @@ "@kbn/dev-utils": "1.0.0", "@types/intl-relativeformat": "^2.1.0", "@types/react-intl": "^2.3.15", - "del": "^4.0.0", + "del": "^4.1.1", "getopts": "^2.2.4", "supports-color": "^7.0.0", "typescript": "3.5.3" diff --git a/packages/kbn-interpreter/package.json b/packages/kbn-interpreter/package.json index aa8eb8abd96c0..a9b30c17e2927 100644 --- a/packages/kbn-interpreter/package.json +++ b/packages/kbn-interpreter/package.json @@ -24,7 +24,7 @@ "babel-loader": "^8.0.6", "copy-webpack-plugin": "^5.0.4", "css-loader": "2.1.1", - "del": "^4.0.0", + "del": "^4.1.1", "getopts": "^2.2.4", "pegjs": "0.10.0", "sass-loader": "^7.3.1", diff --git a/packages/kbn-interpreter/src/common/index.d.ts b/packages/kbn-interpreter/src/common/index.d.ts index a8917b7a65df1..7201ccbb35635 100644 --- a/packages/kbn-interpreter/src/common/index.d.ts +++ b/packages/kbn-interpreter/src/common/index.d.ts @@ -19,4 +19,4 @@ export { Registry } from './lib/registry'; -export { fromExpression, Ast } from './lib/ast'; +export { fromExpression, toExpression, Ast } from './lib/ast'; diff --git a/packages/kbn-interpreter/src/common/lib/ast.d.ts b/packages/kbn-interpreter/src/common/lib/ast.d.ts index 2b0328bda9392..a4ee235359463 100644 --- a/packages/kbn-interpreter/src/common/lib/ast.d.ts +++ b/packages/kbn-interpreter/src/common/lib/ast.d.ts @@ -20,3 +20,4 @@ export type Ast = unknown; export declare function fromExpression(expression: string): Ast; +export declare function toExpression(astObj: Ast, type?: string): string; diff --git a/packages/kbn-plugin-helpers/package.json b/packages/kbn-plugin-helpers/package.json index f4669a5e906ef..ce496abe9c7f1 100644 --- a/packages/kbn-plugin-helpers/package.json +++ b/packages/kbn-plugin-helpers/package.json @@ -16,7 +16,7 @@ "@babel/core": "^7.5.5", "argv-split": "^2.0.1", "commander": "^3.0.0", - "del": "^4.0.0", + "del": "^4.1.1", "execa": "^1.0.0", "globby": "^8.0.1", "gulp-babel": "^8.0.0", diff --git a/packages/kbn-pm/package.json b/packages/kbn-pm/package.json index 9223f144a6f58..70adf5c43939d 100644 --- a/packages/kbn-pm/package.json +++ b/packages/kbn-pm/package.json @@ -41,7 +41,7 @@ "cmd-shim": "^2.1.0", "cpy": "^7.0.1", "dedent": "^0.7.0", - "del": "^4.0.0", + "del": "^4.1.1", "execa": "^1.0.0", "getopts": "^2.2.4", "glob": "^7.1.2", diff --git a/packages/kbn-test/package.json b/packages/kbn-test/package.json index de8ef26636715..6891f01bae2a9 100644 --- a/packages/kbn-test/package.json +++ b/packages/kbn-test/package.json @@ -17,7 +17,7 @@ "dependencies": { "chalk": "^2.4.2", "dedent": "^0.7.0", - "del": "^4.0.0", + "del": "^4.1.1", "getopts": "^2.2.4", "glob": "^7.1.2", "rxjs": "^6.2.1", diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index d864729a85236..36412961ce75b 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -18,24 +18,36 @@ */ import { resolve } from 'path'; -import { run } from '@kbn/dev-utils'; +import { run, createFlagError, Flags } from '@kbn/dev-utils'; import { FunctionalTestRunner } from './functional_test_runner'; +const makeAbsolutePath = (v: string) => resolve(process.cwd(), v); +const toArray = (v: string | string[]) => ([] as string[]).concat(v || []); +const parseInstallDir = (flags: Flags) => { + const flag = flags['kibana-install-dir']; + + if (typeof flag !== 'string' && flag !== undefined) { + throw createFlagError('--kibana-install-dir must be a string or not defined'); + } + + return flag ? makeAbsolutePath(flag) : undefined; +}; + export function runFtrCli() { run( async ({ flags, log }) => { - const resolveConfigPath = (v: string) => resolve(process.cwd(), v); - const toArray = (v: string | string[]) => ([] as string[]).concat(v || []); - const functionalTestRunner = new FunctionalTestRunner( log, - resolveConfigPath(flags.config as string), + makeAbsolutePath(flags.config as string), { mochaOpts: { bail: flags.bail, grep: flags.grep || undefined, invert: flags.invert, }, + kbnTestServer: { + installDir: parseInstallDir(flags), + }, suiteTags: { include: toArray(flags['include-tag'] as string | string[]), exclude: toArray(flags['exclude-tag'] as string | string[]), @@ -84,7 +96,7 @@ export function runFtrCli() { }, { flags: { - string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag'], + string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag', 'kibana-install-dir'], boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'], default: { config: 'test/functional/config.js', @@ -100,6 +112,7 @@ export function runFtrCli() { --exclude-tag=tag a tag to be excluded, pass multiple times for multiple tags --test-stats print the number of tests (included and excluded) to STDERR --updateBaselines replace baseline screenshots with whatever is generated from the test + --kibana-install-dir directory where the Kibana install being tested resides `, }, } diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 4887ad2c6e1d7..d9cf282d8f4b6 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -187,6 +187,7 @@ export const schema = Joi.object() buildArgs: Joi.array(), sourceArgs: Joi.array(), serverArgs: Joi.array(), + installDir: Joi.string(), }) .default(), diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.js b/packages/kbn-test/src/functional_tests/lib/run_ftr.js index a0edfcdb8c7b5..aeda84f9524ed 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.js +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.js @@ -20,7 +20,10 @@ import { FunctionalTestRunner, readConfigFile } from '../../functional_test_runner'; import { CliError } from './run_cli'; -async function createFtr({ configPath, options: { log, bail, grep, updateBaselines, suiteTags } }) { +async function createFtr({ + configPath, + options: { installDir, log, bail, grep, updateBaselines, suiteTags }, +}) { const config = await readConfigFile(log, configPath); return new FunctionalTestRunner(log, configPath, { @@ -28,6 +31,9 @@ async function createFtr({ configPath, options: { log, bail, grep, updateBaselin bail: !!bail, grep, }, + kbnTestServer: { + installDir, + }, updateBaselines, suiteTags: { include: [...suiteTags.include, ...config.get('suiteTags.include')], diff --git a/packages/kbn-test/src/functional_tests/tasks.js b/packages/kbn-test/src/functional_tests/tasks.js index 966b148f0ce6d..6ebc49679fbf3 100644 --- a/packages/kbn-test/src/functional_tests/tasks.js +++ b/packages/kbn-test/src/functional_tests/tasks.js @@ -21,6 +21,7 @@ import { relative } from 'path'; import * as Rx from 'rxjs'; import { startWith, switchMap, take } from 'rxjs/operators'; import { withProcRunner } from '@kbn/dev-utils'; +import dedent from 'dedent'; import { runElasticsearch, @@ -33,14 +34,20 @@ import { import { readConfigFile } from '../functional_test_runner/lib'; -const SUCCESS_MESSAGE = ` +const makeSuccessMessage = options => { + const installDirFlag = options.installDir ? ` --kibana-install-dir=${options.installDir}` : ''; -Elasticsearch and Kibana are ready for functional testing. Start the functional tests -in another terminal session by running this command from this directory: + return ( + '\n\n' + + dedent` + Elasticsearch and Kibana are ready for functional testing. Start the functional tests + in another terminal session by running this command from this directory: - node ${relative(process.cwd(), KIBANA_FTR_SCRIPT)} - -`; + node ${relative(process.cwd(), KIBANA_FTR_SCRIPT)}${installDirFlag} + ` + + '\n\n' + ); +}; /** * Run servers and tests for each config @@ -118,15 +125,15 @@ export async function startServers(options) { // wait for 5 seconds of silence before logging the // success message so that it doesn't get buried - await silence(5000, { log }); - log.info(SUCCESS_MESSAGE); + await silence(log, 5000); + log.success(makeSuccessMessage(options)); await procs.waitForAllToStop(); await es.cleanup(); }); } -async function silence(milliseconds, { log }) { +async function silence(log, milliseconds) { await log .getWritten$() .pipe( diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 5b4a849d4209b..158ff54336075 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -66,7 +66,7 @@ "redux-thunk": "2.2.0", "regenerator-runtime": "^0.13.3", "sass-loader": "^7.3.1", - "sinon": "^7.2.2", + "sinon": "^7.4.2", "style-loader": "^0.23.1", "webpack": "^4.39.2", "webpack-dev-server": "^3.8.0", diff --git a/renovate.json5 b/renovate.json5 index e3b887d52149c..7798909bd3f41 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -369,6 +369,14 @@ '@types/has-ansi', ], }, + { + groupSlug: 'history', + groupName: 'history related packages', + packageNames: [ + 'history', + '@types/history', + ], + }, { groupSlug: 'humps', groupName: 'humps related packages', @@ -617,14 +625,6 @@ '@types/git-url-parse', ], }, - { - groupSlug: 'history', - groupName: 'history related packages', - packageNames: [ - 'history', - '@types/history', - ], - }, { groupSlug: 'jsdom', groupName: 'jsdom related packages', diff --git a/rfcs/text/0004_application_service_mounting.md b/rfcs/text/0004_application_service_mounting.md index 30e8d9a05b8b4..7dc577abc48e3 100644 --- a/rfcs/text/0004_application_service_mounting.md +++ b/rfcs/text/0004_application_service_mounting.md @@ -18,14 +18,14 @@ import ReactDOM from 'react-dom'; import { MyApp } from './componnets'; -export function renderApp(context, targetDomElement) { +export function renderApp(context, { element }) { ReactDOM.render( , - targetDomElement + element ); return () => { - ReactDOM.unmountComponentAtNode(targetDomElement); + ReactDOM.unmountComponentAtNode(element); }; } ``` @@ -38,9 +38,9 @@ class MyPlugin { application.register({ id: 'my-app', title: 'My Application', - async mount(context, targetDomElement) { + async mount(context, params) { const { renderApp } = await import('./applcation'); - return renderApp(context, targetDomElement); + return renderApp(context, params); } }); } @@ -63,9 +63,7 @@ lock-in. ```ts /** A context type that implements the Handler Context pattern from RFC-0003 */ -export interface MountContext { - /** This is the base path for setting up your router. */ - basename: string; +export interface AppMountContext { /** These services serve as an example, but are subject to change. */ core: { http: { @@ -93,6 +91,13 @@ export interface MountContext { [contextName: string]: unknown; } +export interface AppMountParams { + /** The base path the application is mounted on. Used to configure routers. */ + appBasePath: string; + /** The element the application should render into */ + element: HTMLElement; +} + export type Unmount = () => Promise | void; export interface AppSpec { @@ -109,11 +114,11 @@ export interface AppSpec { /** * A mount function called when the user navigates to this app's route. - * @param context the `MountContext generated for this app - * @param targetDomElement An HTMLElement to mount the application onto. + * @param context the `AppMountContext` generated for this app + * @param params the `AppMountParams` * @returns An unmounting function that will be called to unmount the application. */ - mount(context: MountContext, targetDomElement: HTMLElement): Unmount | Promise; + mount(context: MountContext, params: AppMountParams): Unmount | Promise; /** * A EUI iconType that will be used for the app's icon. This icon @@ -158,19 +163,21 @@ When an app is registered via `register`, it must provide a `mount` function that will be invoked whenever the window's location has changed from another app to this app. -This function is called with a `MountContext` and an `HTMLElement` for the -application to render itself to. The mount function must also return a function -that can be called by the ApplicationService to unmount the application at the -given DOM node. The mount function may return a Promise of an unmount function -in order to import UI code dynamically. +This function is called with a `AppMountContext` and an +`AppMountParams` which contains a `HTMLElement` for the application to +render itself to. The mount function must also return a function that can be +called by the ApplicationService to unmount the application at the given DOM +Element. The mount function may return a Promise of an unmount function in order +to import UI code dynamically. The ApplicationService's `register` method will only be available during the *setup* lifecycle event. This allows the system to know when all applications have been registered. -The `mount` function will also get access to the `MountContext` that has many of -the same core services available during the `start` lifecycle. Plugins can also -register additional context attributes via the `registerMountContext` function. +The `mount` function will also get access to the `AppMountContext` that +has many of the same core services available during the `start` lifecycle. +Plugins can also register additional context attributes via the +`registerMountContext` function. ## Routing @@ -190,7 +197,7 @@ An example: "overview" page: mykibana.com/app/my-app/overview When setting up a router, your application should only handle the part of the -URL following the `context.basename` provided when you application is mounted. +URL following the `params.appBasePath` provided when you application is mounted. ### Legacy Applications @@ -211,7 +218,7 @@ a full-featured router and code-splitting. Note that using React or any other 3rd party tools featured here is not required to build a Kibana Application. ```tsx -// my_plugin/public/application.ts +// my_plugin/public/application.tsx import React from 'react'; import ReactDOM from 'react-dom'; @@ -239,16 +246,16 @@ const MyApp = ({ basename }) => ( , ); -export function renderApp(context, targetDomElement) { +export function renderApp(context, params) { ReactDOM.render( - // `context.basename` would be `/app/my-app` in this example. - // This exact string is not guaranteed to be stable, always reference - // `context.basename`. - , - targetDomElem + // `params.appBasePath` would be `/app/my-app` in this example. + // This exact string is not guaranteed to be stable, always reference the + // provided value at `params.appBasePath`. + , + params.element ); - return () => ReactDOM.unmountComponentAtNode(targetDomElem); + return () => ReactDOM.unmountComponentAtNode(params.element); } ``` @@ -259,9 +266,9 @@ export class MyPlugin { setup({ application }) { application.register({ id: 'my-app', - async mount(context, targetDomElem) { + async mount(context, params) { const { renderApp } = await import('./applcation'); - return renderApp(context, targetDomElement); + return renderApp(context, params); } }); } diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index e2e5cf7d129bc..ee37ae48823b3 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -39,7 +39,7 @@ We'll start with an overview of how plugins work in the new platform, and we'll Plugins in the new platform are not especially novel or complicated to describe. Our intention wasn't to build some clever system that magically solved problems through abstractions and layers of obscurity, and we wanted to make sure plugins could continue to use most of the same technologies they use today, at least from a technical perspective. -New platform plugins exist in the `src/plugins` and `x-pack/legacy/plugins` directories. +New platform plugins exist in the `src/plugins` and `x-pack/plugins` directories. ### Architecture diff --git a/src/core/public/application/application_service.mock.ts b/src/core/public/application/application_service.mock.ts index 85d997f3dc9aa..a2db755224636 100644 --- a/src/core/public/application/application_service.mock.ts +++ b/src/core/public/application/application_service.mock.ts @@ -17,23 +17,51 @@ * under the License. */ +import { Subject } from 'rxjs'; + import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; -import { ApplicationService, ApplicationSetup, ApplicationStart } from './application_service'; +import { ApplicationService } from './application_service'; +import { + ApplicationSetup, + InternalApplicationStart, + ApplicationStart, + InternalApplicationSetup, +} from './types'; type ApplicationServiceContract = PublicMethodsOf; const createSetupContractMock = (): jest.Mocked => ({ - registerApp: jest.fn(), + register: jest.fn(), + registerMountContext: jest.fn(), +}); + +const createInternalSetupContractMock = (): jest.Mocked => ({ + register: jest.fn(), registerLegacyApp: jest.fn(), + registerMountContext: jest.fn(), }); -const createStartContractMock = (): jest.Mocked => ({ - ...capabilitiesServiceMock.createStartContract(), +const createStartContractMock = (legacyMode = false): jest.Mocked => ({ + capabilities: capabilitiesServiceMock.createStartContract().capabilities, + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + registerMountContext: jest.fn(), +}); + +const createInternalStartContractMock = (): jest.Mocked => ({ + availableApps: new Map(), + availableLegacyApps: new Map(), + capabilities: capabilitiesServiceMock.createStartContract().capabilities, + navigateToApp: jest.fn(), + getUrlForApp: jest.fn(), + registerMountContext: jest.fn(), + currentAppId$: new Subject(), + getComponent: jest.fn(), }); const createMock = (): jest.Mocked => ({ - setup: jest.fn().mockReturnValue(createSetupContractMock()), - start: jest.fn().mockReturnValue(createStartContractMock()), + setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), + start: jest.fn().mockReturnValue(createInternalStartContractMock()), stop: jest.fn(), }); @@ -41,4 +69,7 @@ export const applicationServiceMock = { create: createMock, createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, + + createInternalSetupContract: createInternalSetupContractMock, + createInternalStartContract: createInternalStartContractMock, }; diff --git a/src/core/public/application/application_service.test.mocks.ts b/src/core/public/application/application_service.test.mocks.ts index c28d0a203068a..d829cf18e56be 100644 --- a/src/core/public/application/application_service.test.mocks.ts +++ b/src/core/public/application/application_service.test.mocks.ts @@ -26,3 +26,11 @@ export const CapabilitiesServiceConstructor = jest jest.doMock('./capabilities', () => ({ CapabilitiesService: CapabilitiesServiceConstructor, })); + +export const MockHistory = { + push: jest.fn(), +}; +export const createBrowserHistoryMock = jest.fn().mockReturnValue(MockHistory); +jest.doMock('history', () => ({ + createBrowserHistory: createBrowserHistoryMock, +})); diff --git a/src/core/public/application/application_service.test.tsx b/src/core/public/application/application_service.test.tsx index d2266671367a2..5b374218a5932 100644 --- a/src/core/public/application/application_service.test.tsx +++ b/src/core/public/application/application_service.test.tsx @@ -17,57 +17,219 @@ * under the License. */ +import { shallow } from 'enzyme'; +import React from 'react'; + import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; -import { MockCapabilitiesService } from './application_service.test.mocks'; +import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks'; import { ApplicationService } from './application_service'; +import { contextServiceMock } from '../context/context_service.mock'; +import { httpServiceMock } from '../http/http_service.mock'; + +describe('#setup()', () => { + describe('register', () => { + it('throws an error if two apps with the same id are registered', () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.register(Symbol(), { id: 'app1' } as any); + expect(() => + setup.register(Symbol(), { id: 'app1' } as any) + ).toThrowErrorMatchingInlineSnapshot( + `"An application is already registered with the id \\"app1\\""` + ); + }); + + it('throws error if additional apps are registered after setup', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + await service.start({ http, injectedMetadata }); + expect(() => + setup.register(Symbol(), { id: 'app1' } as any) + ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); + }); + }); + + describe('registerLegacyApp', () => { + it('throws an error if two apps with the same id are registered', () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.registerLegacyApp({ id: 'app2' } as any); + expect(() => + setup.registerLegacyApp({ id: 'app2' } as any) + ).toThrowErrorMatchingInlineSnapshot( + `"A legacy application is already registered with the id \\"app2\\""` + ); + }); + + it('throws error if additional apps are registered after setup', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + await service.start({ http, injectedMetadata }); + expect(() => + setup.registerLegacyApp({ id: 'app2' } as any) + ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); + }); + }); + + it("`registerMountContext` calls context container's registerContext", () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + const container = context.createContextContainer.mock.results[0].value; + const pluginId = Symbol(); + const noop = () => {}; + setup.registerMountContext(pluginId, 'test' as any, noop as any); + expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', noop); + }); +}); describe('#start()', () => { + beforeEach(() => { + MockHistory.push.mockReset(); + }); + it('exposes available apps from capabilities', async () => { const service = new ApplicationService(); - const setup = service.setup(); - setup.registerApp({ id: 'app1' } as any); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.register(Symbol(), { id: 'app1' } as any); setup.registerLegacyApp({ id: 'app2' } as any); + + const http = httpServiceMock.createStartContract(); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - const startContract = await service.start({ injectedMetadata }); + const startContract = await service.start({ http, injectedMetadata }); + expect(startContract.availableApps).toMatchInlineSnapshot(` -Array [ - Object { - "id": "app1", - }, -] -`); + Map { + "app1" => Object { + "id": "app1", + }, + } + `); expect(startContract.availableLegacyApps).toMatchInlineSnapshot(` -Array [ - Object { - "id": "app2", - }, -] -`); + Map { + "app2" => Object { + "id": "app2", + }, + } + `); }); it('passes registered applications to capabilities', async () => { const service = new ApplicationService(); - const setup = service.setup(); - setup.registerApp({ id: 'app1' } as any); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.register(Symbol(), { id: 'app1' } as any); + + const http = httpServiceMock.createStartContract(); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - await service.start({ injectedMetadata }); + await service.start({ http, injectedMetadata }); + expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ - apps: [{ id: 'app1' }], - legacyApps: [], + apps: new Map([['app1', { id: 'app1' }]]), + legacyApps: new Map(), injectedMetadata, }); }); it('passes registered legacy applications to capabilities', async () => { const service = new ApplicationService(); - const setup = service.setup(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); setup.registerLegacyApp({ id: 'legacyApp1' } as any); + + const http = httpServiceMock.createStartContract(); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); - await service.start({ injectedMetadata }); + await service.start({ http, injectedMetadata }); + expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ - apps: [], - legacyApps: [{ id: 'legacyApp1' }], + apps: new Map(), + legacyApps: new Map([['legacyApp1', { id: 'legacyApp1' }]]), injectedMetadata, }); }); + + it('returns renderable JSX tree', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(false); + const start = await service.start({ http, injectedMetadata }); + + expect(() => shallow(React.createElement(() => start.getComponent()))).not.toThrow(); + }); + + describe('navigateToApp', () => { + it('changes the browser history to /app/:appId', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(false); + const start = await service.start({ http, injectedMetadata }); + + start.navigateToApp('myTestApp'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', undefined); + start.navigateToApp('myOtherApp'); + expect(MockHistory.push).toHaveBeenCalledWith('/app/myOtherApp', undefined); + }); + + it('appends a path if specified', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(false); + const start = await service.start({ http, injectedMetadata }); + + start.navigateToApp('myTestApp', { path: 'deep/link/to/location/2' }); + expect(MockHistory.push).toHaveBeenCalledWith( + '/app/myTestApp/deep/link/to/location/2', + undefined + ); + }); + + it('includes state if specified', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(false); + const start = await service.start({ http, injectedMetadata }); + + start.navigateToApp('myTestApp', { state: 'my-state' }); + expect(MockHistory.push).toHaveBeenCalledWith('/app/myTestApp', 'my-state'); + }); + + it('redirects when in legacyMode', async () => { + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + service.setup({ context }); + + const http = httpServiceMock.createStartContract(); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(true); + const redirectTo = jest.fn(); + const start = await service.start({ http, injectedMetadata, redirectTo }); + start.navigateToApp('myTestApp'); + expect(redirectTo).toHaveBeenCalledWith('/app/myTestApp'); + }); + }); }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 528b81ad40be7..d1855a0370f00 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -17,108 +17,43 @@ * under the License. */ -import { Observable, BehaviorSubject } from 'rxjs'; -import { CapabilitiesService, Capabilities } from './capabilities'; -import { InjectedMetadataStart } from '../injected_metadata'; -import { RecursiveReadonly } from '../../utils'; - -interface BaseApp { - id: string; - - /** - * An ordinal used to sort nav links relative to one another for display. - */ - order: number; - - /** - * The title of the application. - */ - title: string; - - /** - * An observable for a tooltip shown when hovering over app link. - */ - tooltip$?: Observable; - - /** - * A EUI iconType that will be used for the app's icon. This icon - * takes precendence over the `icon` property. - */ - euiIconType?: string; - - /** - * A URL to an image file used as an icon. Used as a fallback - * if `euiIconType` is not provided. - */ - icon?: string; - - /** - * Custom capabilities defined by the app. - */ - capabilities?: Partial; -} - -/** @public */ -export interface App extends BaseApp { - /** - * A mount function called when the user navigates to this app's `rootRoute`. - * @param targetDomElement An HTMLElement to mount the application onto. - * @returns An unmounting function that will be called to unmount the application. - */ - mount(targetDomElement: HTMLElement): () => void; -} - -/** @internal */ -export interface LegacyApp extends BaseApp { - appUrl: string; - subUrlBase?: string; - linkToLastSubUrl?: boolean; -} - -/** @internal */ -export type MixedApp = Partial & Partial & BaseApp; - -/** @public */ -export interface ApplicationSetup { - /** - * Register an mountable application to the system. Apps will be mounted based on their `rootRoute`. - * @param app - */ - registerApp(app: App): void; +import { createBrowserHistory } from 'history'; +import { BehaviorSubject } from 'rxjs'; +import React from 'react'; - /** - * Register metadata about legacy applications. Legacy apps will not be mounted when navigated to. - * @param app - * @internal - */ - registerLegacyApp(app: LegacyApp): void; +import { InjectedMetadataStart } from '../injected_metadata'; +import { CapabilitiesService } from './capabilities'; +import { AppRouter } from './ui'; +import { HttpStart } from '../http'; +import { ContextSetup, IContextContainer } from '../context'; +import { + AppMountContext, + App, + LegacyApp, + AppMounter, + AppUnmount, + AppMountParameters, + InternalApplicationSetup, + InternalApplicationStart, +} from './types'; + +interface SetupDeps { + context: ContextSetup; } -/** - * @public - */ -export interface ApplicationStart { - /** - * Gets the read-only capabilities. - */ - capabilities: RecursiveReadonly; - - /** - * Apps available based on the current capabilities. Should be used - * to show navigation links and make routing decisions. - */ - availableApps: readonly App[]; - +interface StartDeps { + http: HttpStart; + injectedMetadata: InjectedMetadataStart; /** - * Apps available based on the current capabilities. Should be used - * to show navigation links and make routing decisions. - * @internal + * Only necessary for redirecting to legacy apps + * @deprecated */ - availableLegacyApps: readonly LegacyApp[]; + redirectTo?: (path: string) => void; } -interface StartDeps { - injectedMetadata: InjectedMetadataStart; +interface AppBox { + app: App; + mount: AppMounter; } /** @@ -126,31 +61,122 @@ interface StartDeps { * @internal */ export class ApplicationService { - private readonly apps$ = new BehaviorSubject([]); - private readonly legacyApps$ = new BehaviorSubject([]); + private readonly apps$ = new BehaviorSubject>(new Map()); + private readonly legacyApps$ = new BehaviorSubject>(new Map()); private readonly capabilities = new CapabilitiesService(); + private mountContext?: IContextContainer< + AppMountContext, + AppUnmount | Promise, + [AppMountParameters] + >; + + public setup({ context }: SetupDeps): InternalApplicationSetup { + this.mountContext = context.createContextContainer(); - public setup(): ApplicationSetup { return { - registerApp: (app: App) => { - this.apps$.next([...this.apps$.value, app]); + register: (plugin: symbol, app: App) => { + if (this.apps$.value.has(app.id)) { + throw new Error(`An application is already registered with the id "${app.id}"`); + } + if (this.apps$.isStopped) { + throw new Error(`Applications cannot be registered after "setup"`); + } + + const appBox: AppBox = { + app, + mount: this.mountContext!.createHandler(plugin, app.mount), + }; + this.apps$.next(new Map([...this.apps$.value.entries(), [app.id, appBox]])); }, registerLegacyApp: (app: LegacyApp) => { - this.legacyApps$.next([...this.legacyApps$.value, app]); + if (this.legacyApps$.value.has(app.id)) { + throw new Error(`A legacy application is already registered with the id "${app.id}"`); + } + if (this.legacyApps$.isStopped) { + throw new Error(`Applications cannot be registered after "setup"`); + } + + this.legacyApps$.next(new Map([...this.legacyApps$.value.entries(), [app.id, app]])); }, + registerMountContext: this.mountContext.registerContext, }; } - public async start({ injectedMetadata }: StartDeps): Promise { + public async start({ + http, + injectedMetadata, + redirectTo = (path: string) => (window.location.href = path), + }: StartDeps): Promise { + if (!this.mountContext) { + throw new Error(`ApplicationService#setup() must be invoked before start.`); + } + + // Disable registration of new applications this.apps$.complete(); this.legacyApps$.complete(); - return this.capabilities.start({ - apps: this.apps$.value, + const legacyMode = injectedMetadata.getLegacyMode(); + const currentAppId$ = new BehaviorSubject(undefined); + const { availableApps, availableLegacyApps, capabilities } = await this.capabilities.start({ + apps: new Map([...this.apps$.value].map(([id, { app }]) => [id, app])), legacyApps: this.legacyApps$.value, injectedMetadata, }); + + // Only setup history if we're not in legacy mode + const history = legacyMode ? null : createBrowserHistory({ basename: http.basePath.get() }); + + return { + availableApps, + availableLegacyApps, + capabilities, + registerMountContext: this.mountContext.registerContext, + currentAppId$, + + getUrlForApp: (appId, options: { path?: string } = {}) => { + return http.basePath.prepend(appPath(appId, options)); + }, + + navigateToApp: (appId, { path, state }: { path?: string; state?: any } = {}) => { + if (legacyMode) { + // If we're in legacy mode, do a full page refresh to load the NP app. + redirectTo(http.basePath.prepend(appPath(appId, { path }))); + } else { + // basePath not needed here because `history` is configured with basename + history!.push(appPath(appId, { path }), state); + } + }, + + getComponent: () => { + if (legacyMode) { + return null; + } + + // Filter only available apps and map to just the mount function. + const appMounters = new Map( + [...this.apps$.value] + .filter(([id]) => availableApps.has(id)) + .map(([id, { mount }]) => [id, mount]) + ); + + return ( + + ); + }, + }; } public stop() {} } + +const appPath = (appId: string, { path }: { path?: string } = {}): string => + path + ? `/app/${appId}/${path.replace(/^\//, '')}` // Remove preceding slash from path if present + : `/app/${appId}`; diff --git a/src/core/public/application/capabilities/capabilities_service.mock.ts b/src/core/public/application/capabilities/capabilities_service.mock.ts index 71b069fd80434..29c3275f0e3b2 100644 --- a/src/core/public/application/capabilities/capabilities_service.mock.ts +++ b/src/core/public/application/capabilities/capabilities_service.mock.ts @@ -18,11 +18,11 @@ */ import { CapabilitiesService, CapabilitiesStart } from './capabilities_service'; import { deepFreeze } from '../../../utils/'; -import { App, LegacyApp } from '../application_service'; +import { App, LegacyApp } from '../types'; const createStartContractMock = ( - apps: readonly App[] = [], - legacyApps: readonly LegacyApp[] = [] + apps: ReadonlyMap = new Map(), + legacyApps: ReadonlyMap = new Map() ): jest.Mocked => ({ availableApps: apps, availableLegacyApps: legacyApps, diff --git a/src/core/public/application/capabilities/capabilities_service.test.ts b/src/core/public/application/capabilities/capabilities_service.test.ts index 1c60c1eeb195a..e80e9a7af321a 100644 --- a/src/core/public/application/capabilities/capabilities_service.test.ts +++ b/src/core/public/application/capabilities/capabilities_service.test.ts @@ -19,6 +19,7 @@ import { InjectedMetadataService } from '../../injected_metadata'; import { CapabilitiesService } from './capabilities_service'; +import { LegacyApp, App } from '../types'; describe('#start', () => { const injectedMetadata = new InjectedMetadataService({ @@ -39,17 +40,22 @@ describe('#start', () => { } as any, }).start(); - const apps = [{ id: 'app1' }, { id: 'app2', capabilities: { app2: { feature: true } } }] as any; - const legacyApps = [ - { id: 'legacyApp1' }, - { id: 'legacyApp2', capabilities: { app2: { feature: true } } }, - ] as any; + const apps = new Map([ + ['app1', { id: 'app1' }], + ['app2', { id: 'app2', capabilities: { app2: { feature: true } } }], + ] as Array<[string, App]>); + const legacyApps = new Map([ + ['legacyApp1', { id: 'legacyApp1' }], + ['legacyApp2', { id: 'legacyApp2', capabilities: { app2: { feature: true } } }], + ] as Array<[string, LegacyApp]>); it('filters available apps based on returned navLinks', async () => { const service = new CapabilitiesService(); const startContract = await service.start({ apps, legacyApps, injectedMetadata }); - expect(startContract.availableApps).toEqual([{ id: 'app1' }]); - expect(startContract.availableLegacyApps).toEqual([{ id: 'legacyApp1' }]); + expect(startContract.availableApps).toEqual(new Map([['app1', { id: 'app1' }]])); + expect(startContract.availableLegacyApps).toEqual( + new Map([['legacyApp1', { id: 'legacyApp1' }]]) + ); }); it('does not allow Capabilities to be modified', async () => { diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index 51c5a218e70bd..b080f8c138cf2 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -18,12 +18,12 @@ */ import { deepFreeze, RecursiveReadonly } from '../../../utils'; -import { LegacyApp, App } from '../application_service'; +import { LegacyApp, App } from '../types'; import { InjectedMetadataStart } from '../../injected_metadata'; interface StartDeps { - apps: readonly App[]; - legacyApps: readonly LegacyApp[]; + apps: ReadonlyMap; + legacyApps: ReadonlyMap; injectedMetadata: InjectedMetadataStart; } @@ -53,8 +53,8 @@ export interface Capabilities { /** @internal */ export interface CapabilitiesStart { capabilities: RecursiveReadonly; - availableApps: readonly App[]; - availableLegacyApps: readonly LegacyApp[]; + availableApps: ReadonlyMap; + availableLegacyApps: ReadonlyMap; } /** @@ -68,10 +68,23 @@ export class CapabilitiesService { injectedMetadata, }: StartDeps): Promise { const capabilities = deepFreeze(injectedMetadata.getCapabilities()); + const availableApps = new Map( + [...apps].filter( + ([appId]) => + capabilities.navLinks[appId] === undefined || capabilities.navLinks[appId] === true + ) + ); + + const availableLegacyApps = new Map( + [...legacyApps].filter( + ([appId]) => + capabilities.navLinks[appId] === undefined || capabilities.navLinks[appId] === true + ) + ); return { - availableApps: apps.filter(app => capabilities.navLinks[app.id]), - availableLegacyApps: legacyApps.filter(app => capabilities.navLinks[app.id]), + availableApps, + availableLegacyApps, capabilities, }; } diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index 137b46e6573e6..ae25b54cf07a8 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -17,5 +17,17 @@ * under the License. */ -export { ApplicationService, ApplicationSetup, ApplicationStart } from './application_service'; +export { ApplicationService } from './application_service'; export { Capabilities } from './capabilities'; +export { + App, + AppBase, + AppUnmount, + AppMountContext, + AppMountParameters, + ApplicationSetup, + ApplicationStart, + // Internal types + InternalApplicationStart, + LegacyApp, +} from './types'; diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx new file mode 100644 index 0000000000000..e6a1070e1a684 --- /dev/null +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -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 React from 'react'; +import ReactDOM from 'react-dom'; +import { mount, ReactWrapper } from 'enzyme'; +import { createMemoryHistory, History } from 'history'; +import { BehaviorSubject } from 'rxjs'; + +import { I18nProvider } from '@kbn/i18n/react'; + +import { AppMounter, LegacyApp, AppMountParameters } from '../types'; +import { httpServiceMock } from '../../http/http_service.mock'; +import { AppRouter, AppNotFound } from '../ui'; + +const createMountHandler = (htmlString: string) => + jest.fn(async ({ appBasePath: basename, element: el }: AppMountParameters) => { + ReactDOM.render( +
, + el + ); + return jest.fn(() => ReactDOM.unmountComponentAtNode(el)); + }); + +describe('AppContainer', () => { + let apps: Map, Parameters>>; + let legacyApps: Map; + let history: History; + let router: ReactWrapper; + let redirectTo: jest.Mock; + let currentAppId$: BehaviorSubject; + + const navigate = async (path: string) => { + history.push(path); + router.update(); + // flushes any pending promises + return new Promise(resolve => setImmediate(resolve)); + }; + + beforeEach(() => { + redirectTo = jest.fn(); + apps = new Map([ + ['app1', createMountHandler('App 1')], + ['app2', createMountHandler('
App 2
')], + ]); + legacyApps = new Map([ + ['legacyApp1', { id: 'legacyApp1' }], + ['baseApp:legacyApp2', { id: 'baseApp:legacyApp2' }], + ]) as Map; + history = createMemoryHistory(); + currentAppId$ = new BehaviorSubject(undefined); + // Use 'asdf' as the basepath + const http = httpServiceMock.createStartContract({ basePath: '/asdf' }); + router = mount( + + + + ); + }); + + it('calls mountHandler and returned unmount function when navigating between apps', async () => { + await navigate('/app/app1'); + expect(apps.get('app1')!).toHaveBeenCalled(); + expect(router.html()).toMatchInlineSnapshot(` + "
+ basename: /asdf/app/app1 + html: App 1 +
" + `); + + const app1Unmount = await apps.get('app1')!.mock.results[0].value; + await navigate('/app/app2'); + expect(app1Unmount).toHaveBeenCalled(); + + expect(apps.get('app2')!).toHaveBeenCalled(); + expect(router.html()).toMatchInlineSnapshot(` + "
+ basename: /asdf/app/app2 + html:
App 2
+
" + `); + }); + + it('updates currentApp$ after mounting', async () => { + await navigate('/app/app1'); + expect(currentAppId$.value).toEqual('app1'); + await navigate('/app/app2'); + expect(currentAppId$.value).toEqual('app2'); + }); + + it('sets window.location.href when navigating to legacy apps', async () => { + await navigate('/app/legacyApp1'); + expect(redirectTo).toHaveBeenCalledWith('/asdf/app/legacyApp1'); + }); + + it('handles legacy apps with subapps', async () => { + await navigate('/app/baseApp'); + expect(redirectTo).toHaveBeenCalledWith('/asdf/app/baseApp'); + }); + + it('displays error page if no app is found', async () => { + await navigate('/app/unknown'); + expect(router.exists(AppNotFound)).toBe(true); + }); +}); diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts new file mode 100644 index 0000000000000..018d7569ce411 --- /dev/null +++ b/src/core/public/application/types.ts @@ -0,0 +1,300 @@ +/* + * 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 { Observable, Subject } from 'rxjs'; + +import { Capabilities } from './capabilities'; +import { ChromeStart } from '../chrome'; +import { IContextProvider } from '../context'; +import { DocLinksStart } from '../doc_links'; +import { HttpStart } from '../http'; +import { I18nStart } from '../i18n'; +import { NotificationsStart } from '../notifications'; +import { OverlayStart } from '../overlays'; +import { PluginOpaqueId } from '../plugins'; +import { UiSettingsClientContract } from '../ui_settings'; +import { RecursiveReadonly } from '../../utils'; + +/** @public */ +export interface AppBase { + id: string; + + /** + * The title of the application. + */ + title: string; + + /** + * An ordinal used to sort nav links relative to one another for display. + */ + order?: number; + + /** + * An observable for a tooltip shown when hovering over app link. + */ + tooltip$?: Observable; + + /** + * A EUI iconType that will be used for the app's icon. This icon + * takes precendence over the `icon` property. + */ + euiIconType?: string; + + /** + * A URL to an image file used as an icon. Used as a fallback + * if `euiIconType` is not provided. + */ + icon?: string; + + /** + * Custom capabilities defined by the app. + */ + capabilities?: Partial; +} + +/** + * Extension of {@link AppBase | common app properties} with the mount function. + * @public + */ +export interface App extends AppBase { + /** + * A mount function called when the user navigates to this app's route. + * @param context The mount context for this app. + * @param targetDomElement An HTMLElement to mount the application onto. + * @returns An unmounting function that will be called to unmount the application. + */ + mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +} + +/** @internal */ +export interface LegacyApp extends AppBase { + appUrl: string; + subUrlBase?: string; + linkToLastSubUrl?: boolean; +} + +/** + * The context object received when applications are mounted to the DOM. + * @public + */ +export interface AppMountContext { + /** + * Core service APIs available to mounted applications. + */ + core: { + /** {@link ApplicationStart} */ + application: Pick; + /** {@link ChromeStart} */ + chrome: ChromeStart; + /** {@link DocLinksStart} */ + docLinks: DocLinksStart; + /** {@link HttpStart} */ + http: HttpStart; + /** {@link I18nStart} */ + i18n: I18nStart; + /** {@link NotificationsStart} */ + notifications: NotificationsStart; + /** {@link OverlayStart} */ + overlays: OverlayStart; + /** {@link UiSettingsClient} */ + uiSettings: UiSettingsClientContract; + }; +} + +/** @public */ +export interface AppMountParameters { + /** + * The container element to render the application into. + */ + element: HTMLElement; + + /** + * The base path for configuring the application's router. + * + * @example + * + * How to configure react-router with a base path: + * + * ```ts + * // inside your plugin's setup function + * 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); + * }, + * }); + * } + * ``` + * + * ```ts + * // application.tsx + * import React from 'react'; + * import ReactDOM from 'react-dom'; + * import { BrowserRouter, Route } from 'react-router-dom'; + * + * export renderApp = (context, { appBasePath, element }) => { + * ReactDOM.render( + * // pass `appBasePath` to `basename` + * + * + * , + * element + * ); + * + * return () => ReactDOM.unmountComponentAtNode(element); + * } + * ``` + */ + appBasePath: string; +} + +/** + * A function called when an application should be unmounted from the page. This function should be synchronous. + * @public + */ +export type AppUnmount = () => void; + +/** @internal */ +export type AppMounter = (params: AppMountParameters) => Promise; + +/** @public */ +export interface ApplicationSetup { + /** + * Register an mountable application to the system. + * @param app - an {@link App} + */ + register(app: App): void; + + /** + * Register a context provider for application mounting. Will only be available to applications that depend on the + * plugin that registered this context. + * + * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. + * @param provider - A {@link IContextProvider} function + */ + registerMountContext( + contextName: T, + provider: IContextProvider + ): void; +} + +/** @internal */ +export interface InternalApplicationSetup { + /** + * Register an mountable application to the system. + * @param plugin - opaque ID of the plugin that registers this application + * @param app + */ + register(plugin: PluginOpaqueId, app: App): void; + + /** + * Register metadata about legacy applications. Legacy apps will not be mounted when navigated to. + * @param app + * @internal + */ + registerLegacyApp(app: LegacyApp): void; + + /** + * Register a context provider for application mounting. Will only be available to applications that depend on the + * plugin that registered this context. + * + * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. + * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. + * @param provider - A {@link IContextProvider} function + */ + registerMountContext( + pluginOpaqueId: PluginOpaqueId, + contextName: T, + provider: IContextProvider + ): void; +} + +/** @public */ +export interface ApplicationStart { + /** + * Gets the read-only capabilities. + */ + capabilities: RecursiveReadonly; + + /** + * Navigiate to a given app + * + * @param appId + * @param options.path - optional path inside application to deep link to + * @param options.state - optional state to forward to the application + */ + navigateToApp(appId: string, options?: { path?: string; state?: any }): void; + + /** + * Returns a relative URL to a given app, including the global base path. + * @param appId + * @param options.path - optional path inside application to deep link to + */ + getUrlForApp(appId: string, options?: { path?: string }): string; + + /** + * Register a context provider for application mounting. Will only be available to applications that depend on the + * plugin that registered this context. + * + * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. + * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. + * @param provider - A {@link IContextProvider} function + */ + registerMountContext( + contextName: T, + provider: IContextProvider + ): void; +} + +/** @internal */ +export interface InternalApplicationStart + extends Pick { + /** + * Apps available based on the current capabilities. Should be used + * to show navigation links and make routing decisions. + */ + availableApps: ReadonlyMap; + /** + * Apps available based on the current capabilities. Should be used + * to show navigation links and make routing decisions. + * @internal + */ + availableLegacyApps: ReadonlyMap; + + /** + * Register a context provider for application mounting. Will only be available to applications that depend on the + * plugin that registered this context. + * + * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. + * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. + * @param provider - A {@link IContextProvider} function + */ + registerMountContext( + pluginOpaqueId: PluginOpaqueId, + contextName: T, + provider: IContextProvider + ): void; + + // Internal APIs + currentAppId$: Subject; + getComponent(): JSX.Element | null; +} diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx new file mode 100644 index 0000000000000..876cd3aa3a3d3 --- /dev/null +++ b/src/core/public/application/ui/app_container.tsx @@ -0,0 +1,111 @@ +/* + * 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 { RouteComponentProps } from 'react-router-dom'; +import { Subject } from 'rxjs'; + +import { LegacyApp, AppMounter, AppUnmount } from '../types'; +import { HttpStart } from '../../http'; +import { AppNotFound } from './app_not_found_screen'; + +interface Props extends RouteComponentProps<{ appId: string }> { + apps: ReadonlyMap; + legacyApps: ReadonlyMap; + basePath: HttpStart['basePath']; + currentAppId$: Subject; + /** + * Only necessary for redirecting to legacy apps + * @deprecated + */ + redirectTo: (path: string) => void; +} + +interface State { + appNotFound: boolean; +} + +export class AppContainer extends React.Component { + private readonly containerDiv = React.createRef(); + private unmountFunc?: AppUnmount; + + state: State = { appNotFound: false }; + + componentDidMount() { + this.mountApp(); + } + + componentWillUnmount() { + this.unmountApp(); + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.match.params.appId !== this.props.match.params.appId) { + this.unmountApp(); + this.mountApp(); + } + } + + async mountApp() { + const { apps, legacyApps, match, basePath, currentAppId$, redirectTo } = this.props; + const { appId } = match.params; + + const mount = apps.get(appId); + if (mount) { + this.unmountFunc = await mount({ + appBasePath: basePath.prepend(`/app/${appId}`), + element: this.containerDiv.current!, + }); + currentAppId$.next(appId); + this.setState({ appNotFound: false }); + return; + } + + const legacyApp = findLegacyApp(appId, legacyApps); + if (legacyApp) { + this.unmountApp(); + redirectTo(basePath.prepend(`/app/${appId}`)); + this.setState({ appNotFound: false }); + return; + } + + this.setState({ appNotFound: true }); + } + + async unmountApp() { + if (this.unmountFunc) { + this.unmountFunc(); + this.unmountFunc = undefined; + } + } + + render() { + return ( + + {this.state.appNotFound && } +
+ + ); + } +} + +function findLegacyApp(appId: string, apps: ReadonlyMap) { + const matchingApps = [...apps.entries()].filter(([id]) => id.split(':')[0] === appId); + return matchingApps.length ? matchingApps[0][1] : null; +} diff --git a/src/core/public/application/ui/app_not_found_screen.tsx b/src/core/public/application/ui/app_not_found_screen.tsx new file mode 100644 index 0000000000000..73a999c5dbf16 --- /dev/null +++ b/src/core/public/application/ui/app_not_found_screen.tsx @@ -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. + */ + +import { EuiEmptyPrompt, EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const AppNotFound = () => ( + + + + + + + } + body={ +

+ +

+ } + /> +
+
+
+); diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx new file mode 100644 index 0000000000000..9d8acf1978556 --- /dev/null +++ b/src/core/public/application/ui/app_router.tsx @@ -0,0 +1,53 @@ +/* + * 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 { History } from 'history'; +import React from 'react'; +import { Router, Route } from 'react-router-dom'; +import { Subject } from 'rxjs'; + +import { LegacyApp, AppMounter } from '../types'; +import { AppContainer } from './app_container'; +import { HttpStart } from '../../http'; + +interface Props { + apps: ReadonlyMap; + legacyApps: ReadonlyMap; + basePath: HttpStart['basePath']; + currentAppId$: Subject; + history: History; + /** + * Only necessary for redirecting to legacy apps + * @deprecated + */ + redirectTo?: (path: string) => void; +} + +export const AppRouter: React.StatelessComponent = ({ + history, + redirectTo = (path: string) => (window.location.href = path), + ...otherProps +}) => ( + + } + /> + +); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/colors.js b/src/core/public/application/ui/index.ts similarity index 78% rename from src/legacy/core_plugins/metrics/public/visualizations/lib/colors.js rename to src/core/public/application/ui/index.ts index d3c7e2191f996..7fa778740d3b4 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/colors.js +++ b/src/core/public/application/ui/index.ts @@ -17,10 +17,5 @@ * under the License. */ -export const COLORS = { - lineColor: 'rgba(105,112,125,0.2)', - textColor: 'rgba(0,0,0,0.4)', - textColorReversed: 'rgba(255,255,255,0.5)', - valueColor: 'rgba(0,0,0,0.7)', - valueColorReversed: 'rgba(255,255,255,0.8)', -}; +export { AppRouter } from './app_router'; +export { AppNotFound } from './app_not_found_screen'; diff --git a/src/core/public/chrome/chrome_service.mock.ts b/src/core/public/chrome/chrome_service.mock.ts index 74f2a09b895de..3775989c5126b 100644 --- a/src/core/public/chrome/chrome_service.mock.ts +++ b/src/core/public/chrome/chrome_service.mock.ts @@ -27,7 +27,7 @@ import { const createStartContractMock = () => { const startContract: DeeplyMockedKeys = { - getComponent: jest.fn(), + getHeaderComponent: jest.fn(), navLinks: { getNavLinks$: jest.fn(), has: jest.fn(), diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts index 392846f8433ba..45e94040eeb4a 100644 --- a/src/core/public/chrome/chrome_service.test.ts +++ b/src/core/public/chrome/chrome_service.test.ts @@ -38,7 +38,7 @@ const store = new Map(); function defaultStartDeps() { return { - application: applicationServiceMock.createStartContract(), + application: applicationServiceMock.createInternalStartContract(), docLinks: docLinksServiceMock.createStartContract(), http: httpServiceMock.createStartContract(), injectedMetadata: injectedMetadataServiceMock.createStartContract(), @@ -87,7 +87,7 @@ Array [ const start = await service.start(defaultStartDeps()); // 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.getComponent()))).toBeDefined(); + expect(shallow(React.createElement(() => start.getHeaderComponent()))).toBeDefined(); }); }); diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index d829a27260d27..02195c794d280 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -27,7 +27,7 @@ import { IconType } from '@elastic/eui'; import { InjectedMetadataStart } from '../injected_metadata'; import { NotificationsStart } from '../notifications'; -import { ApplicationStart } from '../application'; +import { InternalApplicationStart } from '../application'; import { HttpStart } from '../http'; import { ChromeNavLinks, NavLinksService } from './nav_links'; @@ -74,7 +74,7 @@ interface ConstructorParams { } interface StartDeps { - application: ApplicationStart; + application: InternalApplicationStart; docLinks: DocLinksStart; http: HttpStart; injectedMetadata: InjectedMetadataStart; @@ -84,14 +84,11 @@ interface StartDeps { /** @internal */ export class ChromeService { private readonly stop$ = new ReplaySubject(1); - private readonly browserSupportsCsp: boolean; private readonly navControls = new NavControlsService(); private readonly navLinks = new NavLinksService(); private readonly recentlyAccessed = new RecentlyAccessedService(); - constructor({ browserSupportsCsp }: ConstructorParams) { - this.browserSupportsCsp = browserSupportsCsp; - } + constructor(private readonly params: ConstructorParams) {} public async start({ application, @@ -115,7 +112,7 @@ export class ChromeService { const navLinks = this.navLinks.start({ application, http }); const recentlyAccessed = await this.recentlyAccessed.start({ http }); - if (!this.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { + if (!this.params.browserSupportsCsp && injectedMetadata.getCspConfig().warnLegacyBrowsers) { notifications.toasts.addWarning( i18n.translate('core.chrome.legacyBrowserWarning', { defaultMessage: 'Your browser does not meet the security requirements for Kibana.', @@ -128,11 +125,12 @@ export class ChromeService { navLinks, recentlyAccessed, - getComponent: () => ( + getHeaderComponent: () => (
([ + [ + 'legacyApp1', + { id: 'legacyApp1', order: 0, title: 'Legacy App 1', icon: 'legacyApp1', appUrl: '/app1' }, + ], + [ + 'legacyApp2', + { + id: 'legacyApp2', + order: -10, + title: 'Legacy App 2', + euiIconType: 'canvasApp', + appUrl: '/app2', + }, + ], + ['legacyApp3', { id: 'legacyApp3', order: 20, title: 'Legacy App 3', appUrl: '/app3' }], + ]), } as any; const mockHttp = { 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 2250ec40f0f44..affc639faf0b8 100644 --- a/src/core/public/chrome/nav_links/nav_links_service.ts +++ b/src/core/public/chrome/nav_links/nav_links_service.ts @@ -21,11 +21,11 @@ import { sortBy } from 'lodash'; import { BehaviorSubject, ReplaySubject, Observable } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; import { NavLinkWrapper, ChromeNavLinkUpdateableFields, ChromeNavLink } from './nav_link'; -import { ApplicationStart } from '../../application'; +import { InternalApplicationStart } from '../../application'; import { HttpStart } from '../../http'; interface StartDeps { - application: ApplicationStart; + application: InternalApplicationStart; http: HttpStart; } @@ -99,10 +99,22 @@ export class NavLinksService { private readonly stop$ = new ReplaySubject(1); public start({ application, http }: StartDeps): ChromeNavLinks { - const legacyAppLinks = application.availableLegacyApps.map( - app => + const appLinks = [...application.availableApps].map( + ([appId, app]) => [ - app.id, + appId, + new NavLinkWrapper({ + ...app, + legacy: false, + baseUrl: relativeToAbsolute(http.basePath.prepend(`/app/${appId}`)), + }), + ] as [string, NavLinkWrapper] + ); + + const legacyAppLinks = [...application.availableLegacyApps].map( + ([appId, app]) => + [ + appId, new NavLinkWrapper({ ...app, legacy: true, @@ -112,7 +124,7 @@ export class NavLinksService { ); const navLinks$ = new BehaviorSubject>( - new Map(legacyAppLinks) + new Map([...legacyAppLinks, ...appLinks]) ); const forceAppSwitcherNavigation$ = new BehaviorSubject(false); diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 4dc64c57fa244..afd9f8e4a3820 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -65,6 +65,7 @@ import { } from '../..'; import { HttpStart } from '../../../http'; import { ChromeHelpExtension } from '../../chrome_service'; +import { ApplicationStart, InternalApplicationStart } from '../../../application/types'; // Providing a buffer between the limit and the cut off index // protects from truncating just the last couple (6) characters @@ -115,13 +116,24 @@ function extendRecentlyAccessedHistoryItem( }; } -function extendNavLink(navLink: ChromeNavLink) { +function extendNavLink(navLink: ChromeNavLink, urlForApp: ApplicationStart['getUrlForApp']) { + if (navLink.legacy) { + return { + ...navLink, + href: navLink.url && !navLink.active ? navLink.url : navLink.baseUrl, + }; + } + return { ...navLink, - href: navLink.url && !navLink.active ? navLink.url : navLink.baseUrl, + href: urlForApp(navLink.id), }; } +function isModifiedEvent(event: MouseEvent) { + return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); +} + function findClosestAnchor(element: HTMLElement): HTMLAnchorElement | void { let current = element; while (current) { @@ -149,6 +161,7 @@ export type HeaderProps = Pick>; interface Props { kibanaVersion: string; + application: InternalApplicationStart; appTitle$: Rx.Observable; badge$: Rx.Observable; breadcrumbs$: Rx.Observable; @@ -159,6 +172,7 @@ interface Props { recentlyAccessed$: Rx.Observable; forceAppSwitcherNavigation$: Rx.Observable; helpExtension$: Rx.Observable; + legacyMode: boolean; navControlsLeft$: Rx.Observable; navControlsRight$: Rx.Observable; intl: InjectedIntl; @@ -169,6 +183,7 @@ interface Props { interface State { appTitle: string; + currentAppId?: string; isVisible: boolean; navLinks: ReadonlyArray>; recentlyAccessed: ReadonlyArray>; @@ -203,7 +218,11 @@ class HeaderUI extends Component { this.props.navLinks$, this.props.recentlyAccessed$, // Types for combineLatest only handle up to 6 inferred types so we combine these two separately. - Rx.combineLatest(this.props.navControlsLeft$, this.props.navControlsRight$) + Rx.combineLatest( + this.props.navControlsLeft$, + this.props.navControlsRight$, + this.props.application.currentAppId$ + ) ).subscribe({ next: ([ appTitle, @@ -211,18 +230,21 @@ class HeaderUI extends Component { forceNavigation, navLinks, recentlyAccessed, - [navControlsLeft, navControlsRight], + [navControlsLeft, navControlsRight, currentAppId], ]) => { this.setState({ appTitle, isVisible, forceNavigation, - navLinks: navLinks.map(navLink => extendNavLink(navLink)), + navLinks: navLinks.map(navLink => + extendNavLink(navLink, this.props.application.getUrlForApp) + ), recentlyAccessed: recentlyAccessed.map(ra => extendRecentlyAccessedHistoryItem(navLinks, ra, this.props.basePath) ), navControlsLeft, navControlsRight, + currentAppId, }); }, }); @@ -263,6 +285,7 @@ class HeaderUI extends Component { public render() { const { + application, badge$, basePath, breadcrumbs$, @@ -272,9 +295,11 @@ class HeaderUI extends Component { kibanaDocLink, kibanaVersion, onIsLockedUpdate, + legacyMode, } = this.props; const { appTitle, + currentAppId, isVisible, navControlsLeft, navControlsRight, @@ -291,9 +316,26 @@ class HeaderUI extends Component { .map(navLink => ({ key: navLink.id, label: navLink.title, + + // Use href and onClick to support "open in new tab" and SPA navigation in the same link href: navLink.href, + onClick: (event: MouseEvent) => { + if ( + !legacyMode && // ignore when in legacy mode + !navLink.legacy && // ignore links to legacy apps + !event.defaultPrevented && // onClick prevented default + event.button === 0 && // ignore everything but left clicks + !isModifiedEvent(event) // ignore clicks with modifier keys + ) { + event.preventDefault(); + application.navigateToApp(navLink.id); + } + }, + + // Legacy apps use `active` property, NP apps should match the current app + isActive: navLink.active || currentAppId === navLink.id, isDisabled: navLink.disabled, - isActive: navLink.active, + iconType: navLink.euiIconType, icon: !navLink.euiIconType && navLink.icon ? ( diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 7310a8f33eba4..895fc785b11b1 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -272,7 +272,9 @@ describe('#start()', () => { await startCore(); expect(MockRenderingService.start).toHaveBeenCalledTimes(1); expect(MockRenderingService.start).toHaveBeenCalledWith({ + application: expect.any(Object), chrome: expect.any(Object), + injectedMetadata: expect.any(Object), targetDomElement: expect.any(HTMLElement), }); }); @@ -364,7 +366,7 @@ describe('LegacyPlatformService targetDomElement', () => { it('only mounts the element when start, after setting up the legacyPlatformService', async () => { const core = createCoreSystem(); - let targetDomElementInStart: HTMLElement | null; + let targetDomElementInStart: HTMLElement | undefined; MockLegacyPlatformService.start.mockImplementation(({ targetDomElement }) => { targetDomElementInStart = targetDomElement; }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 7782c93c7bbb1..4eb16572d8fec 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -20,23 +20,29 @@ import './core.css'; import { CoreId } from '../server'; -import { InternalCoreSetup, InternalCoreStart } from '.'; +import { CoreSetup, CoreStart } from '.'; import { ChromeService } from './chrome'; import { FatalErrorsService, FatalErrorsSetup } from './fatal_errors'; import { HttpService } from './http'; import { I18nService } from './i18n'; -import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata'; +import { + InjectedMetadataParams, + InjectedMetadataService, + InjectedMetadataSetup, + InjectedMetadataStart, +} from './injected_metadata'; import { LegacyPlatformParams, LegacyPlatformService } from './legacy'; import { NotificationsService } from './notifications'; import { OverlayService } from './overlays'; import { PluginsService } from './plugins'; import { UiSettingsService } from './ui_settings'; import { ApplicationService } from './application'; -import { mapToObject } from '../utils/'; +import { mapToObject, pick } from '../utils/'; import { DocLinksService } from './doc_links'; import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects/saved_objects_service'; import { ContextService } from './context'; +import { InternalApplicationSetup, InternalApplicationStart } from './application/types'; interface Params { rootDomElement: HTMLElement; @@ -51,6 +57,18 @@ export interface CoreContext { coreId: CoreId; } +/** @internal */ +export interface InternalCoreSetup extends Omit { + application: InternalApplicationSetup; + injectedMetadata: InjectedMetadataSetup; +} + +/** @internal */ +export interface InternalCoreStart extends Omit { + application: InternalApplicationStart; + injectedMetadata: InjectedMetadataStart; +} + /** * The CoreSystem is the root of the new platform, and setups all parts * of Kibana in the UI, including the LegacyPlatform which is managed @@ -77,6 +95,7 @@ export class CoreSystem { private readonly context: ContextService; private readonly rootDomElement: HTMLElement; + private readonly coreContext: CoreContext; private fatalErrorsSetup: FatalErrorsSetup | null = null; constructor(params: Params) { @@ -106,14 +125,14 @@ export class CoreSystem { this.savedObjects = new SavedObjectsService(); this.uiSettings = new UiSettingsService(); this.overlay = new OverlayService(); - this.application = new ApplicationService(); this.chrome = new ChromeService({ browserSupportsCsp }); this.docLinks = new DocLinksService(); this.rendering = new RenderingService(); + this.application = new ApplicationService(); - const core: CoreContext = { coreId: Symbol('core') }; - this.context = new ContextService(core); - this.plugins = new PluginsService(core, injectedMetadata.uiPlugins); + this.coreContext = { coreId: Symbol('core') }; + this.context = new ContextService(this.coreContext); + this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.legacyPlatform = new LegacyPlatformService({ requireLegacyFiles, @@ -133,10 +152,10 @@ export class CoreSystem { const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); - const application = this.application.setup(); const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ pluginDependencies }); + const application = this.application.setup({ context }); const core: InternalCoreSetup = { application, @@ -150,7 +169,11 @@ export class CoreSystem { // Services that do not expose contracts at setup const plugins = await this.plugins.setup(core); - await this.legacyPlatform.setup({ core, plugins: mapToObject(plugins.contracts) }); + + await this.legacyPlatform.setup({ + core, + plugins: mapToObject(plugins.contracts), + }); return { fatalErrors: this.fatalErrorsSetup }; } catch (error) { @@ -171,7 +194,7 @@ export class CoreSystem { const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const savedObjects = await this.savedObjects.start({ http }); const i18n = await this.i18n.start(); - const application = await this.application.start({ injectedMetadata }); + const application = await this.application.start({ http, injectedMetadata }); const coreUiTargetDomElement = document.createElement('div'); coreUiTargetDomElement.id = 'kibana-body'; @@ -200,6 +223,17 @@ export class CoreSystem { }); const uiSettings = await this.uiSettings.start(); + application.registerMountContext(this.coreContext.coreId, 'core', () => ({ + application: pick(application, ['capabilities', 'navigateToApp']), + chrome, + docLinks, + http, + i18n, + notifications, + overlays, + uiSettings, + })); + const core: InternalCoreStart = { application, chrome, @@ -215,9 +249,12 @@ export class CoreSystem { const plugins = await this.plugins.start(core); const rendering = this.rendering.start({ + application, chrome, + injectedMetadata, targetDomElement: coreUiTargetDomElement, }); + await this.legacyPlatform.start({ core, plugins: mapToObject(plugins.contracts), diff --git a/src/core/public/http/http_service.mock.ts b/src/core/public/http/http_service.mock.ts index 4ce84f8ab38d1..a94543414acfa 100644 --- a/src/core/public/http/http_service.mock.ts +++ b/src/core/public/http/http_service.mock.ts @@ -25,7 +25,7 @@ type ServiceSetupMockType = jest.Mocked & { basePath: jest.Mocked; }; -const createServiceMock = (): ServiceSetupMockType => ({ +const createServiceMock = ({ basePath = '' } = {}): ServiceSetupMockType => ({ fetch: jest.fn(), get: jest.fn(), head: jest.fn(), @@ -35,8 +35,8 @@ const createServiceMock = (): ServiceSetupMockType => ({ delete: jest.fn(), options: jest.fn(), basePath: { - get: jest.fn(), - prepend: jest.fn(), + get: jest.fn(() => basePath), + prepend: jest.fn(path => `${basePath}${path}`), remove: jest.fn(), }, addLoadingCount: jest.fn(), @@ -46,22 +46,19 @@ const createServiceMock = (): ServiceSetupMockType => ({ removeAllInterceptors: jest.fn(), }); -const createSetupContractMock = createServiceMock; -const createStartContractMock = createServiceMock; - -const createMock = () => { +const createMock = ({ basePath = '' } = {}) => { const mocked: jest.Mocked> = { setup: jest.fn(), start: jest.fn(), stop: jest.fn(), }; - mocked.setup.mockReturnValue(createSetupContractMock()); - mocked.start.mockReturnValue(createSetupContractMock()); + mocked.setup.mockReturnValue(createServiceMock({ basePath })); + mocked.start.mockReturnValue(createServiceMock({ basePath })); return mocked; }; export const httpServiceMock = { create: createMock, - createSetupContract: createSetupContractMock, - createStartContract: createStartContractMock, + createSetupContract: createServiceMock, + createStartContract: createServiceMock, }; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index abc922ff97c1d..89c309d8427d7 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -71,6 +71,9 @@ import { IContextContainer, IContextProvider, ContextSetup, IContextHandler } fr export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; + +export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application'; + export { SavedObjectsBatchResponse, SavedObjectsBulkCreateObject, @@ -114,6 +117,8 @@ export { * https://github.com/Microsoft/web-build-tools/issues/1237 */ export interface CoreSetup { + /** {@link ApplicationSetup} */ + application: ApplicationSetup; /** {@link ContextSetup} */ context: ContextSetup; /** {@link FatalErrorsSetup} */ @@ -137,7 +142,7 @@ export interface CoreSetup { */ export interface CoreStart { /** {@link ApplicationStart} */ - application: Pick; + application: ApplicationStart; /** {@link ChromeStart} */ chrome: ChromeStart; /** {@link DocLinksStart} */ @@ -156,15 +161,33 @@ export interface CoreStart { uiSettings: UiSettingsClientContract; } -/** @internal */ -export interface InternalCoreSetup extends CoreSetup { - application: ApplicationSetup; +/** + * Setup interface exposed to the legacy platform via the `ui/new_platform` module. + * + * @remarks + * Some methods are not supported in the legacy platform and while present to make this type compatibile with + * {@link CoreSetup}, unsupported methods will throw exceptions when called. + * + * @public + * @deprecated + */ +export interface LegacyCoreSetup extends CoreSetup { + /** @deprecated */ injectedMetadata: InjectedMetadataSetup; } -/** @internal */ -export interface InternalCoreStart extends CoreStart { - application: ApplicationStart; +/** + * Start interface exposed to the legacy platform via the `ui/new_platform` module. + * + * @remarks + * Some methods are not supported in the legacy platform and while present to make this type compatibile with + * {@link CoreStart}, unsupported methods will throw exceptions when called. + * + * @public + * @deprecated + */ +export interface LegacyCoreStart extends CoreStart { + /** @deprecated */ injectedMetadata: InjectedMetadataStart; } diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index c4579bee3f131..9e1d5aeec7ff4 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -25,6 +25,7 @@ const createSetupContractMock = () => { getKibanaBranch: jest.fn(), getCapabilities: jest.fn(), getCspConfig: jest.fn(), + getLegacyMode: jest.fn(), getLegacyMetadata: jest.fn(), getPlugins: jest.fn(), getInjectedVar: jest.fn(), @@ -34,6 +35,7 @@ const createSetupContractMock = () => { setupContract.getCapabilities.mockReturnValue({} as any); setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); + setupContract.getLegacyMode.mockReturnValue(true); setupContract.getLegacyMetadata.mockReturnValue({ nav: [], uiSettings: { diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 9fbc955485512..fa93d0f5288b4 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -51,6 +51,7 @@ export interface InjectedMetadataParams { plugin: DiscoveredPlugin; }>; capabilities: Capabilities; + legacyMode: boolean; legacyMetadata: { app: unknown; translations: unknown; @@ -112,6 +113,10 @@ export class InjectedMetadataService { return this.state.uiPlugins; }, + getLegacyMode: () => { + return this.state.legacyMode; + }, + getLegacyMetadata: () => { return this.state.legacyMetadata; }, @@ -156,6 +161,8 @@ export interface InjectedMetadataSetup { id: string; plugin: DiscoveredPlugin; }>; + /** Indicates whether or not we are rendering a known legacy app. */ + getLegacyMode: () => boolean; getLegacyMetadata: () => { app: unknown; translations: unknown; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index eb5b3e90f1a52..37e07af0a7da5 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -61,7 +61,7 @@ import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsMock } from '../saved_objects/saved_objects_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; -const applicationSetup = applicationServiceMock.createSetupContract(); +const applicationSetup = applicationServiceMock.createInternalSetupContract(); const contextSetup = contextServiceMock.createSetupContract(); const fatalErrorsSetup = fatalErrorsServiceMock.createSetupContract(); const httpSetup = httpServiceMock.createSetupContract(); @@ -88,7 +88,7 @@ const defaultSetupDeps = { plugins: {}, }; -const applicationStart = applicationServiceMock.createStartContract(); +const applicationStart = applicationServiceMock.createInternalStartContract(); const docLinksStart = docLinksServiceMock.createStartContract(); const httpStart = httpServiceMock.createStartContract(); const chromeStart = chromeServiceMock.createStartContract(); @@ -98,6 +98,7 @@ const notificationsStart = notificationServiceMock.createStartContract(); const overlayStart = overlayServiceMock.createStartContract(); const uiSettingsStart = uiSettingsServiceMock.createStartContract(); const savedObjectsStart = savedObjectsMock.createStartContract(); +const mockStorage = { getItem: jest.fn() } as any; const defaultStartDeps = { core: { @@ -112,6 +113,7 @@ const defaultStartDeps = { uiSettings: uiSettingsStart, savedObjects: savedObjectsStart, }, + lastSubUrlStorage: mockStorage, targetDomElement: document.createElement('div'), plugins: {}, }; @@ -132,12 +134,29 @@ describe('#setup()', () => { legacyPlatform.setup(defaultSetupDeps); expect(mockUiNewPlatformSetup).toHaveBeenCalledTimes(1); - expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(defaultSetupDeps.core, {}); + expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(expect.any(Object), {}); }); }); }); describe('#start()', () => { + it('fetches and sets legacy lastSubUrls', () => { + chromeStart.navLinks.getAll.mockReturnValue([ + { id: 'link1', baseUrl: 'http://wowza.com/app1', legacy: true } as any, + ]); + mockStorage.getItem.mockReturnValue('http://wowza.com/app1/subUrl'); + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start({ ...defaultStartDeps, lastSubUrlStorage: mockStorage }); + + expect(chromeStart.navLinks.update).toHaveBeenCalledWith('link1', { + url: 'http://wowza.com/app1/subUrl', + }); + }); + it('initializes ui/new_platform with core APIs', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, @@ -147,7 +166,7 @@ describe('#start()', () => { legacyPlatform.start(defaultStartDeps); expect(mockUiNewPlatformStart).toHaveBeenCalledTimes(1); - expect(mockUiNewPlatformStart).toHaveBeenCalledWith(defaultStartDeps.core, {}); + expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {}); }); describe('useLegacyTestHarness = false', () => { diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 7d852773ad03f..ba93cd7b6b5a7 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -18,7 +18,8 @@ */ import angular from 'angular'; -import { InternalCoreSetup, InternalCoreStart } from '../'; +import { InternalCoreSetup, InternalCoreStart } from '../core_system'; +import { LegacyCoreSetup, LegacyCoreStart } from '../'; /** @internal */ export interface LegacyPlatformParams { @@ -34,7 +35,8 @@ interface SetupDeps { interface StartDeps { core: InternalCoreStart; plugins: Record; - targetDomElement: HTMLElement; + lastSubUrlStorage?: Storage; + targetDomElement?: HTMLElement; } interface BootstrapModule { @@ -55,10 +57,7 @@ export class LegacyPlatformService { constructor(private readonly params: LegacyPlatformParams) {} public setup({ core, plugins }: SetupDeps) { - // Inject parts of the new platform into parts of the legacy platform - // so that legacy APIs/modules can mimic their new platform counterparts - require('ui/new_platform').__setup__(core, plugins); - + // Always register legacy apps, even if not in legacy mode. core.injectedMetadata.getLegacyMetadata().nav.forEach((navLink: any) => core.application.registerLegacyApp({ id: navLink.id, @@ -71,12 +70,57 @@ export class LegacyPlatformService { linkToLastSubUrl: navLink.linkToLastSubUrl, }) ); + + const legacyCore: LegacyCoreSetup = { + ...core, + application: { + register: notSupported(`core.application.register()`), + registerMountContext: notSupported(`core.application.registerMountContext()`), + }, + }; + + // Inject parts of the new platform into parts of the legacy platform + // so that legacy APIs/modules can mimic their new platform counterparts + if (core.injectedMetadata.getLegacyMode()) { + require('ui/new_platform').__setup__(legacyCore, plugins); + } } - public start({ core, targetDomElement, plugins }: StartDeps) { + public start({ + core, + targetDomElement, + plugins, + lastSubUrlStorage = window.sessionStorage, + }: StartDeps) { + // Initialize legacy sub urls + core.chrome.navLinks + .getAll() + .filter(link => link.legacy) + .forEach(navLink => { + const lastSubUrl = lastSubUrlStorage.getItem(`lastSubUrl:${navLink.baseUrl}`); + core.chrome.navLinks.update(navLink.id, { + url: lastSubUrl || navLink.url || navLink.baseUrl, + }); + }); + + // Only import and bootstrap legacy platform if we're in legacy mode. + if (!core.injectedMetadata.getLegacyMode()) { + return; + } + + const legacyCore: LegacyCoreStart = { + ...core, + application: { + capabilities: core.application.capabilities, + getUrlForApp: core.application.getUrlForApp, + navigateToApp: core.application.navigateToApp, + registerMountContext: notSupported(`core.application.registerMountContext()`), + }, + }; + // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts - require('ui/new_platform').__start__(core, plugins); + require('ui/new_platform').__start__(legacyCore, plugins); // Load the bootstrap module before loading the legacy platform files so that // the bootstrap module can modify the environment a bit first @@ -91,7 +135,8 @@ export class LegacyPlatformService { this.targetDomElement = targetDomElement; - this.bootstrapModule.bootstrap(this.targetDomElement); + // `targetDomElement` is always defined when in legacy mode + this.bootstrapModule.bootstrap(this.targetDomElement!); } public stop() { @@ -129,3 +174,7 @@ export class LegacyPlatformService { return require('ui/chrome'); } } + +const notSupported = (methodName: string) => (...args: any[]) => { + throw new Error(`${methodName} is not supported in the legacy platform.`); +}; diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 0f3a01c793ae3..7c99f69d6fd7a 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -42,6 +42,7 @@ export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; function createCoreSetupMock() { const mock: MockedKeys = { + application: applicationServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index 66cb7c4a1171e..f4e25d27447bc 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -76,7 +76,12 @@ export function createPluginSetupContext< plugin: PluginWrapper ): CoreSetup { return { - context: omit(deps.context, 'setCurrentPlugin'), + application: { + register: app => deps.application.register(plugin.opaqueId, app), + registerMountContext: (contextName, provider) => + deps.application.registerMountContext(plugin.opaqueId, contextName, provider), + }, + context: deps.context, fatalErrors: deps.fatalErrors, http: deps.http, notifications: deps.notifications, @@ -107,6 +112,10 @@ export function createPluginStartContext< return { application: { capabilities: deps.application.capabilities, + navigateToApp: deps.application.navigateToApp, + getUrlForApp: deps.application.getUrlForApp, + registerMountContext: (contextName, provider) => + deps.application.registerMountContext(plugin.opaqueId, contextName, provider), }, docLinks: deps.docLinks, http: deps.http, diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 2b689e45b4f1a..d6411554e5f85 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -72,7 +72,7 @@ beforeEach(() => { }, ]; mockSetupDeps = { - application: applicationServiceMock.createSetupContract(), + application: applicationServiceMock.createInternalSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), http: httpServiceMock.createSetupContract(), @@ -81,10 +81,11 @@ beforeEach(() => { uiSettings: uiSettingsServiceMock.createSetupContract(), }; mockSetupContext = { - ...omit(mockSetupDeps, 'application', 'injectedMetadata'), + ...omit(mockSetupDeps, 'injectedMetadata'), + application: expect.any(Object), }; mockStartDeps = { - application: applicationServiceMock.createStartContract(), + application: applicationServiceMock.createInternalStartContract(), docLinks: docLinksServiceMock.createStartContract(), http: httpServiceMock.createStartContract(), chrome: chromeServiceMock.createStartContract(), @@ -97,9 +98,7 @@ beforeEach(() => { }; mockStartContext = { ...omit(mockStartDeps, 'injectedMetadata'), - application: { - capabilities: mockStartDeps.application.capabilities, - }, + application: expect.any(Object), chrome: omit(mockStartDeps.chrome, 'getComponent'), }; diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 13a52d78d72fc..1ab9d7f2fa9b2 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -26,7 +26,7 @@ import { createPluginSetupContext, createPluginStartContext, } from './plugin_context'; -import { InternalCoreSetup, InternalCoreStart } from '..'; +import { InternalCoreSetup, InternalCoreStart } from '../core_system'; /** @internal */ export type PluginsServiceSetupDeps = InternalCoreSetup; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 060188608b860..552476425d8d8 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -11,24 +11,65 @@ import React from 'react'; import * as Rx from 'rxjs'; import { EuiGlobalToastListToast as Toast } from '@elastic/eui'; +// @public +export interface App extends AppBase { + mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +} + +// @public (undocumented) +export interface AppBase { + capabilities?: Partial; + euiIconType?: string; + icon?: string; + // (undocumented) + id: string; + order?: number; + title: string; + tooltip$?: Observable; +} + // @public (undocumented) export interface ApplicationSetup { - // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts - registerApp(app: App): void; - // Warning: (ae-forgotten-export) The symbol "LegacyApp" needs to be exported by the entry point index.d.ts - // - // @internal - registerLegacyApp(app: LegacyApp): void; + register(app: App): void; + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public (undocumented) export interface ApplicationStart { - availableApps: readonly App[]; - // @internal - availableLegacyApps: readonly LegacyApp[]; capabilities: RecursiveReadonly; + getUrlForApp(appId: string, options?: { + path?: string; + }): string; + navigateToApp(appId: string, options?: { + path?: string; + state?: any; + }): void; + registerMountContext(contextName: T, provider: IContextProvider): void; +} + +// @public +export interface AppMountContext { + core: { + application: Pick; + chrome: ChromeStart; + docLinks: DocLinksStart; + http: HttpStart; + i18n: I18nStart; + notifications: NotificationsStart; + overlays: OverlayStart; + uiSettings: UiSettingsClientContract; + }; +} + +// @public (undocumented) +export interface AppMountParameters { + appBasePath: string; + element: HTMLElement; } +// @public +export type AppUnmount = () => void; + // @public export interface Capabilities { [key: string]: Record>; @@ -105,7 +146,7 @@ export interface ChromeNavLink { readonly legacy: boolean; // @deprecated readonly linkToLastSubUrl?: boolean; - readonly order: number; + readonly order?: number; // @deprecated readonly subUrlBase?: string; readonly title: string; @@ -185,6 +226,8 @@ export interface CoreContext { // @public export interface CoreSetup { + // (undocumented) + application: ApplicationSetup; // (undocumented) context: ContextSetup; // (undocumented) @@ -200,7 +243,7 @@ export interface CoreSetup { // @public export interface CoreStart { // (undocumented) - application: Pick; + application: ApplicationStart; // (undocumented) chrome: ChromeStart; // (undocumented) @@ -505,23 +548,19 @@ export type IContextHandler, TContextName extends keyof TContext, TProviderParameters extends any[] = []> = (context: Partial, ...rest: TProviderParameters) => Promise | TContext[TContextName]; -// @internal (undocumented) -export interface InternalCoreSetup extends CoreSetup { - // (undocumented) - application: ApplicationSetup; +// @public @deprecated +export interface LegacyCoreSetup extends CoreSetup { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts // - // (undocumented) + // @deprecated (undocumented) injectedMetadata: InjectedMetadataSetup; } -// @internal (undocumented) -export interface InternalCoreStart extends CoreStart { - // (undocumented) - application: ApplicationStart; +// @public @deprecated +export interface LegacyCoreStart extends CoreStart { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataStart" needs to be exported by the entry point index.d.ts // - // (undocumented) + // @deprecated (undocumented) injectedMetadata: InjectedMetadataStart; } diff --git a/src/core/public/rendering/rendering_service.test.tsx b/src/core/public/rendering/rendering_service.test.tsx index 5b4ab93996657..9a4b46c657f08 100644 --- a/src/core/public/rendering/rendering_service.test.tsx +++ b/src/core/public/rendering/rendering_service.test.tsx @@ -21,46 +21,67 @@ import React from 'react'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { RenderingService } from './rendering_service'; +import { InternalApplicationStart } from '../application'; +import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock'; describe('RenderingService#start', () => { - const getService = () => { + const getService = ({ legacyMode = false }: { legacyMode?: boolean } = {}) => { const rendering = new RenderingService(); + const application = { + getComponent: () =>
Hello application!
, + } as InternalApplicationStart; const chrome = chromeServiceMock.createStartContract(); - chrome.getComponent.mockReturnValue(
Hello chrome!
); + chrome.getHeaderComponent.mockReturnValue(
Hello chrome!
); + const injectedMetadata = injectedMetadataServiceMock.createStartContract(); + injectedMetadata.getLegacyMode.mockReturnValue(legacyMode); const targetDomElement = document.createElement('div'); - const start = rendering.start({ chrome, targetDomElement }); + const start = rendering.start({ application, chrome, injectedMetadata, targetDomElement }); return { start, targetDomElement }; }; - it('renders into provided DOM element', () => { + it('renders application service into provided DOM element', () => { const { targetDomElement } = getService(); - expect(targetDomElement).toMatchInlineSnapshot(` -
-
-
- Hello chrome! -
-
-
-
-`); + expect(targetDomElement.querySelector('div.application')).toMatchInlineSnapshot(` +
+
+ Hello application! +
+
+ `); }); - it('returns a div for the legacy service to render into', () => { - const { - start: { legacyTargetDomElement }, - targetDomElement, - } = getService(); - legacyTargetDomElement.innerHTML = 'Hello legacy!'; - expect(targetDomElement.querySelector('#legacy')).toMatchInlineSnapshot(` - - Hello legacy! - -`); + it('contains wrapper divs', () => { + const { targetDomElement } = getService(); + expect(targetDomElement.querySelector('div.app-wrapper')).toBeDefined(); + expect(targetDomElement.querySelector('div.app-wrapper-pannel')).toBeDefined(); + }); + + describe('legacyMode', () => { + it('renders into provided DOM element', () => { + const { targetDomElement } = getService({ legacyMode: true }); + expect(targetDomElement).toMatchInlineSnapshot(` +
+
+
+ Hello chrome! +
+
+
+
+ `); + }); + + it('returns a div for the legacy service to render into', () => { + const { + start: { legacyTargetDomElement }, + targetDomElement, + } = getService({ legacyMode: true }); + expect(targetDomElement.contains(legacyTargetDomElement!)).toBe(true); + }); }); }); diff --git a/src/core/public/rendering/rendering_service.tsx b/src/core/public/rendering/rendering_service.tsx index cbb931bf59ef9..2e066feca8bf3 100644 --- a/src/core/public/rendering/rendering_service.tsx +++ b/src/core/public/rendering/rendering_service.tsx @@ -22,9 +22,13 @@ import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { InternalChromeStart } from '../chrome'; +import { InternalApplicationStart } from '../application'; +import { InjectedMetadataStart } from '../injected_metadata'; interface StartDeps { + application: InternalApplicationStart; chrome: InternalChromeStart; + injectedMetadata: InjectedMetadataStart; targetDomElement: HTMLDivElement; } @@ -39,28 +43,40 @@ interface StartDeps { * @internal */ export class RenderingService { - start({ chrome, targetDomElement }: StartDeps) { - const chromeUi = chrome.getComponent(); - const legacyRef = React.createRef(); + start({ application, chrome, injectedMetadata, targetDomElement }: StartDeps): RenderingStart { + const chromeUi = chrome.getHeaderComponent(); + const appUi = application.getComponent(); + + const legacyMode = injectedMetadata.getLegacyMode(); + const legacyRef = legacyMode ? React.createRef() : null; ReactDOM.render(
{chromeUi} -
+ {!legacyMode && ( +
+
+
{appUi}
+
+
+ )} + + {legacyMode &&
}
, targetDomElement ); return { - legacyTargetDomElement: legacyRef.current!, + // When in legacy mode, return legacy div, otherwise undefined. + legacyTargetDomElement: legacyRef ? legacyRef.current! : undefined, }; } } /** @internal */ export interface RenderingStart { - legacyTargetDomElement: HTMLDivElement; + legacyTargetDomElement?: HTMLDivElement; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 3df5ebe6f5806..46432cbb0da75 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -113,7 +113,7 @@ export function createPluginSetupContext( null, plugin.opaqueId ), - createRouter: () => deps.http.createRouter(`/${plugin.name}`, plugin.opaqueId), + createRouter: () => deps.http.createRouter('', plugin.opaqueId), registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, diff --git a/src/core/utils/context.mock.ts b/src/core/utils/context.mock.ts index d59d0066c4e6e..4d91c11542b2f 100644 --- a/src/core/utils/context.mock.ts +++ b/src/core/utils/context.mock.ts @@ -24,7 +24,9 @@ export type ContextContainerMock = jest.Mocked> const createContextMock = () => { const contextMock: ContextContainerMock = { registerContext: jest.fn(), - createHandler: jest.fn(), + createHandler: jest.fn((id, handler) => (...args: any[]) => + Promise.resolve(handler({}, ...args)) + ), }; contextMock.createHandler.mockImplementation((pluginId, handler) => (...args) => handler({}, ...args) diff --git a/src/core/utils/pick.ts b/src/core/utils/pick.ts index d55c76a3ca77d..77854f9af680b 100644 --- a/src/core/utils/pick.ts +++ b/src/core/utils/pick.ts @@ -17,10 +17,7 @@ * under the License. */ -export function pick, K extends keyof T>( - obj: T, - keys: K[] -): Pick { +export function pick(obj: T, keys: K[]): Pick { return keys.reduce( (acc, key) => { if (obj.hasOwnProperty(key)) { diff --git a/src/dev/build/tasks/nodejs/node_shasums.test.ts b/src/dev/build/tasks/nodejs/node_shasums.test.ts new file mode 100644 index 0000000000000..ee91d2a370fb1 --- /dev/null +++ b/src/dev/build/tasks/nodejs/node_shasums.test.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const mockResponse = `155ae63f0bb47050e0c31b4f8c17dadc79dcfa8e8f4ec9e3974fd7592afa9a4f node-v8.9.4-aix-ppc64.tar.gz +ca50f7d2035eb805306e303b644bb1cde170ce2615e0a2c6e95fb80881c48c24 node-v8.9.4-darwin-x64.tar.gz +cb79e2da37d2b646a06adaddcda67ff6ba0f77f9ca733b041dabf3dad79c7468 node-v8.9.4-darwin-x64.tar.xz +ef7248e81706daeeec946c19808a50b60ac250e648365d78fda6e40f1f9b23a5 node-v8.9.4-headers.tar.gz +11ed407a4bc3d8c3e73305ac54e91e64c9a9f6a2ae5476791d6fcc14ac159bfc node-v8.9.4-headers.tar.xz +2b133c7d23033fbc2419e66fc08bba35c427a97aba83ed6848b6b4678c0cac65 node-v8.9.4-linux-arm64.tar.gz +7c0369a5dbc98d0989c208ca3ee1b6db4cba576343014fdbf7d36fd2659f7089 node-v8.9.4-linux-arm64.tar.xz +81f138e935323246bd5da518eb0ea8ad00008f3c8a8d606e17589a545a9c73d1 node-v8.9.4-linux-armv6l.tar.gz +501bcae62ea1769924facc9628f407d37753e7a024cf3b12a18ea9dab1b380c9 node-v8.9.4-linux-armv6l.tar.xz +a0dd9009cb8d4be89c8a31131df16ad5ea1580d10ae426c5142aa34b0ad4ea76 node-v8.9.4-linux-armv7l.tar.gz +fe19f195df3d4f362d0cf0eef43c1a6a0b6006a1be2a89ee1808091c2ef4d722 node-v8.9.4-linux-armv7l.tar.xz +c5df73b8571edf97f83b484d6139332fad3b710d51be4aeb8d846059862d4675 node-v8.9.4-linux-ppc64le.tar.gz +21178be5e4c1dbdd99610d24aa934234a368c542ebabb3d98c31d393cf4adf06 node-v8.9.4-linux-ppc64le.tar.xz +d6e53ab2f8364528d4c6800adc1e7fccec607fd07a97b83985732c749a7fc846 node-v8.9.4-linux-s390x.tar.gz +90c6c284db9482a478dd5110e2171435156d56a013aeda2f636b6240eba156bd node-v8.9.4-linux-s390x.tar.xz +21fb4690e349f82d708ae766def01d7fec1b085ce1f5ab30d9bda8ee126ca8fc node-v8.9.4-linux-x64.tar.gz +68b94aac38cd5d87ab79c5b38306e34a20575f31a3ea788d117c20fffcca3370 node-v8.9.4-linux-x64.tar.xz +cc2f7a300353422ede336f5e72b71f0d6eac46732a31b7640648378830dd7513 node-v8.9.4-linux-x86.tar.gz +79f241f31eab5dfe2976fb0633c598dababd207ab0b8a163004f296cd7794a65 node-v8.9.4-linux-x86.tar.xz +b93767f7e186b1ae7204fedafa4110534f577d18d4204f422b626afdd5061e28 node-v8.9.4.pkg +e4a5d945091043c937125cd0d515258785cd4ea806fe3b77000d888de23d2ba0 node-v8.9.4-sunos-x64.tar.gz +b33e8f1495b88fcc0ab1e2579f2f7cf4d39886d577430dcb920a024829d4cf28 node-v8.9.4-sunos-x64.tar.xz +551729411793e427f5760fe8e46f45612e1e8e7c63e55ad34243ebf8ea9a4a7a node-v8.9.4-sunos-x86.tar.gz +6b439bb7204362c0af7a654bce24fcf8059e1772b2f0a9e4e1f8a0b8caa85d26 node-v8.9.4-sunos-x86.tar.xz +729b44b32b2f82ecd5befac4f7518de0c4e3add34e8fe878f745740a66cbbc01 node-v8.9.4.tar.gz +6cdcde9c9c1ca9f450a0b24eafa229ca759e576daa0fae892ce74d541ecdc86f node-v8.9.4.tar.xz +15a847a28358f9ae40bae42f49b033b0180bc10661632c63a9c8487ae980a8ba node-v8.9.4-win-x64.7z +48946e99ac4484e071df25741d2300f3a656f476c5ff3f8116a4746c07ebe3b7 node-v8.9.4-win-x64.zip +50ad674fb4c89edf35d3fee2136da86631cb7c0504589eb71ce8a3bb176493ed node-v8.9.4-win-x86.7z +02e3c65000ac055e05c604aec4cf318212efbd4b60a945ed319072d58314ca32 node-v8.9.4-win-x86.zip +547689da69bacadfee619d208702b73698d14297bd5fef5d80656897989e91b6 node-v8.9.4-x64.msi +f9442188c2f66d167a0ac610dee6d16e226ba28ca93f9569e0276268eb8f85dc node-v8.9.4-x86.msi +b73841f25d6e75d635770fd1a32e4d74d6ab2feed0fd7708bb40b967ae06f33e win-x64/node.exe +5439dc6f0d632ecdeb7342986743a03fe0818e34f0a67e38de74fa9c94886a39 win-x64/node.lib +6ab35445dd564978019cf4f3cfe11dd342b8450015fc054df99aa6f35f21736a win-x64/node_pdb.7z +c064abba981c2373e7e1a8c53b4e4ed1d4927bd9c0f7c065b24dd13b731598bd win-x64/node_pdb.zip +c8430b20cd067d8784d5faae04f9447987a472b22b6d0a2403ea4362ecd3d0bc win-x86/node.exe +c4edece2c0aa68e816c4e067f397eb12e9d0c81bb37b3d349dbaf47cf246b0b7 win-x86/node.lib +6a2ee7a0b0074ece27d171418d82ce25a60b87750ec30c5c9fbeaaca8c206fa5 win-x86/node_pdb.7z +1b44176d888c1bc6a6b05fcc6234031b3b8a58da9de8b99661088f998ac5e269 win-x86/node_pdb.zip`; + +jest.mock('axios', () => ({ + async get(url: string) { + expect(url).toBe('https://nodejs.org/dist/v8.9.4/SHASUMS256.txt'); + return { + status: 200, + data: mockResponse, + }; + }, +})); + +import { getNodeShasums } from './node_shasums'; + +describe('src/dev/build/tasks/nodejs/node_shasums', () => { + it('resolves to an object with shasums for node downloads for version', async () => { + const shasums = await getNodeShasums('8.9.4'); + expect(shasums).toEqual( + expect.objectContaining({ + 'node-v8.9.4.tar.gz': '729b44b32b2f82ecd5befac4f7518de0c4e3add34e8fe878f745740a66cbbc01', + 'node-v8.9.4-win-x64.zip': + '48946e99ac4484e071df25741d2300f3a656f476c5ff3f8116a4746c07ebe3b7', + }) + ); + }); +}); diff --git a/src/dev/build/tasks/nodejs/node_shasums.js b/src/dev/build/tasks/nodejs/node_shasums.ts similarity index 77% rename from src/dev/build/tasks/nodejs/node_shasums.js rename to src/dev/build/tasks/nodejs/node_shasums.ts index 150e9d4f1508b..1b8d01a9b1d94 100644 --- a/src/dev/build/tasks/nodejs/node_shasums.js +++ b/src/dev/build/tasks/nodejs/node_shasums.ts @@ -17,21 +17,21 @@ * under the License. */ -import wreck from '@hapi/wreck'; +import axios from 'axios'; -export async function getNodeShasums(nodeVersion) { +export async function getNodeShasums(nodeVersion: string) { const url = `https://nodejs.org/dist/v${nodeVersion}/SHASUMS256.txt`; - const { res, payload } = await wreck.get(url); + const { status, data } = await axios.get(url); - if (res.statusCode !== 200) { - throw new Error(`${url} failed with a ${res.statusCode} response`); + if (status !== 200) { + throw new Error(`${url} failed with a ${status} response`); } - return payload + return data .toString('utf8') .split('\n') - .reduce((acc, line) => { + .reduce((acc: Record, line: string) => { const [sha, platform] = line.split(' '); return { diff --git a/src/dev/jest/config.integration.js b/src/dev/jest/config.integration.js index 8348b7594961f..6ffa9ac362430 100644 --- a/src/dev/jest/config.integration.js +++ b/src/dev/jest/config.integration.js @@ -24,6 +24,7 @@ export default { testMatch: [ '**/integration_tests/**/*.test.js', '**/integration_tests/**/*.test.ts', + '**/integration_tests/**/*.test.tsx', ], testPathIgnorePatterns: config.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/expand_panel_action.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/expand_panel_action.tsx index 273234cf8d8d5..494110750fcf3 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/expand_panel_action.tsx +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/actions/expand_panel_action.tsx @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Action, IEmbeddable, - ActionContext, IncompatibleActionError, } from '../../../../../../embeddable_api/public/np_ready/public'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable'; @@ -40,7 +39,11 @@ function isExpanded(embeddable: IEmbeddable) { return embeddable.id === embeddable.parent.getInput().expandedPanelId; } -export class ExpandPanelAction extends Action { +interface ActionContext { + embeddable: IEmbeddable; +} + +export class ExpandPanelAction extends Action { public readonly type = EXPAND_PANEL_ACTION; constructor() { @@ -80,7 +83,7 @@ export class ExpandPanelAction extends Action { return Boolean(embeddable.parent && isDashboard(embeddable.parent)); } - public execute({ embeddable }: ActionContext) { + public async execute({ embeddable }: ActionContext) { if (!embeddable.parent || !isDashboard(embeddable.parent)) { throw new IncompatibleActionError(); } diff --git a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/embeddable/dashboard_container.tsx b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/embeddable/dashboard_container.tsx index 8dda752ba0b48..dc7aee514ab26 100644 --- a/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/embeddable/dashboard_container.tsx +++ b/src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public/lib/embeddable/dashboard_container.tsx @@ -21,7 +21,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { Filter } from '@kbn/es-query'; -import { RefreshInterval, TimeRange } from '../../../../../../../../plugins/data/public'; +import { RefreshInterval, TimeRange } from 'src/plugins/data/public'; import { Container, ContainerInput, diff --git a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx index 310a3b11a8456..e5358acc1c05c 100644 --- a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx @@ -19,47 +19,60 @@ import { useRef, useEffect } from 'react'; import React from 'react'; -import { Ast } from '@kbn/interpreter/common'; -import { ExpressionRunnerOptions, ExpressionRunner } from './expression_runner'; -import { Result } from './expressions_service'; +import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './lib/_types'; +import { IExpressionLoader, ExpressionLoader } from './lib/loader'; // Accept all options of the runner as props except for the // dom element which is provided by the component itself -export type ExpressionRendererProps = Pick< - ExpressionRunnerOptions, - Exclude -> & { - expression: string | Ast; +export interface ExpressionRendererProps extends IExpressionLoaderParams { + className: 'string'; + expression: string | ExpressionAST; /** * If an element is specified, but the response of the expression run can't be rendered * because it isn't a valid response or the specified renderer isn't available, * this callback is called with the given result. */ - onRenderFailure?: (result: Result) => void; -}; + onRenderFailure?: (result: IInterpreterResult) => void; +} export type ExpressionRenderer = React.FC; -export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({ +export const createRenderer = (loader: IExpressionLoader): ExpressionRenderer => ({ + className, expression, onRenderFailure, ...options }: ExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); + const handlerRef: React.MutableRefObject = useRef(null); + useEffect(() => { if (mountpoint.current) { - run(expression, { ...options, element: mountpoint.current }).catch(result => { + if (!handlerRef.current) { + handlerRef.current = loader(mountpoint.current, expression, options); + } else { + handlerRef.current.update(expression, options); + } + handlerRef.current.data$.toPromise().catch(result => { if (onRenderFailure) { onRenderFailure(result); } }); } - }, [expression, mountpoint.current]); + }, [ + expression, + options.searchContext, + options.context, + options.variables, + options.disableCaching, + mountpoint.current, + ]); return (
{ mountpoint.current = el; }} diff --git a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts deleted file mode 100644 index 86a36c9409a48..0000000000000 --- a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts +++ /dev/null @@ -1,75 +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 { Ast, fromExpression } from '@kbn/interpreter/common'; - -import { RequestAdapter, DataAdapter } from '../../../../../plugins/inspector/public'; -import { RenderFunctionsRegistry, Interpreter, Result } from './expressions_service'; - -export interface ExpressionRunnerOptions { - // TODO use the real types here once they are ready - context?: object; - getInitialContext?: () => object; - element?: Element; -} - -export type ExpressionRunner = ( - expression: string | Ast, - options: ExpressionRunnerOptions -) => Promise; - -export const createRunFn = ( - renderersRegistry: RenderFunctionsRegistry, - interpreterPromise: Promise -): ExpressionRunner => async (expressionOrAst, { element, context, getInitialContext }) => { - // TODO: make interpreter initialization synchronous to avoid this - const interpreter = await interpreterPromise; - const ast = - typeof expressionOrAst === 'string' ? fromExpression(expressionOrAst) : expressionOrAst; - - const response = await interpreter.interpretAst(ast, context || { type: 'null' }, { - getInitialContext: getInitialContext || (() => ({})), - inspectorAdapters: { - // TODO connect real adapters - requests: new RequestAdapter(), - data: new DataAdapter(), - }, - }); - - if (response.type === 'error') { - throw response; - } - - if (element) { - if (response.type === 'render' && response.as && renderersRegistry.get(response.as) !== null) { - renderersRegistry.get(response.as).render(element, response.value, { - onDestroy: fn => { - // TODO implement - }, - done: () => { - // TODO implement - }, - }); - } else { - throw response; - } - } - - return response; -}; diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx deleted file mode 100644 index 83313e5a2b2fe..0000000000000 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx +++ /dev/null @@ -1,276 +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 { fromExpression, Ast } from '@kbn/interpreter/common'; - -import { - ExpressionsService, - RenderFunctionsRegistry, - RenderFunction, - Interpreter, - ExpressionsServiceDependencies, - Result, - ExpressionsSetup, -} from './expressions_service'; -import { mount } from 'enzyme'; -import React from 'react'; - -const waitForInterpreterRun = async () => { - // Wait for two ticks with empty callback queues - // This makes sure the runFn promise and actual interpretAst - // promise have been resolved and processed - await new Promise(resolve => setTimeout(resolve)); - await new Promise(resolve => setTimeout(resolve)); -}; - -const RENDERER_ID = 'mockId'; - -describe('expressions_service', () => { - let interpretAstMock: jest.Mocked['interpretAst']; - let interpreterMock: jest.Mocked; - let renderFunctionMock: jest.Mocked; - let setupPluginsMock: ExpressionsServiceDependencies; - const expressionResult: Result = { type: 'render', as: RENDERER_ID, value: {} }; - - let api: ExpressionsSetup; - let testExpression: string; - let testAst: Ast; - - beforeEach(() => { - interpretAstMock = jest.fn((..._) => Promise.resolve(expressionResult)); - interpreterMock = { interpretAst: interpretAstMock }; - renderFunctionMock = ({ - render: jest.fn(), - } as unknown) as jest.Mocked; - setupPluginsMock = { - interpreter: { - getInterpreter: () => Promise.resolve({ interpreter: interpreterMock }), - renderersRegistry: ({ - get: (id: string) => (id === RENDERER_ID ? renderFunctionMock : null), - } as unknown) as RenderFunctionsRegistry, - }, - }; - api = new ExpressionsService().setup(setupPluginsMock); - testExpression = 'test | expression'; - testAst = fromExpression(testExpression); - }); - - describe('expression_runner', () => { - it('should return run function', () => { - expect(typeof api.run).toBe('function'); - }); - - it('should call the interpreter with parsed expression', async () => { - await api.run(testExpression, { element: document.createElement('div') }); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - }); - - it('should call the interpreter with given context and getInitialContext functions', async () => { - const getInitialContext = () => ({}); - const context = {}; - - await api.run(testExpression, { getInitialContext, context }); - const interpretCall = interpreterMock.interpretAst.mock.calls[0]; - - expect(interpretCall[1]).toBe(context); - expect(interpretCall[2].getInitialContext).toBe(getInitialContext); - }); - - it('should call the interpreter with passed in ast', async () => { - await api.run(testAst, { element: document.createElement('div') }); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - }); - - it('should return the result of the interpreter run', async () => { - const response = await api.run(testAst, {}); - expect(response).toBe(expressionResult); - }); - - it('should reject the promise if the response is not renderable but an element is passed', async () => { - const unexpectedResult = { type: 'datatable', value: {} }; - interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult)); - expect( - api.run(testAst, { - element: document.createElement('div'), - }) - ).rejects.toBe(unexpectedResult); - }); - - it('should reject the promise if the renderer is not known', async () => { - const unexpectedResult = { type: 'render', as: 'unknown_id' }; - interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult)); - expect( - api.run(testAst, { - element: document.createElement('div'), - }) - ).rejects.toBe(unexpectedResult); - }); - - it('should not reject the promise on unknown renderer if the runner is not rendering', async () => { - const unexpectedResult = { type: 'render', as: 'unknown_id' }; - interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult)); - expect(api.run(testAst, {})).resolves.toBe(unexpectedResult); - }); - - it('should reject the promise if the response is an error', async () => { - const errorResult = { type: 'error', error: {} }; - interpretAstMock.mockReturnValue(Promise.resolve(errorResult)); - expect(api.run(testAst, {})).rejects.toBe(errorResult); - }); - - it('should reject the promise if there are syntax errors', async () => { - expect(api.run('|||', {})).rejects.toBeInstanceOf(Error); - }); - - it('should call the render function with the result and element', async () => { - const element = document.createElement('div'); - - await api.run(testAst, { element }); - expect(renderFunctionMock.render).toHaveBeenCalledWith( - element, - expressionResult.value, - expect.anything() - ); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - }); - }); - - describe('expression_renderer', () => { - it('should call interpreter and render function when called through react component', async () => { - const ExpressionRenderer = api.ExpressionRenderer; - - mount(); - - await waitForInterpreterRun(); - - expect(renderFunctionMock.render).toHaveBeenCalledWith( - expect.any(Element), - expressionResult.value, - expect.anything() - ); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - }); - - it('should call the interpreter with given context and getInitialContext functions', async () => { - const getInitialContext = () => ({}); - const context = {}; - - const ExpressionRenderer = api.ExpressionRenderer; - - mount( - - ); - - await waitForInterpreterRun(); - - const interpretCall = interpreterMock.interpretAst.mock.calls[0]; - - expect(interpretCall[1]).toBe(context); - expect(interpretCall[2].getInitialContext).toBe(getInitialContext); - }); - - it('should call interpreter and render function again if expression changes', async () => { - const ExpressionRenderer = api.ExpressionRenderer; - - const instance = mount(); - - await waitForInterpreterRun(); - - expect(renderFunctionMock.render).toHaveBeenCalledWith( - expect.any(Element), - expressionResult.value, - expect.anything() - ); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - - instance.setProps({ expression: 'supertest | expression ' }); - - await waitForInterpreterRun(); - - expect(renderFunctionMock.render).toHaveBeenCalledTimes(2); - expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(2); - }); - - it('should not call interpreter and render function again if expression does not change', async () => { - const ast = fromExpression(testExpression); - - const ExpressionRenderer = api.ExpressionRenderer; - - const instance = mount(); - - await waitForInterpreterRun(); - - expect(renderFunctionMock.render).toHaveBeenCalledWith( - expect.any(Element), - expressionResult.value, - expect.anything() - ); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - ast, - expect.anything(), - expect.anything() - ); - - instance.update(); - - await waitForInterpreterRun(); - - expect(renderFunctionMock.render).toHaveBeenCalledTimes(1); - expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(1); - }); - - it('should call onRenderFailure if the result can not be rendered', async () => { - const errorResult = { type: 'error', error: {} }; - interpretAstMock.mockReturnValue(Promise.resolve(errorResult)); - const renderFailureSpy = jest.fn(); - - const ExpressionRenderer = api.ExpressionRenderer; - - mount(); - - await waitForInterpreterRun(); - - expect(renderFailureSpy).toHaveBeenCalledWith(errorResult); - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts index fc4cd853afe14..a00512f04c1ac 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts @@ -17,107 +17,51 @@ * under the License. */ -import { Ast } from '@kbn/interpreter/common'; +import { npSetup } from 'ui/new_platform'; +// @ts-ignore -// TODO: -// this type import and the types below them should be switched to the types of -// the interpreter plugin itself once they are ready -import { Registry } from '@kbn/interpreter/common'; -import { Adapters } from 'src/plugins/inspector/public'; -import { Filter } from '@kbn/es-query'; -import { TimeRange } from 'src/plugins/data/public'; +import { setInspector, setInterpreter } from './services'; +import { execute } from './lib/execute'; +import { loader } from './lib/loader'; +import { render } from './lib/render'; import { createRenderer } from './expression_renderer'; -import { createRunFn } from './expression_runner'; -import { Query } from '../query'; -export interface InitialContextObject { - timeRange?: TimeRange; - filters?: Filter[]; - query?: Query; -} - -export type getInitialContextFunction = () => InitialContextObject; - -export interface Handlers { - getInitialContext: getInitialContextFunction; - inspectorAdapters?: Adapters; - abortSignal?: AbortSignal; -} - -type Context = object; -export interface Result { - type: string; - as?: string; - value?: unknown; - error?: unknown; -} - -interface RenderHandlers { - done: () => void; - onDestroy: (fn: () => void) => void; -} - -export interface RenderFunction { - name: string; - displayName: string; - help: string; - validate: () => void; - reuseDomNode: boolean; - render: (domNode: Element, data: unknown, handlers: RenderHandlers) => void; -} - -export type RenderFunctionsRegistry = Registry; +import { Start as IInspector } from '../../../../../plugins/inspector/public'; -export interface Interpreter { - interpretAst(ast: Ast, context: Context, handlers: Handlers): Promise; +export interface ExpressionsServiceStartDependencies { + inspector: IInspector; } - -type InterpreterGetter = () => Promise<{ interpreter: Interpreter }>; - -export interface ExpressionsServiceDependencies { - interpreter: { - renderersRegistry: RenderFunctionsRegistry; - getInterpreter: InterpreterGetter; - }; -} - /** * Expressions Service * @internal */ export class ExpressionsService { - public setup({ - interpreter: { renderersRegistry, getInterpreter }, - }: ExpressionsServiceDependencies) { - const run = createRunFn( - renderersRegistry, - getInterpreter().then(({ interpreter }) => interpreter) - ); + public setup() { + // eslint-disable-next-line + const { getInterpreter } = require('../../../interpreter/public/interpreter'); + getInterpreter() + .then(setInterpreter) + .catch((e: Error) => { + throw new Error('interpreter is not initialized'); + }); + + return { + registerType: npSetup.plugins.data.expressions.registerType, + registerFunction: npSetup.plugins.data.expressions.registerFunction, + registerRenderer: npSetup.plugins.data.expressions.registerRenderer, + }; + } + + public start({ inspector }: ExpressionsServiceStartDependencies) { + const ExpressionRenderer = createRenderer(loader); + setInspector(inspector); return { - /** - * **experimential** This API is experimential and might be removed in the future - * without notice - * - * Executes the given expression string or ast and renders the result into the - * given DOM element. - * - * - * @param expressionOrAst - * @param element - */ - run, - /** - * **experimential** This API is experimential and might be removed in the future - * without notice - * - * Component which executes and renders the given expression in a div element. - * The expression is re-executed on updating the props. - * - * This is a React bridge of the `run` method - * @param props - */ - ExpressionRenderer: createRenderer(run), + execute, + render, + loader, + + ExpressionRenderer, }; } @@ -128,3 +72,4 @@ export class ExpressionsService { /** @public */ export type ExpressionsSetup = ReturnType; +export type ExpressionsStart = ReturnType; diff --git a/src/legacy/core_plugins/data/public/expressions/index.ts b/src/legacy/core_plugins/data/public/expressions/index.ts index fceefce44f81f..ae51804a85fb5 100644 --- a/src/legacy/core_plugins/data/public/expressions/index.ts +++ b/src/legacy/core_plugins/data/public/expressions/index.ts @@ -17,6 +17,5 @@ * under the License. */ -export { ExpressionsService, ExpressionsSetup } from './expressions_service'; +export { ExpressionsService, ExpressionsSetup, ExpressionsStart } from './expressions_service'; export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; -export { ExpressionRunner } from './expression_runner'; diff --git a/src/legacy/core_plugins/data/public/expressions/lib/_types.ts b/src/legacy/core_plugins/data/public/expressions/lib/_types.ts new file mode 100644 index 0000000000000..b3185fda2c178 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/lib/_types.ts @@ -0,0 +1,86 @@ +/* + * 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 { Filter } from '@kbn/es-query'; +import { Adapters } from '../../../../../ui/public/inspector'; +import { Query } from '../../query'; +import { ExpressionAST } from '../../../../../../plugins/data/common/expressions/types'; + +export { ExpressionAST, TimeRange, Adapters, Filter, Query }; + +export type RenderId = number; +export type Data = any; +export type event = any; +export type Context = object; + +export interface SearchContext { + type: 'kibana_context'; + filters?: Filter[]; + query?: Query; + timeRange?: TimeRange; +} + +export type IGetInitialContext = () => SearchContext | Context; + +export interface IExpressionLoaderParams { + searchContext?: SearchContext; + context?: Context; + variables?: Record; + disableCaching?: boolean; + customFunctions?: []; + customRenderers?: []; +} + +export interface IInterpreterHandlers { + getInitialContext: IGetInitialContext; + inspectorAdapters?: Adapters; +} + +export interface IInterpreterResult { + type: string; + as?: string; + value?: unknown; + error?: unknown; +} + +export interface IInterpreterRenderHandlers { + done: () => void; + onDestroy: (fn: () => void) => void; + reload: () => void; + update: (params: any) => void; + event: (event: event) => void; +} + +export interface IInterpreterRenderFunction { + name: string; + displayName: string; + help: string; + validate: () => void; + reuseDomNode: boolean; + render: (domNode: Element, data: unknown, handlers: IInterpreterRenderHandlers) => void; +} + +export interface IInterpreter { + interpretAst( + ast: ExpressionAST, + context: Context, + handlers: IInterpreterHandlers + ): Promise; +} diff --git a/src/legacy/core_plugins/data/public/expressions/lib/execute.test.ts b/src/legacy/core_plugins/data/public/expressions/lib/execute.test.ts new file mode 100644 index 0000000000000..978d0c834ca8e --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/lib/execute.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { execute, ExpressionDataHandler } from './execute'; +import { fromExpression } from '@kbn/interpreter/common'; +import { ExpressionAST } from '../../../../../../plugins/data/common/expressions/types'; + +jest.mock('../services', () => ({ + getInterpreter: () => { + return { + interpretAst: async (expression: ExpressionAST) => { + return {}; + }, + }; + }, +})); + +describe('execute helper function', () => { + it('returns ExpressionDataHandler instance', () => { + const response = execute(''); + expect(response).toBeInstanceOf(ExpressionDataHandler); + }); +}); + +describe('ExpressionDataHandler', () => { + const expressionString = ''; + + describe('constructor', () => { + it('accepts expression string', () => { + const expressionDataHandler = new ExpressionDataHandler(expressionString, {}); + expect(expressionDataHandler.getExpression()).toEqual(expressionString); + }); + + it('accepts expression AST', () => { + const expressionAST = fromExpression(expressionString) as ExpressionAST; + const expressionDataHandler = new ExpressionDataHandler(expressionAST, {}); + expect(expressionDataHandler.getExpression()).toEqual(expressionString); + expect(expressionDataHandler.getAst()).toEqual(expressionAST); + }); + + it('allows passing in context', () => { + const expressionDataHandler = new ExpressionDataHandler(expressionString, { + context: { test: 'hello' }, + }); + expect(expressionDataHandler.getExpression()).toEqual(expressionString); + }); + + it('allows passing in search context', () => { + const expressionDataHandler = new ExpressionDataHandler(expressionString, { + searchContext: { type: 'kibana_context', filters: [] }, + }); + expect(expressionDataHandler.getExpression()).toEqual(expressionString); + }); + }); + + describe('getData()', () => { + it('returns a promise', () => { + const expressionDataHandler = new ExpressionDataHandler(expressionString, {}); + expect(expressionDataHandler.getData()).toBeInstanceOf(Promise); + }); + + it('promise resolves with data', async () => { + const expressionDataHandler = new ExpressionDataHandler(expressionString, {}); + expect(await expressionDataHandler.getData()).toEqual({}); + }); + }); + + it('cancel() aborts request', () => { + const expressionDataHandler = new ExpressionDataHandler(expressionString, {}); + expressionDataHandler.cancel(); + }); + + it('inspect() returns correct inspector adapters', () => { + const expressionDataHandler = new ExpressionDataHandler(expressionString, {}); + expect(expressionDataHandler.inspect()).toHaveProperty('requests'); + expect(expressionDataHandler.inspect()).toHaveProperty('data'); + }); +}); diff --git a/src/legacy/core_plugins/data/public/expressions/lib/execute.ts b/src/legacy/core_plugins/data/public/expressions/lib/execute.ts new file mode 100644 index 0000000000000..4cc1cead3e03c --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/lib/execute.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 { fromExpression } from '@kbn/interpreter/target/common'; +import { DataAdapter, RequestAdapter, Adapters } from '../../../../../../plugins/inspector/public'; +import { getInterpreter } from '../services'; +import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './_types'; + +/** + * The search context describes a specific context (filters, time range and query) + * that will be applied to the expression for execution. Not every expression will + * be effected by that. You have to use special functions + * that will pick up this search context and forward it to following functions that + * understand it. + */ + +export class ExpressionDataHandler { + private abortController: AbortController; + private expression: string; + private ast: ExpressionAST; + + private inspectorAdapters: Adapters; + private promise: Promise; + + 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.abortController = new AbortController(); + this.inspectorAdapters = this.getActiveInspectorAdapters(); + + const getInitialContext = () => ({ + type: 'kibana_context', + ...params.searchContext, + }); + + const defaultContext = { type: 'null' }; + + const interpreter = getInterpreter(); + this.promise = interpreter.interpretAst(this.ast, params.context || defaultContext, { + getInitialContext, + inspectorAdapters: this.inspectorAdapters, + }); + } + + cancel = () => { + this.abortController.abort(); + }; + + getData = async () => { + return await this.promise; + }; + + getExpression = () => { + return this.expression; + }; + + getAst = () => { + return this.ast; + }; + + inspect = () => { + return this.inspectorAdapters; + }; + + /** + * 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 = {}; + + // 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. + 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. + adapters.data = new DataAdapter(); + + return adapters; + }; +} + +export function execute( + expression: string | ExpressionAST, + params: IExpressionLoaderParams = {} +): ExpressionDataHandler { + return new ExpressionDataHandler(expression, params); +} diff --git a/src/legacy/core_plugins/data/public/expressions/lib/loader.test.ts b/src/legacy/core_plugins/data/public/expressions/lib/loader.test.ts new file mode 100644 index 0000000000000..ee9efd9d3373b --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/lib/loader.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { first } from 'rxjs/operators'; +import { loader, ExpressionLoader } from './loader'; +import { fromExpression } from '@kbn/interpreter/common'; +import { IInterpreterRenderHandlers } from './_types'; +import { Observable } from 'rxjs'; +import { ExpressionAST } from '../../../../../../plugins/data/common/expressions/types'; + +const element: HTMLElement = null as any; + +jest.mock('../services', () => ({ + getInterpreter: () => { + return { + interpretAst: async (expression: ExpressionAST) => { + return { type: 'render', as: 'test' }; + }, + }; + }, +})); + +jest.mock('../../../../interpreter/public/registries', () => { + const _registry: Record = {}; + _registry.test = { + render: (el: HTMLElement, value: any, handlers: IInterpreterRenderHandlers) => { + handlers.done(); + }, + }; + return { + renderersRegistry: { + get: (id: string) => { + return _registry[id]; + }, + }, + }; +}); + +describe('execute helper function', () => { + it('returns ExpressionDataHandler instance', () => { + const response = loader(element, '', {}); + expect(response).toBeInstanceOf(ExpressionLoader); + }); +}); + +describe('ExpressionDataHandler', () => { + const expressionString = ''; + + describe('constructor', () => { + it('accepts expression string', () => { + const expressionDataHandler = new ExpressionLoader(element, expressionString, {}); + expect(expressionDataHandler.getExpression()).toEqual(expressionString); + }); + + it('accepts expression AST', () => { + const expressionAST = fromExpression(expressionString) as ExpressionAST; + const expressionDataHandler = new ExpressionLoader(element, expressionAST, {}); + expect(expressionDataHandler.getExpression()).toEqual(expressionString); + expect(expressionDataHandler.getAst()).toEqual(expressionAST); + }); + + it('creates observables', () => { + const expressionLoader = new ExpressionLoader(element, expressionString, {}); + expect(expressionLoader.events$).toBeInstanceOf(Observable); + expect(expressionLoader.render$).toBeInstanceOf(Observable); + expect(expressionLoader.update$).toBeInstanceOf(Observable); + expect(expressionLoader.data$).toBeInstanceOf(Observable); + }); + }); + + it('emits on $data when data is available', async () => { + const expressionLoader = new ExpressionLoader(element, expressionString, {}); + const response = await expressionLoader.data$.pipe(first()).toPromise(); + expect(response).toEqual({ type: 'render', as: 'test' }); + }); + + it('emits on render$ when rendering is done', async () => { + const expressionLoader = new ExpressionLoader(element, expressionString, {}); + const response = await expressionLoader.render$.pipe(first()).toPromise(); + expect(response).toBe(1); + }); + + it('allows updating configuration', async () => { + const expressionLoader = new ExpressionLoader(element, expressionString, {}); + let response = await expressionLoader.render$.pipe(first()).toPromise(); + expect(response).toBe(1); + expressionLoader.update('', {}); + response = await expressionLoader.render$.pipe(first()).toPromise(); + expect(response).toBe(2); + }); + + it('cancel() aborts request', () => { + const expressionDataHandler = new ExpressionLoader(element, expressionString, {}); + expressionDataHandler.cancel(); + }); + + it('inspect() returns correct inspector adapters', () => { + const expressionDataHandler = new ExpressionLoader(element, expressionString, {}); + expect(expressionDataHandler.inspect()).toHaveProperty('data'); + expect(expressionDataHandler.inspect()).toHaveProperty('requests'); + }); +}); diff --git a/src/legacy/core_plugins/data/public/expressions/lib/loader.ts b/src/legacy/core_plugins/data/public/expressions/lib/loader.ts new file mode 100644 index 0000000000000..e3353ab676be6 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/lib/loader.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 { Observable, Subject } from 'rxjs'; +import { first, share } from 'rxjs/operators'; +import { Adapters, InspectorSession } from '../../../../../../plugins/inspector/public'; +import { execute, ExpressionDataHandler } from './execute'; +import { ExpressionRenderHandler } from './render'; +import { RenderId, Data, IExpressionLoaderParams, ExpressionAST } from './_types'; +import { getInspector } from '../services'; + +export class ExpressionLoader { + data$: Observable; + update$: Observable; + render$: Observable; + events$: Observable; + + private dataHandler: ExpressionDataHandler; + private renderHandler: ExpressionRenderHandler; + private dataSubject: Subject; + private data: Data; + + constructor( + element: HTMLElement, + expression: string | ExpressionAST, + params: IExpressionLoaderParams + ) { + this.dataSubject = new Subject(); + this.data$ = this.dataSubject.asObservable().pipe(share()); + + this.renderHandler = new ExpressionRenderHandler(element); + this.render$ = this.renderHandler.render$; + this.update$ = this.renderHandler.update$; + this.events$ = this.renderHandler.events$; + + this.update$.subscribe(({ newExpression, newParams }) => { + this.update(newExpression, newParams); + }); + + this.data$.subscribe(data => { + this.render(data); + }); + + this.execute(expression, params); + // @ts-ignore + this.dataHandler = this.dataHandler; + } + + destroy() {} + + cancel() { + this.dataHandler.cancel(); + } + + getExpression(): string { + return this.dataHandler.getExpression(); + } + + getAst(): ExpressionAST { + return this.dataHandler.getAst(); + } + + getElement(): HTMLElement { + return this.renderHandler.getElement(); + } + + openInspector(title: string): InspectorSession { + return getInspector().open(this.inspect(), { + title, + }); + } + + inspect(): Adapters { + return this.dataHandler.inspect(); + } + + update(expression: string | ExpressionAST, params: IExpressionLoaderParams): Promise { + const promise = this.render$.pipe(first()).toPromise(); + + if (expression !== null) { + this.execute(expression, params); + } else { + this.render(this.data); + } + return promise; + } + + private execute = async ( + expression: string | ExpressionAST, + params: IExpressionLoaderParams + ): Promise => { + if (this.dataHandler) { + this.dataHandler.cancel(); + } + this.dataHandler = execute(expression, params); + const data = await this.dataHandler.getData(); + this.dataSubject.next(data); + return data; + }; + + private async render(data: Data): Promise { + return this.renderHandler.render(data); + } +} + +export type IExpressionLoader = ( + element: HTMLElement, + expression: string | ExpressionAST, + params: IExpressionLoaderParams +) => ExpressionLoader; + +export const loader: IExpressionLoader = (element, expression, params) => { + return new ExpressionLoader(element, expression, params); +}; diff --git a/src/legacy/core_plugins/data/public/expressions/lib/render.test.ts b/src/legacy/core_plugins/data/public/expressions/lib/render.test.ts new file mode 100644 index 0000000000000..1bb2f8d6554b2 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/lib/render.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { render, ExpressionRenderHandler } from './render'; +import { Observable } from 'rxjs'; +import { IInterpreterRenderHandlers } from './_types'; + +const element: HTMLElement = null as any; + +jest.mock('../../../../interpreter/public/registries', () => { + const _registry: Record = {}; + _registry.test = { + render: (el: HTMLElement, value: any, handlers: IInterpreterRenderHandlers) => { + handlers.done(); + }, + }; + return { + renderersRegistry: { + get: (id: string) => { + return _registry[id]; + }, + }, + }; +}); + +describe('render helper function', () => { + it('returns ExpressionRenderHandler instance', () => { + const response = render(element, {}); + expect(response).toBeInstanceOf(ExpressionRenderHandler); + }); +}); + +describe('ExpressionRenderHandler', () => { + const data = { type: 'render', as: 'test' }; + + it('constructor creates observers', () => { + const expressionRenderHandler = new ExpressionRenderHandler(element); + expect(expressionRenderHandler.events$).toBeInstanceOf(Observable); + expect(expressionRenderHandler.render$).toBeInstanceOf(Observable); + expect(expressionRenderHandler.update$).toBeInstanceOf(Observable); + }); + + it('getElement returns the element', () => { + const expressionRenderHandler = new ExpressionRenderHandler(element); + expect(expressionRenderHandler.getElement()).toBe(element); + }); + + describe('render()', () => { + it('throws if invalid data is provided', async () => { + const expressionRenderHandler = new ExpressionRenderHandler(element); + await expect(expressionRenderHandler.render({})).rejects.toThrow(); + }); + + it('throws if renderer does not exist', async () => { + const expressionRenderHandler = new ExpressionRenderHandler(element); + await expect( + expressionRenderHandler.render({ type: 'render', as: 'something' }) + ).rejects.toThrow(); + }); + + it('returns a promise', () => { + const expressionRenderHandler = new ExpressionRenderHandler(element); + expect(expressionRenderHandler.render(data)).toBeInstanceOf(Promise); + }); + + it('resolves a promise once rendering is complete', async () => { + const expressionRenderHandler = new ExpressionRenderHandler(element); + const response = await expressionRenderHandler.render(data); + expect(response).toBe(1); + }); + }); +}); diff --git a/src/legacy/core_plugins/data/public/expressions/lib/render.ts b/src/legacy/core_plugins/data/public/expressions/lib/render.ts new file mode 100644 index 0000000000000..250fa14c9de1d --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/lib/render.ts @@ -0,0 +1,99 @@ +/* + * 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 { Observable } from 'rxjs'; +import * as Rx from 'rxjs'; +import { share, first } from 'rxjs/operators'; +import { renderersRegistry } from '../../../../interpreter/public/registries'; +import { event, RenderId, Data, IInterpreterRenderHandlers } from './_types'; + +export class ExpressionRenderHandler { + render$: Observable; + update$: Observable; + events$: Observable; + + private element: HTMLElement; + private destroyFn?: any; + private renderCount: number = 0; + private handlers: IInterpreterRenderHandlers; + + constructor(element: HTMLElement) { + this.element = element; + + const eventsSubject = new Rx.Subject(); + this.events$ = eventsSubject.asObservable().pipe(share()); + + const renderSubject = new Rx.Subject(); + this.render$ = renderSubject.asObservable().pipe(share()); + + const updateSubject = new Rx.Subject(); + this.update$ = updateSubject.asObservable().pipe(share()); + + this.handlers = { + onDestroy: (fn: any) => { + this.destroyFn = fn; + }, + done: () => { + this.renderCount++; + renderSubject.next(this.renderCount); + }, + reload: () => { + updateSubject.next(null); + }, + update: params => { + updateSubject.next(params); + }, + event: data => { + eventsSubject.next(data); + }, + }; + } + + render = async (data: Data) => { + if (data.type !== 'render' || !data.as) { + throw new Error('invalid data provided to expression renderer'); + } + + if (!renderersRegistry.get(data.as)) { + throw new Error(`invalid renderer id '${data.as}'`); + } + + const promise = this.render$.pipe(first()).toPromise(); + + renderersRegistry.get(data.as).render(this.element, data.value, this.handlers); + + return promise; + }; + + destroy = () => { + if (this.destroyFn) { + this.destroyFn(); + } + }; + + getElement = () => { + return this.element; + }; +} + +export function render(element: HTMLElement, data: Data): ExpressionRenderHandler { + const handler = new ExpressionRenderHandler(element); + handler.render(data); + return handler; +} diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/legend.js b/src/legacy/core_plugins/data/public/expressions/services.ts similarity index 56% rename from src/legacy/core_plugins/metrics/public/visualizations/components/legend.js rename to src/legacy/core_plugins/data/public/expressions/services.ts index 38e3bd3d05921..73f627f8dc886 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/legend.js +++ b/src/legacy/core_plugins/data/public/expressions/services.ts @@ -17,25 +17,25 @@ * under the License. */ -import PropTypes from 'prop-types'; -import React from 'react'; -import { VerticalLegend } from './vertical_legend'; -import { HorizontalLegend } from './horizontal_legend'; +import { IInterpreter } from './lib/_types'; +import { Start as IInspector } from '../../../../../plugins/inspector/public'; -export function Legend(props) { - if (props.legendPosition === 'bottom') { - return ; - } - return ; -} +let interpreter: IInterpreter | undefined; +let inspector: IInspector; -Legend.propTypes = { - legendPosition: PropTypes.string, - onClick: PropTypes.func, - onToggle: PropTypes.func, - series: PropTypes.array, - showLegend: PropTypes.bool, - seriesValues: PropTypes.object, - seriesFilter: PropTypes.array, - tickFormatter: PropTypes.func, +export const getInterpreter = (): IInterpreter => { + if (!interpreter) throw new Error('interpreter was not set'); + return interpreter; +}; + +export const setInterpreter = (inspectorInstance: IInterpreter) => { + interpreter = inspectorInstance; +}; + +export const getInspector = (): IInspector => { + return inspector; +}; + +export const setInspector = (inspectorInstance: IInspector) => { + inspector = inspectorInstance; }; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx index d235cddccf165..4ca1d581aef7d 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx @@ -56,6 +56,9 @@ class FilterBarUI extends Component { }; public render() { + if (!this.props.uiSettings) { + return null; + } const classes = classNames('globalFilterBar', this.props.className); return ( diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 4e1e83d71afe8..8bf13a24f1775 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -18,7 +18,7 @@ */ // /// Define plugin function -import { DataPlugin as Plugin, DataSetup } from './plugin'; +import { DataPlugin as Plugin, DataSetup, DataStart } from './plugin'; export function plugin() { return new Plugin(); @@ -28,7 +28,9 @@ export function plugin() { /** @public types */ export type DataSetup = DataSetup; -export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from './expressions'; +export type DataStart = DataStart; + +export { ExpressionRenderer, ExpressionRendererProps } from './expressions'; export { FilterBar, ApplyFiltersPopover } from './filter'; export { Field, diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.js b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts similarity index 61% rename from src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.js rename to src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts index b7fcb79da3269..d89a4a35e6e9f 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.js +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts @@ -17,18 +17,23 @@ * under the License. */ -import _ from 'lodash'; -import mockLogstashFields from '../../../../../../fixtures/logstash_fields'; -import { stubbedSavedObjectIndexPattern } from '../../../../../../fixtures/stubbed_saved_object_index_pattern'; +import { defaults, pluck, last, get } from 'lodash'; import { IndexedArray } from 'ui/indexed_array'; +import { IndexPattern } from './index_pattern'; + +// @ts-ignore import { DuplicateField } from 'ui/errors'; +// @ts-ignore +import mockLogStashFields from '../../../../../../fixtures/logstash_fields'; +// @ts-ignore -import { IndexPattern } from './index_pattern'; +import { stubbedSavedObjectIndexPattern } from '../../../../../../fixtures/stubbed_saved_object_index_pattern'; +import { Field } from '../index_patterns_service'; jest.mock('ui/registry/field_formats', () => ({ fieldFormats: { getDefaultInstance: jest.fn(), - } + }, })); jest.mock('ui/utils/mapping_setup', () => ({ @@ -36,16 +41,16 @@ jest.mock('ui/utils/mapping_setup', () => ({ id: true, title: true, fieldFormatMap: { - _deserialize: jest.fn().mockImplementation(() => ([])), + _deserialize: jest.fn().mockImplementation(() => []), }, - })) + })), })); jest.mock('ui/notify', () => ({ toastNotifications: { addDanger: jest.fn(), addError: jest.fn(), - } + }, })); jest.mock('ui/saved_objects', () => { @@ -54,32 +59,32 @@ jest.mock('ui/saved_objects', () => { }; }); -let mockFieldsFetcherResponse = []; +let mockFieldsFetcherResponse: any[] = []; + jest.mock('./_fields_fetcher', () => ({ createFieldsFetcher: jest.fn().mockImplementation(() => ({ fetch: jest.fn().mockImplementation(() => { return new Promise(resolve => resolve(mockFieldsFetcherResponse)); }), every: jest.fn(), - })) + })), })); -let object; +let object: any = {}; + const savedObjectsClient = { create: jest.fn(), get: jest.fn().mockImplementation(() => object), update: jest.fn().mockImplementation(async (type, id, body, { version }) => { if (object._version !== version) { - throw { + throw new Object({ res: { - status: 409 - } - }; + status: 409, + }, + }); } - object.attributes.title = body.title; object._version += 'a'; - return { id: object._id, _version: object._version, @@ -96,35 +101,44 @@ const config = { }; const apiClient = { + _getUrl: jest.fn(), getFieldsForTimePattern: jest.fn(), getFieldsForWildcard: jest.fn(), }; // helper function to create index patterns -function create(id, payload) { - const indexPattern = new IndexPattern(id, cfg => config.get(cfg), savedObjectsClient, apiClient, patternCache); +function create(id: string, payload?: any): Promise { + const indexPattern = new IndexPattern( + id, + (cfg: any) => config.get(cfg), + savedObjectsClient as any, + apiClient, + patternCache + ); setDocsourcePayload(id, payload); return indexPattern.init(); } -function setDocsourcePayload(id, providedPayload) { - object = _.defaults(providedPayload || {}, stubbedSavedObjectIndexPattern(id)); +function setDocsourcePayload(id: string | null, providedPayload: any) { + object = defaults(providedPayload || {}, stubbedSavedObjectIndexPattern(id)); } describe('IndexPattern', () => { const indexPatternId = 'test-pattern'; - let indexPattern; + + let indexPattern: IndexPattern; + // create an indexPattern instance for each test - beforeEach(function () { - return create(indexPatternId).then(function (pattern) { + beforeEach(() => { + return create(indexPatternId).then((pattern: IndexPattern) => { indexPattern = pattern; }); }); - describe('api', function () { - it('should have expected properties', function () { + describe('api', () => { + test('should have expected properties', () => { expect(indexPattern).toHaveProperty('refreshFields'); expect(indexPattern).toHaveProperty('popularizeField'); expect(indexPattern).toHaveProperty('getScriptedFields'); @@ -140,16 +154,16 @@ describe('IndexPattern', () => { }); }); - describe('init', function () { - it('should append the found fields', function () { + describe('init', () => { + test('should append the found fields', () => { expect(savedObjectsClient.get).toHaveBeenCalled(); - expect(indexPattern.fields).toHaveLength(mockLogstashFields().length); + expect(indexPattern.fields).toHaveLength(mockLogStashFields().length); expect(indexPattern.fields).toBeInstanceOf(IndexedArray); }); }); - describe('fields', function () { - it('should have expected properties on fields', function () { + describe('fields', () => { + test('should have expected properties on fields', function() { expect(indexPattern.fields[0]).toHaveProperty('displayName'); expect(indexPattern.fields[0]).toHaveProperty('filterable'); expect(indexPattern.fields[0]).toHaveProperty('format'); @@ -158,42 +172,45 @@ describe('IndexPattern', () => { }); }); - describe('getScriptedFields', function () { - it('should return all scripted fields', function () { - const scriptedNames = _(mockLogstashFields()).where({ scripted: true }).pluck('name').value(); - const respNames = _.pluck(indexPattern.getScriptedFields(), 'name'); + describe('getScriptedFields', () => { + test('should return all scripted fields', () => { + const scriptedNames = mockLogStashFields() + .filter((item: Field) => item.scripted === true) + .map((item: Field) => item.name); + const respNames = pluck(indexPattern.getScriptedFields(), 'name'); + expect(respNames).toEqual(scriptedNames); }); }); - describe('getNonScriptedFields', function () { - it('should return all non-scripted fields', function () { - const notScriptedNames = _(mockLogstashFields()).where({ scripted: false }).pluck('name').value(); - const respNames = _.pluck(indexPattern.getNonScriptedFields(), 'name'); + describe('getNonScriptedFields', () => { + test('should return all non-scripted fields', () => { + const notScriptedNames = mockLogStashFields() + .filter((item: Field) => item.scripted === false) + .map((item: Field) => item.name); + const respNames = pluck(indexPattern.getNonScriptedFields(), 'name'); + expect(respNames).toEqual(notScriptedNames); }); - }); - describe('refresh fields', function () { - it('should fetch fields from the fieldsFetcher', async function () { + describe('refresh fields', () => { + test('should fetch fields from the fieldsFetcher', async () => { expect(indexPattern.fields.length).toBeGreaterThan(2); - mockFieldsFetcherResponse = [ - { name: 'foo' }, - { name: 'bar' } - ]; + mockFieldsFetcherResponse = [{ name: 'foo' }, { name: 'bar' }]; await indexPattern.refreshFields(); mockFieldsFetcherResponse = []; const newFields = indexPattern.getNonScriptedFields(); + expect(newFields).toHaveLength(2); expect(newFields.map(f => f.name)).toEqual(['foo', 'bar']); }); - it('should preserve the scripted fields', async function () { + test('should preserve the scripted fields', async () => { // add spy to indexPattern.getScriptedFields // sinon.spy(indexPattern, 'getScriptedFields'); @@ -202,13 +219,16 @@ describe('IndexPattern', () => { // called to append scripted fields to the response from mapper.getFieldsForIndexPattern // sinon.assert.calledOnce(indexPattern.getScriptedFields); - expect(indexPattern.getScriptedFields().map(f => f.name)) - .toEqual(mockLogstashFields().filter(f => f.scripted).map(f => f.name)); + expect(indexPattern.getScriptedFields().map(f => f.name)).toEqual( + mockLogStashFields() + .filter((f: Field) => f.scripted) + .map((f: Field) => f.name) + ); }); }); - describe('add and remove scripted fields', function () { - it('should append the scripted field', function () { + describe('add and remove scripted fields', () => { + test('should append the scripted field', async () => { // keep a copy of the current scripted field count // const saveSpy = sinon.spy(indexPattern, 'save'); const oldCount = indexPattern.getScriptedFields().length; @@ -217,107 +237,128 @@ describe('IndexPattern', () => { const scriptedField = { name: 'new scripted field', script: 'false', - type: 'boolean' + type: 'boolean', }; - indexPattern.addScriptedField(scriptedField.name, scriptedField.script, scriptedField.type); + + await indexPattern.addScriptedField( + scriptedField.name, + scriptedField.script, + scriptedField.type, + 'lang' + ); + const scriptedFields = indexPattern.getScriptedFields(); // expect(saveSpy.callCount).to.equal(1); expect(scriptedFields).toHaveLength(oldCount + 1); expect(indexPattern.fields.byName[scriptedField.name].name).toEqual(scriptedField.name); }); - it('should remove scripted field, by name', function () { + test('should remove scripted field, by name', async () => { // const saveSpy = sinon.spy(indexPattern, 'save'); const scriptedFields = indexPattern.getScriptedFields(); const oldCount = scriptedFields.length; - const scriptedField = _.last(scriptedFields); + const scriptedField = last(scriptedFields); - indexPattern.removeScriptedField(scriptedField.name); + await indexPattern.removeScriptedField(scriptedField.name); // expect(saveSpy.callCount).to.equal(1); expect(indexPattern.getScriptedFields().length).toEqual(oldCount - 1); expect(indexPattern.fields.byName[scriptedField.name]).toEqual(undefined); }); - it('should not allow duplicate names', async () => { + test('should not allow duplicate names', async () => { const scriptedFields = indexPattern.getScriptedFields(); - const scriptedField = _.last(scriptedFields); + const scriptedField = last(scriptedFields); expect.assertions(1); try { - await indexPattern.addScriptedField(scriptedField.name, '\'new script\'', 'string'); + await indexPattern.addScriptedField(scriptedField.name, "'new script'", 'string', 'lang'); } catch (e) { expect(e).toBeInstanceOf(DuplicateField); } }); }); - describe('popularizeField', function () { - it('should increment the popularity count by default', function () { + describe('popularizeField', () => { + test('should increment the popularity count by default', () => { // const saveSpy = sinon.stub(indexPattern, 'save'); - indexPattern.fields.forEach(function (field) { - const oldCount = field.count; + indexPattern.fields.forEach(async field => { + const oldCount = field.count || 0; - indexPattern.popularizeField(field.name); + await indexPattern.popularizeField(field.name); // expect(saveSpy.callCount).to.equal(i + 1); expect(field.count).toEqual(oldCount + 1); }); }); - it('should increment the popularity count', function () { + test('should increment the popularity count', () => { // const saveSpy = sinon.stub(indexPattern, 'save'); - indexPattern.fields.forEach(function (field) { - const oldCount = field.count; + indexPattern.fields.forEach(async field => { + const oldCount = field.count || 0; const incrementAmount = 4; - indexPattern.popularizeField(field.name, incrementAmount); + await indexPattern.popularizeField(field.name, incrementAmount); // expect(saveSpy.callCount).to.equal(i + 1); expect(field.count).toEqual(oldCount + incrementAmount); }); }); - it('should decrement the popularity count', function () { - indexPattern.fields.forEach(function (field) { - const oldCount = field.count; + test('should decrement the popularity count', () => { + indexPattern.fields.forEach(async field => { + const oldCount = field.count || 0; const incrementAmount = 4; const decrementAmount = -2; - indexPattern.popularizeField(field.name, incrementAmount); - indexPattern.popularizeField(field.name, decrementAmount); + await indexPattern.popularizeField(field.name, incrementAmount); + await indexPattern.popularizeField(field.name, decrementAmount); expect(field.count).toEqual(oldCount + incrementAmount + decrementAmount); }); }); - it('should not go below 0', function () { - indexPattern.fields.forEach(function (field) { + test('should not go below 0', () => { + indexPattern.fields.forEach(async field => { const decrementAmount = -Number.MAX_VALUE; - indexPattern.popularizeField(field.name, decrementAmount); + + await indexPattern.popularizeField(field.name, decrementAmount); + expect(field.count).toEqual(0); }); }); }); - it('should handle version conflicts', async () => { + test('should handle version conflicts', async () => { setDocsourcePayload(null, { - _version: 'foo', _id: 'foo', + _version: 'foo', attributes: { - title: 'something' - } + title: 'something', + }, }); // Create a normal index pattern - const pattern = new IndexPattern('foo', cfg => config.get(cfg), savedObjectsClient, apiClient, patternCache); + const pattern = new IndexPattern( + 'foo', + (cfg: any) => config.get(cfg), + savedObjectsClient as any, + apiClient, + patternCache + ); await pattern.init(); - expect(pattern.version).toBe('fooa'); + expect(get(pattern, 'version')).toBe('fooa'); // Create the same one - we're going to handle concurrency - const samePattern = new IndexPattern('foo', cfg => config.get(cfg), savedObjectsClient, apiClient, patternCache); + const samePattern = new IndexPattern( + 'foo', + (cfg: any) => config.get(cfg), + savedObjectsClient as any, + apiClient, + patternCache + ); await samePattern.init(); - expect(samePattern.version).toBe('fooaa'); + expect(get(samePattern, 'version')).toBe('fooaa'); // This will conflict because samePattern did a save (from refreshFields) // but the resave should work fine diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.js b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts similarity index 68% rename from src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.js rename to src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index 039910e8d3b23..8fc5c89e5d17c 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.js +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -17,66 +17,65 @@ * under the License. */ +// eslint-disable-next-line max-classes-per-file import { IndexPatterns } from './index_patterns'; +import { SavedObjectsClientContract, UiSettingsClientContract } from 'kibana/public'; jest.mock('../errors', () => ({ IndexPatternMissingIndices: jest.fn(), })); - jest.mock('ui/registry/field_formats', () => ({ fieldFormats: { getDefaultInstance: jest.fn(), - } + }, })); jest.mock('ui/notify', () => ({ toastNotifications: { addDanger: jest.fn(), - } + }, })); jest.mock('./index_pattern', () => { class IndexPattern { init = async () => { return this; - } + }; } return { - IndexPattern + IndexPattern, }; }); jest.mock('./index_patterns_api_client', () => { class IndexPatternsApiClient { - getFieldsForWildcard = async () => ({}) + getFieldsForWildcard = async () => ({}); } return { - IndexPatternsApiClient + IndexPatternsApiClient, }; }); -const savedObjectsClient = { - create: jest.fn(), - get: jest.fn(), - update: jest.fn() -}; - -const config = { - get: jest.fn(), -}; +describe('IndexPatterns', () => { + let indexPatterns: IndexPatterns; + beforeEach(() => { + const savedObjectsClient = {} as SavedObjectsClientContract; + const uiSettings = {} as UiSettingsClientContract; -describe('IndexPatterns', () => { - const indexPatterns = new IndexPatterns(config, savedObjectsClient); + indexPatterns = new IndexPatterns(uiSettings, savedObjectsClient); + }); - it('does not cache gets without an id', function () { + test('does not cache gets without an id', () => { expect(indexPatterns.get()).not.toBe(indexPatterns.get()); }); - it('does cache gets for the same id', function () { - expect(indexPatterns.get(1)).toBe(indexPatterns.get(1)); + test('does cache gets for the same id', () => { + const id = '1'; + + expect(indexPatterns.get(id)).toBe(indexPatterns.get(id)); }); }); diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.js b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.js deleted file mode 100644 index bea4c6c489c9b..0000000000000 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.js +++ /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 { http } from './index_patterns_api_client.test.mock'; -import { IndexPatternsApiClient } from './index_patterns_api_client'; - -describe('IndexPatternsApiClient', () => { - it('uses the right URI to fetch fields for time patterns', async function () { - const fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => ({})); - const indexPatternsApiClient = new IndexPatternsApiClient(); - await indexPatternsApiClient.getFieldsForTimePattern(); - - expect(fetchSpy).toHaveBeenCalledWith('/api/index_patterns/_fields_for_time_pattern', { - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, - method: 'GET', - prependBasePath: true, - query: {}, - }); - }); - - it('uses the right URI to fetch fields for wildcard', async function () { - const fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => ({})); - const indexPatternsApiClient = new IndexPatternsApiClient(); - await indexPatternsApiClient.getFieldsForWildcard(); - - expect(fetchSpy).toHaveBeenCalledWith('/api/index_patterns/_fields_for_wildcard', { - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, - method: 'GET', - prependBasePath: true, - query: {}, - }); - }); - - it('uses the right URI to fetch fields for wildcard given a type', async function () { - const fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => ({})); - const indexPatternsApiClient = new IndexPatternsApiClient(); - await indexPatternsApiClient.getFieldsForWildcard({ type: 'rollup' }); - - expect(fetchSpy).toHaveBeenCalledWith('/api/index_patterns/rollup/_fields_for_wildcard', { - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, - method: 'GET', - prependBasePath: true, - query: {}, - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.js b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts similarity index 99% rename from src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.js rename to src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts index a793fa6a25bea..06933dc409052 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.js +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts @@ -17,7 +17,6 @@ * under the License. */ - import { setup } from '../../../../../../test_utils/public/http_test_setup'; export const { http } = setup(injectedMetadata => { diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts new file mode 100644 index 0000000000000..944108e1501f1 --- /dev/null +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.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 { http } from './index_patterns_api_client.test.mock'; +import { IndexPatternsApiClient } from './index_patterns_api_client'; + +const requestData = { + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + method: 'GET', + prependBasePath: true, + query: {}, +}; + +describe('IndexPatternsApiClient', () => { + let fetchSpy: jest.Mock; + let indexPatternsApiClient: IndexPatternsApiClient; + + beforeEach(() => { + fetchSpy = jest.spyOn(http, 'fetch').mockImplementation(() => Promise.resolve({})); + indexPatternsApiClient = new IndexPatternsApiClient(); + }); + + test('uses the right URI to fetch fields for time patterns', async function() { + const expectedPath = '/api/index_patterns/_fields_for_time_pattern'; + + await indexPatternsApiClient.getFieldsForTimePattern(); + + expect(fetchSpy).toHaveBeenCalledWith(expectedPath, requestData); + }); + + test('uses the right URI to fetch fields for wildcard', async function() { + const expectedPath = '/api/index_patterns/_fields_for_wildcard'; + + await indexPatternsApiClient.getFieldsForWildcard(); + + expect(fetchSpy).toHaveBeenCalledWith(expectedPath, requestData); + }); + + test('uses the right URI to fetch fields for wildcard given a type', async function() { + const expectedPath = '/api/index_patterns/rollup/_fields_for_wildcard'; + + await indexPatternsApiClient.getFieldsForWildcard({ type: 'rollup' }); + + expect(fetchSpy).toHaveBeenCalledWith(expectedPath, requestData); + }); +}); diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts index 14350a6284191..3d86a200eab9b 100644 --- a/src/legacy/core_plugins/data/public/legacy.ts +++ b/src/legacy/core_plugins/data/public/legacy.ts @@ -34,9 +34,7 @@ * data that will eventually be injected by the new platform. */ -import { npSetup } from 'ui/new_platform'; -// @ts-ignore -import { renderersRegistry } from 'plugins/interpreter/registries'; +import { npSetup, npStart } from 'ui/new_platform'; // @ts-ignore import { getInterpreter } from 'plugins/interpreter/interpreter'; import { LegacyDependenciesPlugin } from './shim/legacy_dependencies_plugin'; @@ -47,8 +45,9 @@ const legacyPlugin = new LegacyDependenciesPlugin(); export const setup = dataPlugin.setup(npSetup.core, { __LEGACY: legacyPlugin.setup(), - interpreter: { - renderersRegistry, - getInterpreter, - }, + inspector: npSetup.plugins.inspector, +}); + +export const start = dataPlugin.start(npStart.core, { + inspector: npStart.plugins.inspector, }); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index a38e55e8139ed..b62c8106bc168 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -18,12 +18,16 @@ */ import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { ExpressionsService, ExpressionsSetup } from './expressions'; +import { ExpressionsService, ExpressionsSetup, ExpressionsStart } from './expressions'; import { SearchService, SearchSetup } from './search'; import { QueryService, QuerySetup } from './query'; import { FilterService, FilterSetup } from './filter'; import { IndexPatternsService, IndexPatternsSetup } from './index_patterns'; import { LegacyDependenciesPluginSetup } from './shim/legacy_dependencies_plugin'; +import { + Start as InspectorStart, + Setup as InspectorSetup, +} from '../../../../plugins/inspector/public'; /** * Interface for any dependencies on other plugins' `setup` contracts. @@ -32,7 +36,11 @@ import { LegacyDependenciesPluginSetup } from './shim/legacy_dependencies_plugin */ export interface DataPluginSetupDependencies { __LEGACY: LegacyDependenciesPluginSetup; - interpreter: any; + inspector: InspectorSetup; +} + +export interface DataPluginStartDependencies { + inspector: InspectorStart; } /** @@ -48,6 +56,10 @@ export interface DataSetup { search: SearchSetup; } +export interface DataStart { + expressions: ExpressionsStart; +} + /** * Data Plugin - public * @@ -59,7 +71,7 @@ export interface DataSetup { * in the setup/start interfaces. The remaining items exported here are either types, * or static code. */ -export class DataPlugin implements Plugin { +export class DataPlugin implements Plugin { // Exposed services, sorted alphabetically private readonly expressions: ExpressionsService = new ExpressionsService(); private readonly filter: FilterService = new FilterService(); @@ -67,7 +79,7 @@ export class DataPlugin implements Plugin = ({ } const saveQueryForm = ( - + {savedQueryDescriptionText} diff --git a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx index 8d98e4d2f9b7a..3fbfec364fad9 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/index.tsx @@ -18,7 +18,7 @@ */ import { Filter } from '@kbn/es-query'; -import { RefreshInterval, TimeRange } from 'ui/timefilter'; +import { RefreshInterval, TimeRange } from 'src/plugins/data/public'; import { Query } from '../../query/query_bar'; export * from './components'; diff --git a/src/legacy/ui/public/timefilter/get_time.test.ts b/src/legacy/core_plugins/data/public/timefilter/get_time.test.ts similarity index 100% rename from src/legacy/ui/public/timefilter/get_time.test.ts rename to src/legacy/core_plugins/data/public/timefilter/get_time.test.ts diff --git a/src/legacy/ui/public/timefilter/get_time.ts b/src/legacy/core_plugins/data/public/timefilter/get_time.ts similarity index 100% rename from src/legacy/ui/public/timefilter/get_time.ts rename to src/legacy/core_plugins/data/public/timefilter/get_time.ts diff --git a/src/legacy/core_plugins/data/public/timefilter/index.ts b/src/legacy/core_plugins/data/public/timefilter/index.ts new file mode 100644 index 0000000000000..da57bca126b0f --- /dev/null +++ b/src/legacy/core_plugins/data/public/timefilter/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { Timefilter } from './timefilter'; +export { TimeHistory } from './time_history'; +export { getTime } from './get_time'; diff --git a/src/legacy/ui/public/timefilter/lib/diff_time_picker_vals.test.ts b/src/legacy/core_plugins/data/public/timefilter/lib/diff_time_picker_vals.test.ts similarity index 100% rename from src/legacy/ui/public/timefilter/lib/diff_time_picker_vals.test.ts rename to src/legacy/core_plugins/data/public/timefilter/lib/diff_time_picker_vals.test.ts diff --git a/src/legacy/ui/public/timefilter/lib/diff_time_picker_vals.ts b/src/legacy/core_plugins/data/public/timefilter/lib/diff_time_picker_vals.ts similarity index 100% rename from src/legacy/ui/public/timefilter/lib/diff_time_picker_vals.ts rename to src/legacy/core_plugins/data/public/timefilter/lib/diff_time_picker_vals.ts diff --git a/src/legacy/ui/public/timefilter/lib/parse_querystring.ts b/src/legacy/core_plugins/data/public/timefilter/lib/parse_querystring.ts similarity index 100% rename from src/legacy/ui/public/timefilter/lib/parse_querystring.ts rename to src/legacy/core_plugins/data/public/timefilter/lib/parse_querystring.ts diff --git a/src/legacy/ui/public/timefilter/time_history.ts b/src/legacy/core_plugins/data/public/timefilter/time_history.ts similarity index 94% rename from src/legacy/ui/public/timefilter/time_history.ts rename to src/legacy/core_plugins/data/public/timefilter/time_history.ts index 66957ec5987c9..98e759fb27a9d 100644 --- a/src/legacy/ui/public/timefilter/time_history.ts +++ b/src/legacy/core_plugins/data/public/timefilter/time_history.ts @@ -19,7 +19,7 @@ import moment from 'moment'; import { TimeRange } from 'src/plugins/data/public'; -import { PersistedLog } from '../persisted_log'; +import { PersistedLog } from 'ui/persisted_log'; export class TimeHistory { private history: PersistedLog; @@ -52,5 +52,3 @@ export class TimeHistory { return this.history.get(); } } - -export const timeHistory = new TimeHistory(); diff --git a/src/legacy/ui/public/timefilter/timefilter.test.mocks.ts b/src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts similarity index 93% rename from src/legacy/ui/public/timefilter/timefilter.test.mocks.ts rename to src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts index f6d50167e45db..7354916c3fc35 100644 --- a/src/legacy/ui/public/timefilter/timefilter.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/timefilter/timefilter.test.mocks.ts @@ -17,7 +17,7 @@ * under the License. */ -import { chromeServiceMock } from '../../../../core/public/mocks'; +import { chromeServiceMock } from '../../../../../core/public/mocks'; jest.doMock('ui/new_platform', () => ({ npStart: { diff --git a/src/legacy/ui/public/timefilter/timefilter.test.ts b/src/legacy/core_plugins/data/public/timefilter/timefilter.test.ts similarity index 91% rename from src/legacy/ui/public/timefilter/timefilter.test.ts rename to src/legacy/core_plugins/data/public/timefilter/timefilter.test.ts index f8885d842ef69..47896189aa080 100644 --- a/src/legacy/ui/public/timefilter/timefilter.test.ts +++ b/src/legacy/core_plugins/data/public/timefilter/timefilter.test.ts @@ -19,46 +19,38 @@ import './timefilter.test.mocks'; -jest.mock( - 'ui/chrome', - () => ({ - getBasePath: () => `/some/base/path`, - getUiSettingsClient: () => { - return { - get: (key: string) => { - switch (key) { - case 'timepicker:timeDefaults': - return { from: 'now-15m', to: 'now' }; - case 'timepicker:refreshIntervalDefaults': - return { pause: false, value: 0 }; - default: - throw new Error(`Unexpected config key: ${key}`); - } - }, - }; - }, - }), - { virtual: true } -); - -jest.mock( - 'ui/timefilter/lib/parse_querystring', - () => ({ - parseQueryString: () => { - return { - // Can not access local variable from within a mock - // @ts-ignore - forceNow: global.nowTime, - }; - }, - }), - { virtual: true } -); +jest.mock('ui/chrome', () => ({ + getBasePath: () => `/some/base/path`, + getUiSettingsClient: () => { + return { + get: (key: string) => { + switch (key) { + case 'timepicker:timeDefaults': + return { from: 'now-15m', to: 'now' }; + case 'timepicker:refreshIntervalDefaults': + return { pause: false, value: 0 }; + default: + throw new Error(`Unexpected config key: ${key}`); + } + }, + }; + }, +})); + +jest.mock('./lib/parse_querystring', () => ({ + parseQueryString: () => { + return { + // Can not access local variable from within a mock + // @ts-ignore + forceNow: global.nowTime, + }; + }, +})); import sinon from 'sinon'; import expect from '@kbn/expect'; import moment from 'moment'; -import { timefilter } from './timefilter'; +import { timefilter } from 'ui/timefilter'; import { Subscription } from 'rxjs'; import { TimeRange, RefreshInterval } from 'src/plugins/data/public'; diff --git a/src/legacy/ui/public/timefilter/timefilter.ts b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts similarity index 69% rename from src/legacy/ui/public/timefilter/timefilter.ts rename to src/legacy/core_plugins/data/public/timefilter/timefilter.ts index 729575a833e37..b07060018f9d7 100644 --- a/src/legacy/ui/public/timefilter/timefilter.ts +++ b/src/legacy/core_plugins/data/public/timefilter/timefilter.ts @@ -20,18 +20,18 @@ import _ from 'lodash'; import { Subject, BehaviorSubject } from 'rxjs'; import moment, { Moment } from 'moment'; -import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; -import chrome from 'ui/chrome'; -import { UiSettingsClientContract } from 'src/core/public'; import { RefreshInterval, TimeRange } from 'src/plugins/data/public'; import { IndexPattern } from 'src/legacy/core_plugins/data/public'; -import { IScope } from 'angular'; -import { timeHistory } from './time_history'; +import { TimeHistory } from './time_history'; import { areRefreshIntervalsDifferent, areTimeRangesDifferent } from './lib/diff_time_picker_vals'; -import uiRoutes from '../routes'; import { parseQueryString } from './lib/parse_querystring'; import { calculateBounds, getTime } from './get_time'; +export interface TimefilterConfig { + timeDefaults: TimeRange; + refreshIntervalDefaults: RefreshInterval; +} + // Timefilter accepts moment input but always returns string output export type InputTimeRange = | TimeRange @@ -53,13 +53,15 @@ export class Timefilter { private _time: TimeRange; private _refreshInterval!: RefreshInterval; + private _history: TimeHistory; public isTimeRangeSelectorEnabled: boolean = false; public isAutoRefreshSelectorEnabled: boolean = false; - constructor(uiSettings: UiSettingsClientContract) { - this._time = uiSettings.get('timepicker:timeDefaults'); - this.setRefreshInterval(uiSettings.get('timepicker:refreshIntervalDefaults')); + constructor(config: TimefilterConfig, timeHistory: TimeHistory) { + this._history = timeHistory; + this._time = config.timeDefaults; + this.setRefreshInterval(config.refreshIntervalDefaults); } getEnabledUpdated$ = () => { @@ -106,7 +108,7 @@ export class Timefilter { from: newTime.from, to: newTime.to, }; - timeHistory.add(this._time); + this._history.add(this._time); this.timeUpdate$.next(); this.fetch$.next(); } @@ -221,64 +223,3 @@ export class Timefilter { this.autoRefreshFetch$.next(); }; } - -export const timefilter = new Timefilter(chrome.getUiSettingsClient()); - -// TODO -// remove everything underneath once globalState is no longer an angular service -// and listener can be registered without angular. -function convertISO8601(stringTime: string): string { - const obj = moment(stringTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); - return obj.isValid() ? obj.toString() : stringTime; -} - -// Currently some parts of Kibana (index patterns, timefilter) rely on addSetupWork in the uiRouter -// and require it to be executed to properly function. -// This function is exposed for applications that do not use uiRoutes like APM -// Kibana issue https://github.com/elastic/kibana/issues/19110 tracks the removal of this dependency on uiRouter -export const registerTimefilterWithGlobalState = _.once((globalState: any, $rootScope: IScope) => { - const uiSettings = chrome.getUiSettingsClient(); - const timeDefaults = uiSettings.get('timepicker:timeDefaults'); - const refreshIntervalDefaults = uiSettings.get('timepicker:refreshIntervalDefaults'); - - timefilter.setTime(_.defaults(globalState.time || {}, timeDefaults)); - timefilter.setRefreshInterval( - _.defaults(globalState.refreshInterval || {}, refreshIntervalDefaults) - ); - - globalState.on('fetch_with_changes', () => { - // clone and default to {} in one - const newTime: TimeRange = _.defaults({}, globalState.time, timeDefaults); - const newRefreshInterval: RefreshInterval = _.defaults( - {}, - globalState.refreshInterval, - refreshIntervalDefaults - ); - - if (newTime) { - if (newTime.to) newTime.to = convertISO8601(newTime.to); - if (newTime.from) newTime.from = convertISO8601(newTime.from); - } - - timefilter.setTime(newTime); - timefilter.setRefreshInterval(newRefreshInterval); - }); - - const updateGlobalStateWithTime = () => { - globalState.time = timefilter.getTime(); - globalState.refreshInterval = timefilter.getRefreshInterval(); - globalState.save(); - }; - - subscribeWithScope($rootScope, timefilter.getRefreshIntervalUpdate$(), { - next: updateGlobalStateWithTime, - }); - - subscribeWithScope($rootScope, timefilter.getTimeUpdate$(), { - next: updateGlobalStateWithTime, - }); -}); - -uiRoutes.addSetupWork((globalState, $rootScope) => { - return registerTimefilterWithGlobalState(globalState, $rootScope); -}); diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/api/execute_trigger_actions.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/api/execute_trigger_actions.ts index 5c6216fb629ac..b7bf7e33e7c9d 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/api/execute_trigger_actions.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/api/execute_trigger_actions.ts @@ -18,9 +18,9 @@ */ import { EmbeddableApiPure } from './types'; -import { Action, ActionContext, buildContextMenuForActions, openContextMenu } from '../lib'; +import { Action, buildContextMenuForActions, openContextMenu } from '../lib'; -const executeSingleAction = async (action: Action, actionContext: ActionContext) => { +const executeSingleAction = async (action: Action, actionContext: A) => { const href = action.getHref(actionContext); // TODO: Do we need a `getHref()` special case? diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/api/types.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/api/types.ts index e68b3b89f5eb0..18073d2b90083 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/api/types.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/api/types.ts @@ -24,7 +24,6 @@ import { EmbeddableFactory, ExecuteTriggerActions, GetEmbeddableFactories, - TriggerContext, } from '../lib'; export interface EmbeddableApi { @@ -35,7 +34,7 @@ export interface EmbeddableApi { getEmbeddableFactories: GetEmbeddableFactories; getTrigger: (id: string) => Trigger; getTriggerActions: (id: string) => Action[]; - getTriggerCompatibleActions: (triggerId: string, context: TriggerContext) => Promise; + getTriggerCompatibleActions: (triggerId: string, context: C) => Promise>>; registerAction: (action: Action) => void; // TODO: Make `registerEmbeddableFactory` receive only `factory` argument. registerEmbeddableFactory: (id: string, factory: EmbeddableFactory) => void; diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts index 4cceceb57824f..79bdd65f9cfcd 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts @@ -26,7 +26,6 @@ export { APPLY_FILTER_TRIGGER, PANEL_BADGE_TRIGGER, Action, - ActionContext, Adapters, AddPanelAction, ApplyFilterAction, @@ -59,7 +58,6 @@ export { PropertySpec, SavedObjectMetaData, Trigger, - TriggerContext, ViewMode, isErrorEmbeddable, openAddPanelFlyout, diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/action.ts index 1002915de1d93..5569a3938bc72 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/action.ts @@ -17,20 +17,7 @@ * under the License. */ -import { IEmbeddable } from '../embeddables'; - -export interface ActionContext< - TEmbeddable extends IEmbeddable = IEmbeddable, - TTriggerContext extends {} = {} -> { - embeddable: TEmbeddable; - triggerContext?: TTriggerContext; -} - -export abstract class Action< - TEmbeddable extends IEmbeddable = IEmbeddable, - TTriggerContext extends {} = {} -> { +export abstract class Action { /** * Determined the order when there is more than one action matched to a trigger. * Higher numbers are displayed first. @@ -43,7 +30,7 @@ export abstract class Action< /** * Optional EUI icon type that can be displayed along with the title. */ - public getIconType(context: ActionContext): string | undefined { + public getIconType(context: ActionContext): string | undefined { return undefined; } @@ -51,27 +38,25 @@ export abstract class Action< * Returns a title to be displayed to the user. * @param context */ - public abstract getDisplayName(context: ActionContext): string; + public abstract getDisplayName(context: ActionContext): string; /** * Returns a promise that resolves to true if this action is compatible given the context, * otherwise resolves to false. */ - public async isCompatible( - context: ActionContext - ): Promise { + public async isCompatible(context: ActionContext): Promise { return true; } /** * If this returns something truthy, this is used in addition to the `execute` method when clicked. */ - public getHref(context: ActionContext): string | undefined { + public getHref(context: ActionContext): string | undefined { return undefined; } /** * Executes the action. */ - public abstract execute(context: ActionContext): void; + public abstract async execute(context: ActionContext): Promise; } diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/apply_filter_action.test.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/apply_filter_action.test.ts index e32500af1d7fd..c14f258b5641c 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/apply_filter_action.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/apply_filter_action.test.ts @@ -48,9 +48,7 @@ describe('isCompatible()', () => { }), }), } as any, - triggerContext: { - filters: [], - }, + filters: [], }); expect(result).toBe(true); }); @@ -65,9 +63,7 @@ describe('isCompatible()', () => { }), }), } as any, - triggerContext: { - filters: [], - }, + filters: [], }); expect(result).toBe(false); }); @@ -83,25 +79,8 @@ describe('isCompatible()', () => { }), }), } as any, - triggerContext: { - // filters: [], - } as any, - }); + } as any); expect(result1).toBe(false); - - const result2 = await action.isCompatible({ - embeddable: { - getRoot: () => ({ - getInput: () => ({ - filters: [], - }), - }), - } as any, - // triggerContext: { - // filters: [], - // } as any - }); - expect(result2).toBe(false); }); }); @@ -125,42 +104,41 @@ describe('execute()', () => { const error = expectError(() => action.execute({ embeddable: getEmbeddable(), - triggerContext: {}, } as any) ); expect(error).toBeInstanceOf(Error); }); - test('updates filter input on success', () => { + test('updates filter input on success', async done => { const action = new ApplyFilterAction(); const [embeddable, root] = getEmbeddable(); - action.execute({ + await action.execute({ embeddable, - triggerContext: { - filters: ['FILTER' as any], - }, + filters: ['FILTER' as any], }); expect(root.updateInput).toHaveBeenCalledTimes(1); expect(root.updateInput.mock.calls[0][0]).toMatchObject({ filters: ['FILTER'], }); + + done(); }); - test('checks if action isCompatible', () => { + test('checks if action isCompatible', async done => { const action = new ApplyFilterAction(); const spy = jest.spyOn(action, 'isCompatible'); const [embeddable] = getEmbeddable(); - action.execute({ + await action.execute({ embeddable, - triggerContext: { - filters: ['FILTER' as any], - }, + filters: ['FILTER' as any], }); expect(spy).toHaveBeenCalledTimes(1); + + done(); }); }); }); diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/apply_filter_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/apply_filter_action.ts index c7b12629eeabe..24549a64bfc23 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/apply_filter_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/apply_filter_action.ts @@ -20,14 +20,18 @@ import { i18n } from '@kbn/i18n'; import { Filter } from '@kbn/es-query'; import { IEmbeddable, EmbeddableInput } from '../embeddables'; -import { Action, ActionContext } from './action'; +import { Action } from './action'; import { IncompatibleActionError } from '../errors'; export const APPLY_FILTER_ACTION = 'APPLY_FILTER_ACTION'; type RootEmbeddable = IEmbeddable; +interface ActionContext { + embeddable: IEmbeddable; + filters: Filter[]; +} -export class ApplyFilterAction extends Action { +export class ApplyFilterAction extends Action { public readonly type = APPLY_FILTER_ACTION; constructor() { @@ -40,30 +44,27 @@ export class ApplyFilterAction extends Action) { + public async isCompatible(context: ActionContext) { + if (context.embeddable === undefined) { + return false; + } const root = context.embeddable.getRoot() as RootEmbeddable; - return Boolean( - root.getInput().filters !== undefined && - context.triggerContext && - context.triggerContext.filters !== undefined - ); + return Boolean(root.getInput().filters !== undefined && context.filters !== undefined); } - public execute({ - embeddable, - triggerContext, - }: ActionContext) { - if (!triggerContext) { - throw new Error('Applying a filter requires a filter as context'); + public async execute({ embeddable, filters }: ActionContext) { + if (!filters || !embeddable) { + throw new Error('Applying a filter requires a filter and embeddable as context'); } - const root = embeddable.getRoot() as RootEmbeddable; - if (!this.isCompatible({ triggerContext, embeddable })) { + if (!(await this.isCompatible({ embeddable, filters }))) { throw new IncompatibleActionError(); } + const root = embeddable.getRoot() as RootEmbeddable; + root.updateInput({ - filters: triggerContext.filters, + filters, }); } } diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/edit_panel_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/edit_panel_action.ts index 5edd82bd14a37..0b37491212810 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/edit_panel_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/edit_panel_action.ts @@ -18,13 +18,18 @@ */ import { i18n } from '@kbn/i18n'; -import { Action, ActionContext } from './action'; +import { Action } from './action'; import { GetEmbeddableFactory, ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; +import { IEmbeddable } from '../embeddables'; export const EDIT_PANEL_ACTION_ID = 'editPanel'; -export class EditPanelAction extends Action { +interface ActionContext { + embeddable: IEmbeddable; +} + +export class EditPanelAction extends Action { public readonly type = EDIT_PANEL_ACTION_ID; constructor(private readonly getEmbeddableFactory: GetEmbeddableFactory) { super(EDIT_PANEL_ACTION_ID); @@ -56,8 +61,8 @@ export class EditPanelAction extends Action { return Boolean(canEditEmbeddable && inDashboardEditMode); } - public execute() { - return undefined; + public async execute() { + return; } public getHref({ embeddable }: ActionContext): string { diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/index.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/index.ts index ae0df0a2122c6..7deace8345af9 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/index.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export { Action, ActionContext } from './action'; +export { Action } from './action'; export * from './apply_filter_action'; export * from './edit_panel_action'; diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/context_menu_actions/build_eui_context_menu_panels.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/context_menu_actions/build_eui_context_menu_panels.ts index b51a40a3ef2d9..5b1998834a2ed 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/context_menu_actions/build_eui_context_menu_panels.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/context_menu_actions/build_eui_context_menu_panels.ts @@ -20,21 +20,21 @@ import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Action, ActionContext } from '../actions'; +import { Action } from '../actions'; /** * Transforms an array of Actions to the shape EuiContextMenuPanel expects. */ -export async function buildContextMenuForActions({ +export async function buildContextMenuForActions({ actions, actionContext, closeMenu, }: { - actions: Action[]; - actionContext: ActionContext; + actions: Array>; + actionContext: A; closeMenu: () => void; }): Promise { - const menuItems = await buildEuiContextMenuPanelItems({ + const menuItems = await buildEuiContextMenuPanelItems({ actions, actionContext, closeMenu, @@ -52,13 +52,13 @@ export async function buildContextMenuForActions({ /** * Transform an array of Actions into the shape needed to build an EUIContextMenu */ -async function buildEuiContextMenuPanelItems({ +async function buildEuiContextMenuPanelItems({ actions, actionContext, closeMenu, }: { - actions: Action[]; - actionContext: ActionContext; + actions: Array>; + actionContext: A; closeMenu: () => void; }) { const items: EuiContextMenuPanelItemDescriptor[] = []; @@ -88,13 +88,13 @@ async function buildEuiContextMenuPanelItems({ * @param {Embeddable} embeddable * @return {EuiContextMenuPanelItemDescriptor} */ -function convertPanelActionToContextMenuItem({ +function convertPanelActionToContextMenuItem({ action, actionContext, closeMenu, }: { - action: Action; - actionContext: ActionContext; + action: Action; + actionContext: A; closeMenu: () => void; }): EuiContextMenuPanelItemDescriptor { const menuPanelItem: EuiContextMenuPanelItemDescriptor = { diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.tsx index c06f4ed99e8fd..172a5a76c4b21 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/embeddable_panel.tsx @@ -37,7 +37,7 @@ import { AddPanelAction } from './panel_header/panel_actions/add_panel/add_panel import { CustomizePanelTitleAction } from './panel_header/panel_actions/customize_title/customize_panel_action'; import { PanelHeader } from './panel_header/panel_header'; import { InspectPanelAction } from './panel_header/panel_actions/inspect_panel_action'; -import { EditPanelAction, Action, ActionContext } from '../actions'; +import { EditPanelAction, Action } from '../actions'; import { CustomizePanelModal } from './panel_header/panel_actions/customize_title/customize_panel_modal'; import { Start as InspectorStartContract } from '../../../../../../../../plugins/inspector/public'; @@ -192,7 +192,7 @@ export class EmbeddablePanel extends React.Component { }); const createGetUserData = (overlays: CoreStart['overlays']) => - async function getUserData(context: ActionContext) { + async function getUserData(context: { embeddable: IEmbeddable }) { return new Promise<{ title: string | undefined }>(resolve => { const session = overlays.openModal( { test('Is not compatible when embeddable is not a container', async () => { expect( - await action.isCompatible({ - embeddable, - }) + // @ts-ignore + await action.isCompatible({ embeddable }) ).toBe(false); }); diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts index 5c660f392d42c..7a49927e47251 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_action.ts @@ -18,14 +18,19 @@ */ import { i18n } from '@kbn/i18n'; import { ViewMode, GetEmbeddableFactory, GetEmbeddableFactories } from '../../../../types'; -import { Action, ActionContext } from '../../../../actions'; +import { Action } from '../../../../actions'; import { openAddPanelFlyout } from './open_add_panel_flyout'; import { NotificationsStart } from '../../../../../../../../../../../core/public'; import { KibanaReactOverlays } from '../../../../../../../../../../../plugins/kibana_react/public'; +import { IContainer } from '../../../../containers'; export const ADD_PANEL_ACTION_ID = 'ADD_PANEL_ACTION_ID'; -export class AddPanelAction extends Action { +interface ActionContext { + embeddable: IContainer; +} + +export class AddPanelAction extends Action { public readonly type = ADD_PANEL_ACTION_ID; constructor( diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts index 51001de0fefb5..d91cee3048c1f 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/customize_title/customize_panel_action.ts @@ -18,14 +18,19 @@ */ import { i18n } from '@kbn/i18n'; -import { Action, ActionContext } from '../../../../actions'; +import { Action } from '../../../../actions'; import { ViewMode } from '../../../../types'; +import { IEmbeddable } from '../../../../embeddables'; const CUSTOMIZE_PANEL_ACTION_ID = 'CUSTOMIZE_PANEL_ACTION_ID'; type GetUserData = (context: ActionContext) => Promise<{ title: string | undefined }>; -export class CustomizePanelTitleAction extends Action { +interface ActionContext { + embeddable: IEmbeddable; +} + +export class CustomizePanelTitleAction extends Action { public readonly type = CUSTOMIZE_PANEL_ACTION_ID; constructor(private readonly getDataFromUser: GetUserData) { diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts index 06ba48a5e23b2..3e4e2a36d46b9 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/inspect_panel_action.ts @@ -18,12 +18,17 @@ */ import { i18n } from '@kbn/i18n'; -import { Action, ActionContext } from '../../../actions'; +import { Action } from '../../../actions'; import { Start as InspectorStartContract } from '../../../../../../../../../../plugins/inspector/public'; +import { IEmbeddable } from '../../../embeddables'; export const INSPECT_PANEL_ACTION_ID = 'openInspector'; -export class InspectPanelAction extends Action { +interface ActionContext { + embeddable: IEmbeddable; +} + +export class InspectPanelAction extends Action { public readonly type = INSPECT_PANEL_ACTION_ID; constructor(private readonly inspector: InspectorStartContract) { diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx index 6421f4e85730c..22e3be89f1ae9 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.test.tsx @@ -28,7 +28,7 @@ import { } from '../../../test_samples/embeddables/filterable_embeddable'; import { FilterableEmbeddableFactory } from '../../../test_samples/embeddables/filterable_embeddable_factory'; import { FilterableContainer } from '../../../test_samples/embeddables/filterable_container'; -import { GetEmbeddableFactory } from '../../../types'; +import { GetEmbeddableFactory, ViewMode } from '../../../types'; import { ContactCardEmbeddable } from '../../../test_samples/embeddables/contact_card/contact_card_embeddable'; const embeddableFactories = new Map(); @@ -45,7 +45,7 @@ beforeEach(async () => { query: { match: {} }, }; container = new FilterableContainer( - { id: 'hello', panels: {}, filters: [derivedFilter] }, + { id: 'hello', panels: {}, filters: [derivedFilter], viewMode: ViewMode.EDIT }, getFactory ); @@ -55,6 +55,7 @@ beforeEach(async () => { FilterableEmbeddable >(FILTERABLE_EMBEDDABLE, { id: '123', + viewMode: ViewMode.EDIT, }); if (isErrorEmbeddable(filterableEmbeddable)) { @@ -68,7 +69,7 @@ test('Removes the embeddable', async () => { const removePanelAction = new RemovePanelAction(); expect(container.getChild(embeddable.id)).toBeDefined(); - removePanelAction.execute({ embeddable }); + await removePanelAction.execute({ embeddable }); expect(container.getChild(embeddable.id)).toBeUndefined(); }); diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts index f8fc9d02992d4..2a61857d17f77 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/panel/panel_header/panel_actions/remove_panel_action.ts @@ -19,8 +19,9 @@ import { i18n } from '@kbn/i18n'; import { ContainerInput, IContainer } from '../../../containers'; import { ViewMode } from '../../../types'; -import { Action, ActionContext } from '../../../actions'; +import { Action } from '../../../actions'; import { IncompatibleActionError } from '../../../errors'; +import { IEmbeddable } from '../../../embeddables'; export const REMOVE_PANEL_ACTION = 'deletePanel'; @@ -28,13 +29,17 @@ interface ExpandedPanelInput extends ContainerInput { expandedPanelId: string; } +interface ActionContext { + embeddable: IEmbeddable; +} + function hasExpandedPanelInput( container: IContainer ): container is IContainer<{}, ExpandedPanelInput> { return (container as IContainer<{}, ExpandedPanelInput>).getInput().expandedPanelId !== undefined; } -export class RemovePanelAction extends Action { +export class RemovePanelAction extends Action { public readonly type = REMOVE_PANEL_ACTION; constructor() { super(REMOVE_PANEL_ACTION); @@ -63,8 +68,8 @@ export class RemovePanelAction extends Action { ); } - public execute({ embeddable }: ActionContext) { - if (!embeddable.parent || !this.isCompatible({ embeddable })) { + public async execute({ embeddable }: ActionContext) { + if (!embeddable.parent || !(await this.isCompatible({ embeddable }))) { throw new IncompatibleActionError(); } embeddable.parent.removeEmbeddable(embeddable.id); diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/edit_mode_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/edit_mode_action.ts index d2e893887808b..f87ff835f59cc 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/edit_mode_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/edit_mode_action.ts @@ -18,11 +18,15 @@ */ import { ViewMode } from '../../types'; -import { Action, ActionContext } from '../../actions'; +import { Action } from '../../actions'; +import { IEmbeddable } from '../../embeddables'; export const EDIT_MODE_ACTION = 'EDIT_MODE_ACTION'; -export class EditModeAction extends Action { +interface ActionContext { + embeddable: IEmbeddable; +} +export class EditModeAction extends Action { public readonly type = EDIT_MODE_ACTION; constructor() { @@ -37,7 +41,7 @@ export class EditModeAction extends Action { return context.embeddable.getInput().viewMode === ViewMode.EDIT; } - execute() { + async execute() { return; } } diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/hello_world_action.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/hello_world_action.tsx index bfd7ac6541f01..356679b5e4501 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/hello_world_action.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/hello_world_action.tsx @@ -35,7 +35,7 @@ export class HelloWorldAction extends Action { return 'Hello World Action!'; } - public execute() { + public async execute() { const flyoutSession = this.overlays.openFlyout( flyoutSession && flyoutSession.close()}> Hello World, I am a hello world action! diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/restricted_action.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/restricted_action.ts index 950b843816c7a..16aede6da18d1 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/restricted_action.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/restricted_action.ts @@ -17,15 +17,15 @@ * under the License. */ -import { Action, ActionContext } from '../../actions'; +import { Action } from '../../actions'; export const RESTRICTED_ACTION = 'RESTRICTED_ACTION'; -export class RestrictedAction extends Action { +export class RestrictedAction extends Action { public readonly type = RESTRICTED_ACTION; - private isCompatibleFn: (context: ActionContext) => boolean; - constructor(isCompatible: (context: ActionContext) => boolean) { + private isCompatibleFn: (context: A) => boolean; + constructor(isCompatible: (context: A) => boolean) { super(RESTRICTED_ACTION); this.isCompatibleFn = isCompatible; } @@ -34,9 +34,9 @@ export class RestrictedAction extends Action { return `I am only sometimes compatible`; } - async isCompatible(context: ActionContext) { + async isCompatible(context: A) { return this.isCompatibleFn(context); } - execute() {} + async execute() {} } diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/say_hello_action.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/say_hello_action.tsx index e64d0e8cec58c..1a884148498aa 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/say_hello_action.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/say_hello_action.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { Action, ActionContext } from '../../actions'; +import { Action } from '../../actions'; import { EmbeddableInput, Embeddable, EmbeddableOutput, IEmbeddable } from '../../embeddables'; import { IncompatibleActionError } from '../../errors'; @@ -36,7 +36,12 @@ export function hasFullNameOutput( ); } -export class SayHelloAction extends Action { +interface ActionContext { + embeddable: Embeddable; + message?: string; +} + +export class SayHelloAction extends Action { public readonly type = SAY_HELLO_ACTION; private sayHello: (name: string) => void; @@ -53,9 +58,7 @@ export class SayHelloAction extends Action { // Can use typescript generics to get compiler time warnings for immediate feedback if // the context is not compatible. - async isCompatible( - context: ActionContext> - ) { + async isCompatible(context: ActionContext) { // Option 1: only compatible with Greeting Embeddables. // return context.embeddable.type === CONTACT_CARD_EMBEDDABLE; @@ -63,20 +66,15 @@ export class SayHelloAction extends Action { return hasFullNameOutput(context.embeddable); } - async execute( - context: ActionContext< - Embeddable, - { message?: string } - > - ) { + async execute(context: ActionContext) { if (!(await this.isCompatible(context))) { throw new IncompatibleActionError(); } const greeting = `Hello, ${context.embeddable.getOutput().fullName}`; - if (context.triggerContext && context.triggerContext.message) { - this.sayHello(`${greeting}. ${context.triggerContext.message}`); + if (context.message) { + this.sayHello(`${greeting}. ${context.message}`); } else { this.sayHello(greeting); } diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/send_message_action.tsx b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/send_message_action.tsx index 9ac8232725299..0f54f80398f20 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/send_message_action.tsx +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/actions/send_message_action.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { EuiFlyoutBody } from '@elastic/eui'; -import { Action, ActionContext, IncompatibleActionError } from '../..'; +import { Action, IncompatibleActionError } from '../..'; import { Embeddable, EmbeddableInput } from '../../embeddables'; import { GetMessageModal } from './get_message_modal'; import { FullNameEmbeddableOutput, hasFullNameOutput } from './say_hello_action'; @@ -26,6 +26,10 @@ import { CoreStart } from '../../../../../../../../../core/public'; export const SEND_MESSAGE_ACTION = 'SEND_MESSAGE_ACTION'; +interface ActionContext { + embeddable: Embeddable; + message: string; +} export class SendMessageAction extends Action { public readonly type = SEND_MESSAGE_ACTION; @@ -37,28 +41,18 @@ export class SendMessageAction extends Action { return 'Send message'; } - async isCompatible( - context: ActionContext> - ) { + async isCompatible(context: ActionContext) { return hasFullNameOutput(context.embeddable); } - async sendMessage( - context: ActionContext>, - message: string - ) { + async sendMessage(context: ActionContext, message: string) { const greeting = `Hello, ${context.embeddable.getOutput().fullName}`; const content = message ? `${greeting}. ${message}` : greeting; this.overlays.openFlyout({content}); } - async execute( - context: ActionContext< - Embeddable, - { message?: string } - > - ) { + async execute(context: ActionContext) { if (!(await this.isCompatible(context))) { throw new IncompatibleActionError(); } diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/types.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/types.ts index 9d10a0f059f3c..925afe07ff539 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/types.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/types.ts @@ -17,9 +17,7 @@ * under the License. */ -import { Action, ActionContext } from './actions'; -import { IEmbeddable } from './embeddables'; -import { IContainer } from './containers'; +import { Action } from './actions'; import { EmbeddableFactory } from './embeddables/embeddable_factory'; import { Adapters } from '../../../../../../../plugins/inspector/public'; @@ -55,18 +53,10 @@ export interface SavedObjectMetaData { showSavedObject?(savedObject: any): any; } -export interface TriggerContext { - embeddable: IEmbeddable; - container?: IContainer; -} - -export type ExecuteTriggerActions = ( - triggerId: string, - actionContext: ActionContext -) => Promise; -export type GetActionsCompatibleWithTrigger = ( +export type ExecuteTriggerActions = (triggerId: string, actionContext: A) => Promise; +export type GetActionsCompatibleWithTrigger = ( triggerId: string, - context: TriggerContext + context: C ) => Promise; export type GetEmbeddableFactory = (id: string) => EmbeddableFactory | undefined; export type GetEmbeddableFactories = () => IterableIterator; diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/apply_filter_action.test.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/apply_filter_action.test.ts index 7cd56290bf781..6e5d2182b7063 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/apply_filter_action.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/apply_filter_action.test.ts @@ -85,7 +85,7 @@ test('ApplyFilterAction applies the filter to the root of the container tree', a query: { match: { extension: { query: 'foo' } } }, }; - applyFilterAction.execute({ embeddable, triggerContext: { filters: [filter] } }); + await applyFilterAction.execute({ embeddable, filters: [filter] }); expect(root.getInput().filters.length).toBe(1); expect(node1.getInput().filters.length).toBe(1); expect(embeddable.getInput().filters.length).toBe(1); @@ -124,6 +124,7 @@ test('ApplyFilterAction is incompatible if the root container does not accept a throw new Error(); } + // @ts-ignore expect(await applyFilterAction.isCompatible({ embeddable })).toBe(false); }); @@ -160,6 +161,7 @@ test('trying to execute on incompatible context throws an error ', async () => { } async function check() { + // @ts-ignore await applyFilterAction.execute({ embeddable }); } await expect(check()).rejects.toThrow(Error); diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/execute_trigger_actions.test.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/execute_trigger_actions.test.ts index a2406a548b0cb..1b5a80af52987 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/execute_trigger_actions.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/execute_trigger_actions.test.ts @@ -19,7 +19,7 @@ import { testPlugin, TestPluginReturn } from './test_plugin'; import { of } from './helpers'; -import { ActionContext, Action, openContextMenu } from '../lib'; +import { Action, openContextMenu, IEmbeddable } from '../lib'; import { ContactCardEmbeddable, CONTACT_USER_TRIGGER, @@ -31,11 +31,11 @@ jest.mock('../lib/context_menu_actions'); const executeFn = jest.fn(); const openContextMenuSpy = (openContextMenu as any) as jest.SpyInstance; -class TestAction extends Action { +class TestAction extends Action { public readonly type = 'testAction'; - public checkCompatibility: (context: ActionContext) => boolean; + public checkCompatibility: (context: A) => boolean; - constructor(id: string, checkCompatibility: (context: ActionContext) => boolean) { + constructor(id: string, checkCompatibility: (context: A) => boolean) { super(id); this.checkCompatibility = checkCompatibility; } @@ -44,11 +44,11 @@ class TestAction extends Action { return 'test'; } - async isCompatible(context: ActionContext) { + async isCompatible(context: A) { return this.checkCompatibility(context); } - execute(context: ActionContext) { + async execute(context: unknown) { executeFn(context); } } @@ -124,7 +124,10 @@ test('does not execute an incompatible action', async () => { title: 'My trigger', actionIds: ['test1'], }; - const action = new TestAction('test1', ({ embeddable }) => embeddable.id === 'executeme'); + const action = new TestAction<{ embeddable: IEmbeddable }>( + 'test1', + ({ embeddable }) => embeddable.id === 'executeme' + ); const embeddable = new ContactCardEmbeddable( { id: 'executeme', @@ -187,7 +190,7 @@ test('passes whole action context to isCompatible()', async () => { title: 'My trigger', actionIds: ['test'], }; - const action = new TestAction('test', ({ triggerContext }) => { + const action = new TestAction<{ triggerContext: any }>('test', ({ triggerContext }) => { expect(triggerContext).toEqual({ foo: 'bar', }); diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/get_trigger_compatible_actions.test.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/get_trigger_compatible_actions.test.ts index ba926458cce13..f70bfcb080ae9 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/get_trigger_compatible_actions.test.ts +++ b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/tests/get_trigger_compatible_actions.test.ts @@ -22,7 +22,7 @@ import { HelloWorldAction } from '../lib/test_samples/actions/hello_world_action import { SayHelloAction } from '../lib/test_samples/actions/say_hello_action'; import { RestrictedAction } from '../lib/test_samples/actions/restricted_action'; import { EmptyEmbeddable } from '../lib/test_samples/embeddables/empty_embeddable'; -import { ActionContext, CONTEXT_MENU_TRIGGER } from '../lib'; +import { CONTEXT_MENU_TRIGGER, IEmbeddable } from '../lib'; import { of } from './helpers'; let action: SayHelloAction; @@ -73,7 +73,7 @@ test('getTriggerCompatibleActions returns attached actions', async () => { test('filters out actions not applicable based on the context', async () => { const { setup, doStart } = embeddables; - const restrictedAction = new RestrictedAction((context: ActionContext) => { + const restrictedAction = new RestrictedAction<{ embeddable: IEmbeddable }>(context => { return context.embeddable.id === 'accept'; }); diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/string.js b/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/string.js index 04c0b9a045d38..659d040189891 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/string.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/__tests__/string.js @@ -70,4 +70,11 @@ describe('String Format', function () { expect(string.convert(value)).to.be(value); }); + it('decode a URL Param string', function () { + const string = new StringFormat({ + transform: 'urlparam' + }); + expect(string.convert('%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98')).to.be('안녕 키바나'); + }); + }); diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/string.js b/src/legacy/core_plugins/kibana/common/field_formats/types/string.js index acab65058159d..9440f740c7659 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/string.js +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/string.js @@ -26,7 +26,8 @@ const TRANSFORM_OPTIONS = [ { kind: 'upper', text: 'Upper Case' }, { kind: 'title', text: 'Title Case' }, { kind: 'short', text: 'Short Dots' }, - { kind: 'base64', text: 'Base64 Decode' } + { kind: 'base64', text: 'Base64 Decode' }, + { kind: 'urlparam', text: 'URL Param Decode' } ]; const DEFAULT_TRANSFORM_OPTION = false; @@ -57,6 +58,7 @@ export function createStringFormat(FieldFormat) { case 'title': return this._toTitleCase(val); case 'short': return shortenDottedString(val); case 'base64': return this._base64Decode(val); + case 'urlparam': return decodeURIComponent(val); default: return asPrettyString(val); } } 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 a0fe853484eee..1f65ccebb67d9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.tsx @@ -36,7 +36,7 @@ import { import { KbnUrl } from 'ui/url/kbn_url'; import { Filter } from '@kbn/es-query'; -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { IndexPattern } from 'ui/index_patterns'; import { IPrivate } from 'ui/private'; import { StaticIndexPattern, Query, SavedQuery } from 'plugins/data'; 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 081ad3fa3a95d..5b4a4c99d403a 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 @@ -33,8 +33,7 @@ import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { docTitle } from 'ui/doc_title/doc_title'; -// @ts-ignore -import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; +import { showSaveModal, SaveResult } from 'ui/saved_objects/show_saved_object_save_modal'; import { showShareContextMenu, ShareContextMenuExtensionsRegistryProvider } from 'ui/share'; import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; @@ -604,7 +603,7 @@ export class DashboardAppController { * @return {Promise} * @resolved {String} - The id of the doc */ - function save(saveOptions: SaveOptions): Promise<{ id?: string } | { error: Error }> { + function save(saveOptions: SaveOptions): Promise { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function(id) { if (id) { @@ -695,7 +694,7 @@ export class DashboardAppController { isTitleDuplicateConfirmed, onTitleDuplicate, }; - return save(saveOptions).then((response: { id?: string } | { error: Error }) => { + return save(saveOptions).then((response: SaveResult) => { // If the save wasn't successful, put the original values back. if (!(response as { id: string }).id) { dashboardStateManager.setTitle(currentTitle); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts index 40e65f5683ee2..be76f498c8911 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_state.test.ts @@ -23,7 +23,8 @@ import { DashboardStateManager } from './dashboard_state_manager'; import { getAppStateMock, getSavedDashboardMock } from './__tests__'; import { AppStateClass } from 'ui/state_management/app_state'; import { DashboardAppState } from './types'; -import { Timefilter, TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; +import { Timefilter } from 'ui/timefilter'; import { ViewMode } from '../../../embeddable_api/public/np_ready/public'; describe('DashboardState', function() { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts index 93f487ba6d502..707b5a0f5f5f5 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/update_saved_dashboard.ts @@ -19,7 +19,8 @@ import _ from 'lodash'; import { AppState } from 'ui/state_management/app_state'; -import { Timefilter, RefreshInterval } from 'ui/timefilter'; +import { Timefilter } from 'ui/timefilter'; +import { RefreshInterval } from 'src/plugins/data/public'; import { FilterUtils } from './filter_utils'; import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard'; 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 a8cd8ffcb77f9..d8216361562e2 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 @@ -19,14 +19,10 @@ import React, { Fragment } 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 { - EuiLink, - EuiButton, - EuiEmptyPrompt, -} from '@elastic/eui'; +import { EuiLink, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { TableListView } from './../../table_list_view'; @@ -37,8 +33,7 @@ export const EMPTY_FILTER = ''; // 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 DashboardListingUi extends React.Component { - +export class DashboardListing extends React.Component { constructor(props) { super(props); } @@ -54,21 +49,15 @@ class DashboardListingUi extends React.Component { listingLimit={this.props.listingLimit} initialFilter={this.props.initialFilter} noItemsFragment={this.getNoItemsMessage()} - entityName={ - i18n.translate('kbn.dashboard.listing.table.entityName', { - defaultMessage: 'dashboard' - }) - } - entityNamePlural={ - i18n.translate('kbn.dashboard.listing.table.entityNamePlural', { - defaultMessage: 'dashboards' - }) - } - tableListTitle={ - i18n.translate('kbn.dashboard.listing.dashboardsTitle', { - defaultMessage: 'Dashboards' - }) - } + entityName={i18n.translate('kbn.dashboard.listing.table.entityName', { + defaultMessage: 'dashboard', + })} + entityNamePlural={i18n.translate('kbn.dashboard.listing.table.entityNamePlural', { + defaultMessage: 'dashboards', + })} + tableListTitle={i18n.translate('kbn.dashboard.listing.dashboardsTitle', { + defaultMessage: 'Dashboards', + })} /> ); } @@ -146,7 +135,6 @@ class DashboardListingUi extends React.Component { />
); - } getTableColumns() { @@ -154,7 +142,7 @@ class DashboardListingUi extends React.Component { { field: 'title', name: i18n.translate('kbn.dashboard.listing.table.titleColumnName', { - defaultMessage: 'Title' + defaultMessage: 'Title', }), sortable: true, render: (field, record) => ( @@ -164,22 +152,22 @@ class DashboardListingUi extends React.Component { > {field} - ) + ), }, { field: 'description', name: i18n.translate('kbn.dashboard.listing.table.descriptionColumnName', { - defaultMessage: 'Description' + defaultMessage: 'Description', }), dataType: 'string', sortable: true, - } + }, ]; return tableColumns; } } -DashboardListingUi.propTypes = { +DashboardListing.propTypes = { createItem: PropTypes.func.isRequired, findItems: PropTypes.func.isRequired, deleteItems: PropTypes.func.isRequired, @@ -190,8 +178,6 @@ DashboardListingUi.propTypes = { initialFilter: PropTypes.string, }; -DashboardListingUi.defaultProps = { +DashboardListing.defaultProps = { initialFilter: EMPTY_FILTER, }; - -export const DashboardListing = injectI18n(DashboardListingUi); 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 866cc7300310d..57de395525e1b 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 @@ -17,63 +17,54 @@ * under the License. */ -jest.mock('ui/notify', +jest.mock( + 'ui/notify', () => ({ toastNotifications: { addWarning: () => {}, - } - }), { virtual: true }); + }, + }), + { virtual: true } +); -jest.mock('lodash', +jest.mock( + 'lodash', () => ({ ...require.requireActual('lodash'), // mock debounce to fire immediately with no internal timer - debounce: function (func) { + debounce: func => { function debounced(...args) { return func.apply(this, args); } return debounced; - } - }), { virtual: true }); + }, + }), + { virtual: true } +); import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallow } from 'enzyme'; -import { - DashboardListing, -} from './dashboard_listing'; +import { DashboardListing } from './dashboard_listing'; -const find = (num) => { +const find = num => { const hits = []; for (let i = 0; i < num; i++) { hits.push({ id: `dashboard${i}`, title: `dashboard${i} title`, - description: `dashboard${i} desc` + description: `dashboard${i} desc`, }); } return Promise.resolve({ total: num, - hits: hits + hits: hits, }); }; test('renders empty page in before initial fetch to avoid flickering', () => { - const component = shallowWithIntl( {}} - createItem={() => {}} - editItem={() => {}} - getViewUrl={() => {}} - listingLimit={1000} - hideWriteControls={false} - />); - expect(component).toMatchSnapshot(); -}); - -describe('after fetch', () => { - test('initialFilter', async () => { - const component = shallowWithIntl( {}} createItem={() => {}} @@ -81,8 +72,25 @@ describe('after fetch', () => { getViewUrl={() => {}} listingLimit={1000} hideWriteControls={false} - initialFilter="my dashboard" - />); + /> + ); + expect(component).toMatchSnapshot(); +}); + +describe('after fetch', () => { + test('initialFilter', async () => { + const component = shallow( + {}} + createItem={() => {}} + editItem={() => {}} + getViewUrl={() => {}} + listingLimit={1000} + hideWriteControls={false} + initialFilter="my dashboard" + /> + ); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -93,15 +101,17 @@ describe('after fetch', () => { }); test('renders table rows', async () => { - const component = shallowWithIntl( {}} - createItem={() => {}} - editItem={() => {}} - getViewUrl={() => {}} - listingLimit={1000} - hideWriteControls={false} - />); + const component = shallow( + {}} + createItem={() => {}} + editItem={() => {}} + getViewUrl={() => {}} + listingLimit={1000} + hideWriteControls={false} + /> + ); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -112,15 +122,17 @@ describe('after fetch', () => { }); test('renders call to action when no dashboards exist', async () => { - const component = shallowWithIntl( {}} - createItem={() => {}} - editItem={() => {}} - getViewUrl={() => {}} - listingLimit={1} - hideWriteControls={false} - />); + const component = shallow( + {}} + createItem={() => {}} + editItem={() => {}} + getViewUrl={() => {}} + listingLimit={1} + hideWriteControls={false} + /> + ); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -131,15 +143,17 @@ describe('after fetch', () => { }); test('hideWriteControls', async () => { - const component = shallowWithIntl( {}} - createItem={() => {}} - editItem={() => {}} - getViewUrl={() => {}} - listingLimit={1} - hideWriteControls={true} - />); + const component = shallow( + {}} + createItem={() => {}} + editItem={() => {}} + getViewUrl={() => {}} + listingLimit={1} + hideWriteControls={true} + /> + ); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); @@ -150,15 +164,17 @@ describe('after fetch', () => { }); test('renders warning when listingLimit is exceeded', async () => { - const component = shallowWithIntl( {}} - createItem={() => {}} - editItem={() => {}} - getViewUrl={() => {}} - listingLimit={1} - hideWriteControls={false} - />); + const component = shallow( + {}} + createItem={() => {}} + editItem={() => {}} + getViewUrl={() => {}} + listingLimit={1} + hideWriteControls={false} + /> + ); // Ensure all promises resolve await new Promise(resolve => process.nextTick(resolve)); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts index 6c8bf3321b05a..b1b5a57ca27dc 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import semver from 'semver'; import { GridData } from 'src/legacy/core_plugins/dashboard_embeddable_container/public/np_ready/public'; +import uuid from 'uuid'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel630, @@ -123,16 +124,17 @@ function migratePre61PanelToLatest( const { columns, sort, row, col, size_x: sizeX, size_y: sizeY, ...rest } = panel; + const panelIndex = panel.panelIndex ? panel.panelIndex.toString() : uuid.v4(); return { ...rest, version, - panelIndex: panel.panelIndex.toString(), + panelIndex, gridData: { x: (col - 1) * PANEL_WIDTH_SCALE_FACTOR, y: (row - 1) * heightScaleFactor, w: sizeX ? sizeX * PANEL_WIDTH_SCALE_FACTOR : DEFAULT_PANEL_WIDTH, h: sizeY ? sizeY * heightScaleFactor : DEFAULT_PANEL_HEIGHT, - i: panel.panelIndex.toString(), + i: panelIndex, }, embeddableConfig, }; @@ -175,10 +177,11 @@ function migrate610PanelToLatest( : PANEL_HEIGHT_SCALE_FACTOR; const { columns, sort, ...rest } = panel; + const panelIndex = panel.panelIndex ? panel.panelIndex.toString() : uuid.v4(); return { ...rest, version, - panelIndex: panel.panelIndex.toString(), + panelIndex, gridData: { w: panel.gridData.w * PANEL_WIDTH_SCALE_FACTOR, h: panel.gridData.h * heightScaleFactor, @@ -212,10 +215,11 @@ function migrate620PanelToLatest( : PANEL_HEIGHT_SCALE_FACTOR; const { columns, sort, ...rest } = panel; + const panelIndex = panel.panelIndex ? panel.panelIndex.toString() : uuid.v4(); return { ...rest, version, - panelIndex: panel.panelIndex.toString(), + panelIndex, gridData: { w: panel.gridData.w * PANEL_WIDTH_SCALE_FACTOR, h: panel.gridData.h * heightScaleFactor, @@ -242,10 +246,11 @@ function migrate630PanelToLatest( } const { columns, sort, ...rest } = panel; + const panelIndex = panel.panelIndex ? panel.panelIndex.toString() : uuid.v4(); return { ...rest, version, - panelIndex: panel.panelIndex.toString(), + panelIndex, embeddableConfig, }; } @@ -256,10 +261,11 @@ function migrate640To720PanelsToLatest( panel: RawSavedDashboardPanel630, version: string ): RawSavedDashboardPanel730ToLatest { + const panelIndex = panel.panelIndex ? panel.panelIndex.toString() : uuid.v4(); return { ...panel, version, - panelIndex: panel.panelIndex.toString(), + panelIndex, }; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts index 3ffa6c569cc4b..6404b5bdbd63b 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts @@ -106,6 +106,38 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when expect(newDoc.attributes.uiStateJSON).toBeUndefined(); }); +// See https://github.com/elastic/kibana/issues/44639 - apparently this can happen. +test('dashboard migration works when panelsJSON is missing panelIndex', () => { + const doc: DashboardDoc700To720 = { + id: '1', + type: 'dashboard', + references: [], + attributes: { + description: '', + uiStateJSON: '{}', + title: 'fancy stuff', + timeRestore: true, + version: 1, + panelsJSON: + '[{"id":"funky-funky","type":"visualization","size_x":7,"size_y":5,"col":1,"row":1},{"id":"funky-funky2","type":"search","size_x":5,"size_y":5,"col":8,"row":1,"columns":["_source"],"sort":["@timestamp","desc"]}]', + optionsJSON: '{"darkTheme":false}', + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"filter":[{"query":{"query_string":{"query":"user:spiderman","analyze_wildcard":true}}}]}', + }, + }, + }; + + const doc700: DashboardDoc700To720 = migrations.dashboard['7.0.0'](doc, mockLogger); + const newDoc = migrations.dashboard['7.3.0'](doc700, mockLogger); + + const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); + expect(parsedSearchSource.filter.length).toBe(0); + expect(parsedSearchSource.query.query).toBe('user:spiderman'); + + expect(newDoc.attributes.uiStateJSON).toBeUndefined(); +}); + test('dashboard migration 7.3.0 migrates panels', () => { const doc: DashboardDoc700To720 = { id: '1', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/types.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/types.ts index 17398a9e828f4..7fba2f4003f31 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/types.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/types.ts @@ -129,7 +129,7 @@ export interface RawSavedDashboardPanelTo60 { readonly size_y?: number; readonly row: number; readonly col: number; - panelIndex: number | string; // earlier versions allowed this to be number or string + panelIndex?: number | string; // earlier versions allowed this to be number or string. Some very early versions seem to be missing this entirely readonly name: string; // This is where custom panel titles are stored prior to Embeddable API v2 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 68fd8f0a5a976..1231ca28ed014 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 @@ -20,7 +20,7 @@ import { SearchSource } from 'ui/courier'; import { SavedObject } from 'ui/saved_objects/saved_object'; import moment from 'moment'; -import { RefreshInterval } from 'ui/timefilter'; +import { RefreshInterval } from 'src/plugins/data/public'; import { Query } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js index b6e2856cfee74..ac3bf7ed2d794 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.test.js @@ -19,55 +19,48 @@ import React from 'react'; import sinon from 'sinon'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { - findTestSubject, -} from '@elastic/eui/lib/test'; +import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; -import { - DashboardCloneModal, -} from './clone_modal'; +import { DashboardCloneModal } from './clone_modal'; let onClone; let onClose; -let component; beforeEach(() => { onClone = sinon.spy(); onClose = sinon.spy(); }); -function createComponent(creationMethod = mountWithIntl) { - component = creationMethod( - - ); -} - test('renders DashboardCloneModal', () => { - createComponent(shallowWithIntl); + const component = shallowWithI18nProvider( + + ); expect(component).toMatchSnapshot(); // eslint-disable-line }); test('onClone', () => { - createComponent(); + const component = mountWithI18nProvider( + + ); findTestSubject(component, 'cloneConfirmButton').simulate('click'); sinon.assert.calledWith(onClone, 'dash title'); sinon.assert.notCalled(onClose); }); test('onClose', () => { - createComponent(); + const component = mountWithI18nProvider( + + ); findTestSubject(component, 'cloneCancelButton').simulate('click'); sinon.assert.calledOnce(onClose); sinon.assert.notCalled(onClone); }); test('title', () => { - createComponent(); + const component = mountWithI18nProvider( + + ); const event = { target: { value: 'a' } }; component.find('input').simulate('change', event); findTestSubject(component, 'cloneConfirmButton').simulate('click'); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.tsx index f216dcf4506e4..e5e75e4b7d277 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/clone_modal.tsx @@ -18,7 +18,8 @@ */ import React, { Fragment } from 'react'; -import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -43,7 +44,6 @@ interface Props { ) => Promise; onClose: () => void; title: string; - intl: InjectedIntl; } interface State { @@ -53,7 +53,7 @@ interface State { isLoading: boolean; } -class DashboardCloneModalUi extends React.Component { +export class DashboardCloneModal extends React.Component { private isMounted = false; constructor(props: Props) { @@ -117,15 +117,12 @@ class DashboardCloneModalUi extends React.Component { @@ -215,5 +212,3 @@ class DashboardCloneModalUi extends React.Component { ); } } - -export const DashboardCloneModal = injectI18n(DashboardCloneModalUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.tsx index 07c8c392bd805..af284e6f557cb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/options.tsx @@ -18,7 +18,7 @@ */ import React, { Component } from 'react'; -import { injectI18n, InjectedIntl } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow, EuiSwitch } from '@elastic/eui'; @@ -27,7 +27,6 @@ interface Props { onUseMarginsChange: (useMargins: boolean) => void; hidePanelTitles: boolean; onHidePanelTitlesChange: (hideTitles: boolean) => void; - intl: InjectedIntl; } interface State { @@ -35,7 +34,7 @@ interface State { hidePanelTitles: boolean; } -class OptionsMenuUi extends Component { +export class OptionsMenu extends Component { state = { useMargins: this.props.useMargins, hidePanelTitles: this.props.hidePanelTitles, @@ -62,10 +61,12 @@ class OptionsMenuUi extends Component { { { ); } } - -export const OptionsMenu = injectI18n(OptionsMenuUi); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.test.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.test.js index ceee75055fa90..153a049276cee 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.test.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.test.js @@ -18,20 +18,20 @@ */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { - DashboardSaveModal, -} from './save_modal'; +import { DashboardSaveModal } from './save_modal'; test('renders DashboardSaveModal', () => { - const component = shallowWithIntl( {}} - onClose={() => {}} - title="dash title" - description="dash description" - timeRestore={true} - showCopyOnSave={true} - />); + const component = shallowWithI18nProvider( + {}} + onClose={() => {}} + title="dash title" + description="dash description" + timeRestore={true} + showCopyOnSave={true} + /> + ); expect(component).toMatchSnapshot(); // eslint-disable-line }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx index d889d4be16fc6..47455f04ba809 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/save_modal.tsx @@ -18,7 +18,7 @@ */ import React, { Fragment } from 'react'; -import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { EuiFormRow, EuiTextArea, EuiSwitch } from '@elastic/eui'; @@ -46,7 +46,6 @@ interface Props { description: string; timeRestore: boolean; showCopyOnSave: boolean; - intl: InjectedIntl; } interface State { @@ -54,7 +53,7 @@ interface State { timeRestore: boolean; } -class DashboardSaveModalUi extends React.Component { +export class DashboardSaveModal extends React.Component { state: State = { description: this.props.description, timeRestore: this.props.timeRestore, @@ -152,5 +151,3 @@ class DashboardSaveModalUi extends React.Component { ); } } - -export const DashboardSaveModal = injectI18n(DashboardSaveModalUi); diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/cell.html b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/cell.html index cea5712219653..77702ed606474 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/cell.html +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row/cell.html @@ -17,6 +17,7 @@ class="fa fa-search-plus kbnDocTableRowFilterButton" data-column="<%- column %>" tooltip-append-to-body="1" + data-test-subj="docTableCellFilter" tooltip="{{ ::'kbn.docTable.tableRow.filterForValueButtonTooltip' | i18n: {defaultMessage: 'Filter for value'} }}" aria-label="{{ ::'kbn.docTable.tableRow.filterForValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter for value'} }}" > @@ -25,6 +26,7 @@ ng-click="inlineFilter($event, '-')" class="fa fa-search-minus kbnDocTableRowFilterButton" data-column="<%- column %>" + data-test-subj="docTableCellFilterNegate" tooltip="{{ ::'kbn.docTable.tableRow.filterOutValueButtonTooltip' | i18n: {defaultMessage: 'Filter out value'} }}" aria-label="{{ ::'kbn.docTable.tableRow.filterOutValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter out value'} }}" tooltip-append-to-body="1" 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 92fad1713177a..e307d5da7ebe8 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 @@ -35,7 +35,8 @@ import { Filter, FilterStateStore } from '@kbn/es-query'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; import { toastNotifications } from 'ui/notify'; -import { timefilter, getTime, TimeRange } from 'ui/timefilter'; +import { timefilter, getTime } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { Query, onlyDisabledFiltersChanged } from '../../../../data/public'; import { APPLY_FILTER_TRIGGER, @@ -256,9 +257,7 @@ export class SearchEmbeddable extends Embeddable await this.executeTriggerActions(APPLY_FILTER_TRIGGER, { embeddable: this, - triggerContext: { - filters, - }, + filters, }); }; } diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index 88dafaacf7357..982e56731bacb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -22,7 +22,7 @@ import { capabilities } from 'ui/capabilities'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; import { IPrivate } from 'ui/private'; -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { EmbeddableFactory, 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 981580079ae18..a0e15e99b742d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/types.ts @@ -18,7 +18,7 @@ */ import { StaticIndexPattern } from 'ui/index_patterns'; -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { Query } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; import { SavedSearch } from '../types'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js index 5dc9d8d4ee737..0155b10f60218 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/settings/lib/get_category_name.js @@ -45,6 +45,9 @@ const names = { search: i18n.translate('kbn.management.settings.categoryNames.searchLabel', { defaultMessage: 'Search', }), + siem: i18n.translate('kbn.management.settings.categoryNames.siemLabel', { + defaultMessage: 'SIEM', + }), }; export function getCategoryName(category) { 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 d750010a132d6..6aa0a08d88cbb 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 @@ -29,7 +29,7 @@ import { } from 'ui/visualize/loader/types'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { Filter } from '@kbn/es-query'; import { EmbeddableInput, 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 a77baef960af1..a370a7c409fcb 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 @@ -41,10 +41,6 @@ import 'uiExports/visualize'; import { i18n } from '@kbn/i18n'; import { capabilities } from 'ui/capabilities'; -// @ts-ignore -import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -// @ts-ignore -import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import chrome from 'ui/chrome'; import { getVisualizeLoader } from 'ui/visualize/loader'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js index d221dcfc5ecaf..4da6ed16a144c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js +++ b/src/legacy/core_plugins/kibana/public/visualize/listing/visualize_listing.js @@ -30,11 +30,13 @@ import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { VisualizeListingTable } from './visualize_listing_table'; import { NewVisModal } from '../wizard/new_vis_modal'; import { VisualizeConstants } from '../visualize_constants'; -import { visualizations } from 'plugins/visualizations'; +import { setup } from '../../../../visualizations/public/np_ready/public/legacy'; import { i18n } from '@kbn/i18n'; const app = uiModules.get('app/visualize', ['ngRoute', 'react']); -app.directive('visualizeListingTable', reactDirective => reactDirective(wrapInI18nContext(VisualizeListingTable))); +app.directive('visualizeListingTable', reactDirective => + reactDirective(wrapInI18nContext(VisualizeListingTable)) +); app.directive('newVisModal', reactDirective => reactDirective(wrapInI18nContext(NewVisModal))); export function VisualizeListingController($injector, createNewVis) { @@ -44,7 +46,7 @@ export function VisualizeListingController($injector, createNewVis) { const savedObjectClient = Private(SavedObjectsClientProvider); this.visTypeRegistry = Private(VisTypesRegistryProvider); - this.visTypeAliases = visualizations.types.visTypeAliasRegistry.get(); + this.visTypeAliases = setup.types.visTypeAliasRegistry.get(); timefilter.disableAutoRefreshSelector(); timefilter.disableTimeRangeSelector(); @@ -81,15 +83,16 @@ export function VisualizeListingController($injector, createNewVis) { const services = Private(SavedObjectRegistryProvider).byLoaderPropertiesName; const visualizationService = services.visualizations; - this.fetchItems = (filter) => { + this.fetchItems = filter => { const isLabsEnabled = config.get('visualize:enableLabs'); - return visualizationService.findListItems(filter, config.get('savedObjects:listingLimit')) + return visualizationService + .findListItems(filter, config.get('savedObjects:listingLimit')) .then(result => { this.totalItems = result.total; return { total: result.total, - hits: result.hits.filter(result => (isLabsEnabled || result.type.stage !== 'experimental')) + hits: result.hits.filter(result => isLabsEnabled || result.type.stage !== 'experimental'), }; }); }; @@ -98,23 +101,27 @@ export function VisualizeListingController($injector, createNewVis) { return Promise.all( selectedItems.map(item => { return savedObjectClient.delete(item.savedObjectType, item.id); - }), - ).then(() => { - chrome.untrackNavLinksForDeletedSavedObjects(selectedItems.map(item => item.id)); - }).catch(error => { - toastNotifications.addError(error, { - title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', { - defaultMessage: 'Error deleting visualization', - }), + }) + ) + .then(() => { + chrome.untrackNavLinksForDeletedSavedObjects(selectedItems.map(item => item.id)); + }) + .catch(error => { + toastNotifications.addError(error, { + title: i18n.translate('kbn.visualize.visualizeListingDeleteErrorTitle', { + defaultMessage: 'Error deleting visualization', + }), + }); }); - }); }; - chrome.breadcrumbs.set([{ - text: i18n.translate('kbn.visualize.visualizeListingBreadcrumbsTitle', { - defaultMessage: 'Visualize', - }) - }]); + chrome.breadcrumbs.set([ + { + text: i18n.translate('kbn.visualize.visualizeListingBreadcrumbsTitle', { + defaultMessage: 'Visualize', + }), + }, + ]); this.listingLimit = config.get('savedObjects:listingLimit'); diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js index cfe5eeec3834f..5f0a2be54e095 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js @@ -22,7 +22,7 @@ import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import { uiModules } from 'ui/modules'; import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; -import { visualizations } from 'plugins/visualizations'; +import { setup } from '../../../../visualizations/public/np_ready/public/legacy'; import { createVisualizeEditUrl } from '../visualize_constants'; import { findListItems } from './find_list_items'; @@ -32,13 +32,18 @@ const app = uiModules.get('app/visualize'); // edited by the object editor. savedObjectManagementRegistry.register({ service: 'savedVisualizations', - title: 'visualizations' + title: 'visualizations', }); app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) { const visTypes = Private(VisTypesRegistryProvider); const savedObjectClient = Private(SavedObjectsClientProvider); - const saveVisualizationLoader = new SavedObjectLoader(SavedVis, kbnUrl, chrome, savedObjectClient); + const saveVisualizationLoader = new SavedObjectLoader( + SavedVis, + kbnUrl, + chrome, + savedObjectClient + ); saveVisualizationLoader.mapHitSource = function (source, id) { source.id = id; @@ -46,8 +51,11 @@ app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) let typeName = source.typeName; if (source.visState) { - try { typeName = JSON.parse(source.visState).type; } - catch (e) { /* missing typename handled below */ } // eslint-disable-line no-empty + try { + typeName = JSON.parse(source.visState).type; + } catch (e) { + /* missing typename handled below */ + } // eslint-disable-line no-empty } if (!typeName || !visTypes.byName[typeName]) { @@ -78,7 +86,7 @@ app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) size, mapSavedObjectApiHits: this.mapSavedObjectApiHits.bind(this), savedObjectsClient: this.savedObjectsClient, - visTypes: visualizations.types.visTypeAliasRegistry.get(), + visTypes: setup.types.visTypeAliasRegistry.get(), }); }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx b/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx index 4039bf3dff7a5..f7bb0a8f45731 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; import { VisType } from 'ui/vis'; -import { VisTypeAlias } from 'plugins/visualizations'; +import { VisTypeAlias } from '../../../../visualizations/public/np_ready/public'; import { VisualizeConstants } from '../visualize_constants'; import { SearchSelection } from './search_selection'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/type_selection/type_selection.tsx b/src/legacy/core_plugins/kibana/public/visualize/wizard/type_selection/type_selection.tsx index f2eb273af8f04..76fc84921627a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/type_selection/type_selection.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/type_selection/type_selection.tsx @@ -37,7 +37,7 @@ import { } from '@elastic/eui'; import { memoizeLast } from 'ui/utils/memoize'; import { VisType } from 'ui/vis'; -import { VisTypeAlias } from 'plugins/visualizations'; +import { VisTypeAlias } from '../../../../../visualizations/public/np_ready/public'; import { NewVisHelp } from './new_vis_help'; import { VisHelpText } from './vis_help_text'; import { VisTypeIcon } from './vis_type_icon'; diff --git a/src/legacy/core_plugins/metrics/common/set_is_reversed.js b/src/legacy/core_plugins/metrics/common/set_is_reversed.js index a5faed3d8a4e8..b633d004b9705 100644 --- a/src/legacy/core_plugins/metrics/common/set_is_reversed.js +++ b/src/legacy/core_plugins/metrics/common/set_is_reversed.js @@ -52,7 +52,7 @@ export const isBackgroundDark = (backgroundColor, currentTheme) => { const themeIsDark = isThemeDark(currentTheme); // If a background color doesn't exist or it inherits, pass back if it's a darktheme - if (backgroundColor === undefined || backgroundColor === 'inherit') { + if (!backgroundColor || backgroundColor === 'inherit') { return themeIsDark; } diff --git a/src/legacy/core_plugins/metrics/public/components/annotations_editor.js b/src/legacy/core_plugins/metrics/public/components/annotations_editor.js index f517875a522d7..53ada137c803f 100644 --- a/src/legacy/core_plugins/metrics/public/components/annotations_editor.js +++ b/src/legacy/core_plugins/metrics/public/components/annotations_editor.js @@ -26,11 +26,10 @@ import { AddDeleteButtons } from './add_delete_buttons'; import { ColorPicker } from './color_picker'; import { FieldSelect } from './aggs/field_select'; import uuid from 'uuid'; -import { IconSelect } from './icon_select'; +import { IconSelect } from './icon_select/icon_select'; import { YesNo } from './yes_no'; import { QueryBarWrapper } from './query_bar_wrapper'; import { getDefaultQueryLanguage } from './lib/get_default_query_language'; - import { htmlIdGenerator, EuiFlexGroup, diff --git a/src/legacy/core_plugins/metrics/public/components/icon_select.js b/src/legacy/core_plugins/metrics/public/components/icon_select.js deleted file mode 100644 index fcf1f38270949..0000000000000 --- a/src/legacy/core_plugins/metrics/public/components/icon_select.js +++ /dev/null @@ -1,127 +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 PropTypes from 'prop-types'; -import React from 'react'; -import { EuiComboBox } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -function renderOption(option) { - const icon = option.value; - const label = option.label; - return ( - - - ); -} - -export function IconSelect(props) { - const selectedIcon = props.icons.find(option => { - return props.value === option.value; - }); - return ( - - ); -} - -IconSelect.defaultProps = { - icons: [ - { - value: 'fa-asterisk', - label: i18n.translate('tsvb.iconSelect.asteriskLabel', { defaultMessage: 'Asterisk' }), - }, - { - value: 'fa-bell', - label: i18n.translate('tsvb.iconSelect.bellLabel', { defaultMessage: 'Bell' }), - }, - { - value: 'fa-bolt', - label: i18n.translate('tsvb.iconSelect.boltLabel', { defaultMessage: 'Bolt' }), - }, - { - value: 'fa-bomb', - label: i18n.translate('tsvb.iconSelect.bombLabel', { defaultMessage: 'Bomb' }), - }, - { - value: 'fa-bug', - label: i18n.translate('tsvb.iconSelect.bugLabel', { defaultMessage: 'Bug' }), - }, - { - value: 'fa-comment', - label: i18n.translate('tsvb.iconSelect.commentLabel', { defaultMessage: 'Comment' }), - }, - { - value: 'fa-exclamation-circle', - label: i18n.translate('tsvb.iconSelect.exclamationCircleLabel', { - defaultMessage: 'Exclamation Circle', - }), - }, - { - value: 'fa-exclamation-triangle', - label: i18n.translate('tsvb.iconSelect.exclamationTriangleLabel', { - defaultMessage: 'Exclamation Triangle', - }), - }, - { - value: 'fa-fire', - label: i18n.translate('tsvb.iconSelect.fireLabel', { defaultMessage: 'Fire' }), - }, - { - value: 'fa-flag', - label: i18n.translate('tsvb.iconSelect.flagLabel', { defaultMessage: 'Flag' }), - }, - { - value: 'fa-heart', - label: i18n.translate('tsvb.iconSelect.heartLabel', { defaultMessage: 'Heart' }), - }, - { - value: 'fa-map-marker', - label: i18n.translate('tsvb.iconSelect.mapMarkerLabel', { defaultMessage: 'Map Marker' }), - }, - { - value: 'fa-map-pin', - label: i18n.translate('tsvb.iconSelect.mapPinLabel', { defaultMessage: 'Map Pin' }), - }, - { - value: 'fa-star', - label: i18n.translate('tsvb.iconSelect.starLabel', { defaultMessage: 'Star' }), - }, - { - value: 'fa-tag', - label: i18n.translate('tsvb.iconSelect.tagLabel', { defaultMessage: 'Tag' }), - }, - ], -}; - -IconSelect.propTypes = { - icons: PropTypes.array, - id: PropTypes.string, - onChange: PropTypes.func, - value: PropTypes.string.isRequired, -}; diff --git a/src/legacy/core_plugins/metrics/public/components/icon_select/__snapshots__/icon_select.test.js.snap b/src/legacy/core_plugins/metrics/public/components/icon_select/__snapshots__/icon_select.test.js.snap new file mode 100644 index 0000000000000..fd22bcafb8df4 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/icon_select/__snapshots__/icon_select.test.js.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js should render and match a snapshot 1`] = ` + +`; + +exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js should render and match a snapshot 1`] = ` + + + Comment + +`; + +exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js ICONS should match and save an icons collection snapshot 1`] = ` +Array [ + Object { + "label": "Asterisk", + "value": "fa-asterisk", + }, + Object { + "label": "Bell", + "value": "fa-bell", + }, + Object { + "label": "Bolt", + "value": "fa-bolt", + }, + Object { + "label": "Comment", + "value": "fa-comment", + }, + Object { + "label": "Map Marker", + "value": "fa-map-marker", + }, + Object { + "label": "Map Pin", + "value": "fa-map-pin", + }, + Object { + "label": "Star", + "value": "fa-star", + }, + Object { + "label": "Tag", + "value": "fa-tag", + }, + Object { + "label": "Bomb", + "value": "fa-bomb", + }, + Object { + "label": "Bug", + "value": "fa-bug", + }, + Object { + "label": "Exclamation Circle", + "value": "fa-exclamation-circle", + }, + Object { + "label": "Exclamation Triangle", + "value": "fa-exclamation-triangle", + }, + Object { + "label": "Fire", + "value": "fa-fire", + }, + Object { + "label": "Flag", + "value": "fa-flag", + }, + Object { + "label": "Heart", + "value": "fa-heart", + }, +] +`; diff --git a/src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js b/src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js new file mode 100644 index 0000000000000..00a925496b928 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js @@ -0,0 +1,120 @@ +/* + * 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 PropTypes from 'prop-types'; +import React from 'react'; +import { EuiComboBox, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ICON_TYPES_MAP } from '../../visualizations/constants/icons'; + +export const ICONS = [ + { + value: 'fa-asterisk', + label: i18n.translate('tsvb.iconSelect.asteriskLabel', { defaultMessage: 'Asterisk' }), + }, + { + value: 'fa-bell', + label: i18n.translate('tsvb.iconSelect.bellLabel', { defaultMessage: 'Bell' }), + }, + { + value: 'fa-bolt', + label: i18n.translate('tsvb.iconSelect.boltLabel', { defaultMessage: 'Bolt' }), + }, + { + value: 'fa-comment', + label: i18n.translate('tsvb.iconSelect.commentLabel', { defaultMessage: 'Comment' }), + }, + { + value: 'fa-map-marker', + label: i18n.translate('tsvb.iconSelect.mapMarkerLabel', { defaultMessage: 'Map Marker' }), + }, + { + value: 'fa-map-pin', + label: i18n.translate('tsvb.iconSelect.mapPinLabel', { defaultMessage: 'Map Pin' }), + }, + { + value: 'fa-star', + label: i18n.translate('tsvb.iconSelect.starLabel', { defaultMessage: 'Star' }), + }, + { + value: 'fa-tag', + label: i18n.translate('tsvb.iconSelect.tagLabel', { defaultMessage: 'Tag' }), + }, + { + value: 'fa-bomb', + label: i18n.translate('tsvb.iconSelect.bombLabel', { defaultMessage: 'Bomb' }), + }, + { + value: 'fa-bug', + label: i18n.translate('tsvb.iconSelect.bugLabel', { defaultMessage: 'Bug' }), + }, + { + value: 'fa-exclamation-circle', + label: i18n.translate('tsvb.iconSelect.exclamationCircleLabel', { + defaultMessage: 'Exclamation Circle', + }), + }, + { + value: 'fa-exclamation-triangle', + label: i18n.translate('tsvb.iconSelect.exclamationTriangleLabel', { + defaultMessage: 'Exclamation Triangle', + }), + }, + { + value: 'fa-fire', + label: i18n.translate('tsvb.iconSelect.fireLabel', { defaultMessage: 'Fire' }), + }, + { + value: 'fa-flag', + label: i18n.translate('tsvb.iconSelect.flagLabel', { defaultMessage: 'Flag' }), + }, + { + value: 'fa-heart', + label: i18n.translate('tsvb.iconSelect.heartLabel', { defaultMessage: 'Heart' }), + }, +]; + +export function IconView({ value: icon, label }) { + return ( + + + {` ${label}`} + + ); +} + +export function IconSelect({ value, onChange }) { + const selectedIcon = ICONS.find(option => value === option.value) || ICONS[0]; + + return ( + + ); +} + +IconSelect.propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, +}; diff --git a/src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.test.js b/src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.test.js new file mode 100644 index 0000000000000..042fedac565db --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.test.js @@ -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 { IconSelect, IconView, ICONS } from './icon_select'; + +describe('src/legacy/core_plugins/metrics/public/components/icon_select/icon_select.js', () => { + describe('', () => { + test('should render and match a snapshot', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + + test("should put the default value if the passed one does't match with icons collection", () => { + const wrapper = shallow(); + + expect(wrapper.prop('selectedOptions')).toEqual([ICONS[0]]); + }); + }); + + describe('', () => { + test('should render and match a snapshot', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('ICONS', () => { + test('should match and save an icons collection snapshot', () => { + expect(ICONS).toMatchSnapshot(); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/tick_formatter.js b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/tick_formatter.js index bcca2f88ac2c4..f31af4e846305 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/__tests__/tick_formatter.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/__tests__/tick_formatter.js @@ -18,11 +18,11 @@ */ import { expect } from 'chai'; -import { tickFormatter } from '../tick_formatter'; +import { createTickFormatter } from '../tick_formatter'; -describe('tickFormatter(format, template)', () => { +describe('createTickFormatter(format, template)', () => { it('returns a number with two decimal place by default', () => { - const fn = tickFormatter(); + const fn = createTickFormatter(); expect(fn(1.5556)).to.equal('1.56'); }); @@ -30,7 +30,7 @@ describe('tickFormatter(format, template)', () => { const config = { 'format:percent:defaultPattern': '0.[00]%', }; - const fn = tickFormatter('percent', null, key => config[key]); + const fn = createTickFormatter('percent', null, key => config[key]); expect(fn(0.5556)).to.equal('55.56%'); }); @@ -38,12 +38,12 @@ describe('tickFormatter(format, template)', () => { const config = { 'format:bytes:defaultPattern': '0.0b', }; - const fn = tickFormatter('bytes', null, key => config[key]); + const fn = createTickFormatter('bytes', null, key => config[key]); expect(fn(1500 ^ 10)).to.equal('1.5KB'); }); it('returns a custom formatted string with custom formatter', () => { - const fn = tickFormatter('0.0a'); + const fn = createTickFormatter('0.0a'); expect(fn(1500)).to.equal('1.5k'); }); @@ -51,17 +51,22 @@ describe('tickFormatter(format, template)', () => { const config = { 'format:number:defaultLocale': 'fr', }; - const fn = tickFormatter('0,0.0', null, key => config[key]); + const fn = createTickFormatter('0,0.0', null, key => config[key]); expect(fn(1500)).to.equal('1 500,0'); }); it('returns a custom formatted string with custom formatter and template', () => { - const fn = tickFormatter('0.0a', '{{value}}/s'); + const fn = createTickFormatter('0.0a', '{{value}}/s'); expect(fn(1500)).to.equal('1.5k/s'); }); + it('returns "foo" if passed a string', () => { + const fn = createTickFormatter(); + expect(fn('foo')).to.equal('foo'); + }); + it('returns value if passed a bad formatter', () => { - const fn = tickFormatter('102'); + const fn = createTickFormatter('102'); expect(fn(100)).to.equal('100'); }); @@ -69,7 +74,7 @@ describe('tickFormatter(format, template)', () => { const config = { 'format:number:defaultPattern': '0,0.[00]', }; - const fn = tickFormatter('number', '{{value', key => config[key]); + const fn = createTickFormatter('number', '{{value', key => config[key]); expect(fn(1.5556)).to.equal('1.56'); }); }); diff --git a/src/legacy/core_plugins/metrics/public/components/lib/charts.js b/src/legacy/core_plugins/metrics/public/components/lib/charts.js new file mode 100644 index 0000000000000..e0e3cb1db21ce --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/lib/charts.js @@ -0,0 +1,26 @@ +/* + * 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 { uniq, map, size, flow } from 'lodash'; + +export const areFieldsDifferent = name => series => + flow( + uniq, + size + )(map(series, name)) > 1; diff --git a/src/legacy/core_plugins/metrics/public/components/lib/convert_series_to_vars.js b/src/legacy/core_plugins/metrics/public/components/lib/convert_series_to_vars.js index 7fdee5dbe845b..bcab9ec026d9a 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/convert_series_to_vars.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/convert_series_to_vars.js @@ -19,7 +19,7 @@ import _ from 'lodash'; import { getLastValue } from '../../../common/get_last_value'; -import { tickFormatter } from './tick_formatter'; +import { createTickFormatter } from './tick_formatter'; import moment from 'moment'; export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig = null) => { @@ -32,7 +32,7 @@ export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig .filter(v => v) .join('.'); - const formatter = tickFormatter( + const formatter = createTickFormatter( seriesModel.formatter, seriesModel.value_template, getConfig diff --git a/src/legacy/core_plugins/metrics/public/components/lib/get_axis_label_string.js b/src/legacy/core_plugins/metrics/public/components/lib/get_axis_label_string.js index 4cd83e3fa2832..6a8822beca120 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/get_axis_label_string.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/get_axis_label_string.js @@ -20,6 +20,10 @@ import { convertIntervalIntoUnit } from './get_interval'; import { i18n } from '@kbn/i18n'; export function getAxisLabelString(interval) { + if (!interval) { + return ''; + } + const convertedValue = convertIntervalIntoUnit(interval); if (convertedValue) { diff --git a/src/legacy/core_plugins/metrics/public/components/lib/new_series_fn.js b/src/legacy/core_plugins/metrics/public/components/lib/new_series_fn.js index 0041038b80cb9..9d2398ed079a9 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/new_series_fn.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/new_series_fn.js @@ -20,6 +20,7 @@ import uuid from 'uuid'; import _ from 'lodash'; import { newMetricAggFn } from './new_metric_agg_fn'; +import { STACKED_OPTIONS } from '../../visualizations/constants'; export const newSeriesFn = (obj = {}) => { return _.assign( @@ -35,7 +36,7 @@ export const newSeriesFn = (obj = {}) => { line_width: 1, point_size: 1, fill: 0.5, - stacked: 'none', + stacked: STACKED_OPTIONS.NONE, }, obj ); diff --git a/src/legacy/core_plugins/metrics/public/components/lib/stacked.js b/src/legacy/core_plugins/metrics/public/components/lib/stacked.js new file mode 100644 index 0000000000000..f1cfcfbc05ba3 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/lib/stacked.js @@ -0,0 +1,20 @@ +/* + * 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 isPercentDisabled = seriesQuantity => seriesQuantity < 2; diff --git a/src/legacy/core_plugins/metrics/public/components/lib/tick_formatter.js b/src/legacy/core_plugins/metrics/public/components/lib/tick_formatter.js index f0a995fa7ef3b..0459d11c74ef0 100644 --- a/src/legacy/core_plugins/metrics/public/components/lib/tick_formatter.js +++ b/src/legacy/core_plugins/metrics/public/components/lib/tick_formatter.js @@ -22,7 +22,7 @@ import { isNumber } from 'lodash'; import { fieldFormats } from 'ui/registry/field_formats'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; -export const tickFormatter = (format = '0,0.[00]', template, getConfig = null) => { +export const createTickFormatter = (format = '0,0.[00]', template, getConfig = null) => { if (!template) template = '{{value}}'; const render = handlebars.compile(template, { knownHelpersOnly: true }); let formatter; diff --git a/src/legacy/core_plugins/metrics/public/components/markdown_editor.js b/src/legacy/core_plugins/metrics/public/components/markdown_editor.js index 9fb766f174c48..4096fd6ee1134 100644 --- a/src/legacy/core_plugins/metrics/public/components/markdown_editor.js +++ b/src/legacy/core_plugins/metrics/public/components/markdown_editor.js @@ -24,7 +24,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { tickFormatter } from './lib/tick_formatter'; +import { createTickFormatter } from './lib/tick_formatter'; import { convertSeriesToVars } from './lib/convert_series_to_vars'; import _ from 'lodash'; import 'brace/mode/markdown'; @@ -59,7 +59,7 @@ export class MarkdownEditor extends Component { const series = _.get(visData, `${model.id}.series`, []); const variables = convertSeriesToVars(series, model, dateFormat, this.props.getConfig); const rows = []; - const rawFormatter = tickFormatter('0.[0000]', null, this.props.getConfig); + const rawFormatter = createTickFormatter('0.[0000]', null, this.props.getConfig); const createPrimitiveRow = key => { const snippet = `{{ ${key} }}`; diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config.js b/src/legacy/core_plugins/metrics/public/components/panel_config.js index d1487955887b6..d58c682660e01 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config.js @@ -57,7 +57,7 @@ export function PanelConfig(props) { return function cleanup() { visDataSubscription.unsubscribe(); }; - }, [props.visData$]); + }, [model.id, props.visData$]); const updateControlValidity = (controlKey, isControlValid) => { formValidationResults[controlKey] = isControlValid; diff --git a/src/legacy/core_plugins/metrics/public/components/series.js b/src/legacy/core_plugins/metrics/public/components/series.js index 06cf533cb8fd1..1d397dd93121d 100644 --- a/src/legacy/core_plugins/metrics/public/components/series.js +++ b/src/legacy/core_plugins/metrics/public/components/series.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { assign } from 'lodash'; +import { assign, get } from 'lodash'; import { TimeseriesSeries as timeseries } from './vis_types/timeseries/series'; import { MetricSeries as metric } from './vis_types/metric/series'; @@ -79,30 +79,41 @@ export class Series extends Component { return Boolean(Component) ? ( - {visData => ( - - )} + {visData => { + const series = get(visData, `${panel.id}.series`, []); + const counter = {}; + const seriesQuantity = series.reduce((acc, value) => { + counter[value.seriesId] = counter[value.seriesId] + 1 || 1; + acc[value.seriesId] = counter[value.seriesId]; + return acc; + }, {}); + + return ( + + ); + }} ) : ( ); } @@ -96,4 +97,5 @@ Split.propTypes = { model: PropTypes.object, onChange: PropTypes.func, panel: PropTypes.object, + seriesQuantity: PropTypes.object, }; diff --git a/src/legacy/core_plugins/metrics/public/components/splits/terms.js b/src/legacy/core_plugins/metrics/public/components/splits/terms.js index 8eaef0754cad8..90196fb6fef61 100644 --- a/src/legacy/core_plugins/metrics/public/components/splits/terms.js +++ b/src/legacy/core_plugins/metrics/public/components/splits/terms.js @@ -23,6 +23,7 @@ import { get, find } from 'lodash'; import { GroupBySelect } from './group_by_select'; import { createTextHandler } from '../lib/create_text_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { isPercentDisabled } from '../lib/stacked'; import { FieldSelect } from '../aggs/field_select'; import { MetricSelect } from '../aggs/metric_select'; import { @@ -36,6 +37,7 @@ import { } from '@elastic/eui'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { FIELD_TYPES } from '../../../common/field_types'; +import { STACKED_OPTIONS } from '../../visualizations/constants'; const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' }; @@ -46,6 +48,7 @@ export const SplitByTermsUI = ({ model: seriesModel, fields, uiRestrictions, + seriesQuantity, }) => { const htmlId = htmlIdGenerator(); const handleTextChange = createTextHandler(onChange); @@ -86,6 +89,14 @@ export const SplitByTermsUI = ({ const selectedField = find(fields[indexPattern], ({ name }) => name === model.terms_field); const selectedFieldType = get(selectedField, 'type'); + if ( + seriesQuantity && + model.stacked === STACKED_OPTIONS.PERCENT && + isPercentDisabled(seriesQuantity[model.id]) + ) { + onChange({ ['stacked']: STACKED_OPTIONS.NONE }); + } + return (
@@ -218,6 +229,7 @@ SplitByTermsUI.propTypes = { indexPattern: PropTypes.string, fields: PropTypes.object, uiRestrictions: PropTypes.object, + seriesQuantity: PropTypes.object, }; export const SplitByTerms = injectI18n(SplitByTermsUI); diff --git a/src/legacy/core_plugins/metrics/public/components/splits/terms.test.js b/src/legacy/core_plugins/metrics/public/components/splits/terms.test.js index 0daa083816c50..4d322cd7b7e61 100644 --- a/src/legacy/core_plugins/metrics/public/components/splits/terms.test.js +++ b/src/legacy/core_plugins/metrics/public/components/splits/terms.test.js @@ -40,8 +40,12 @@ describe('src/legacy/core_plugins/metrics/public/components/splits/terms.test.js formatMessage: jest.fn(), }, model: { + id: 123, terms_field: 'OriginCityName', }, + seriesQuantity: { + id123: 123, + }, onChange: jest.fn(), indexPattern: 'kibana_sample_data_flights', fields: { diff --git a/src/legacy/core_plugins/metrics/public/components/svg/bomb_icon.js b/src/legacy/core_plugins/metrics/public/components/svg/bomb_icon.js new file mode 100644 index 0000000000000..865bd67ea9c35 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/svg/bomb_icon.js @@ -0,0 +1,26 @@ +/* + * 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'; + +export const bombIcon = () => ( + + + +); diff --git a/src/legacy/core_plugins/metrics/public/components/svg/fire_icon.js b/src/legacy/core_plugins/metrics/public/components/svg/fire_icon.js new file mode 100644 index 0000000000000..9ec45907d4636 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/components/svg/fire_icon.js @@ -0,0 +1,26 @@ +/* + * 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'; + +export const fireIcon = () => ( + + + +); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_editor.js b/src/legacy/core_plugins/metrics/public/components/vis_editor.js index cf8c4a02b9e58..d6cbdcd60d2cf 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_editor.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_editor.js @@ -27,7 +27,7 @@ import { VisEditorVisualization } from './vis_editor_visualization'; import { Visualization } from './visualization'; import { VisPicker } from './vis_picker'; import { PanelConfig } from './panel_config'; -import { brushHandler } from '../lib/create_brush_handler'; +import { createBrushHandler } from '../lib/create_brush_handler'; import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../common/extract_index_patterns'; @@ -51,7 +51,7 @@ export class VisEditor extends Component { visFields: props.visFields, extractedIndexPatterns: [''], }; - this.onBrush = brushHandler(props.vis.API.timeFilter); + this.onBrush = createBrushHandler(props.vis.API.timeFilter); this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); this.visData$ = this.visDataSubject.asObservable().pipe(share()); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_editor_visualization.js b/src/legacy/core_plugins/metrics/public/components/vis_editor_visualization.js index e313719b3dd99..fd399e66bb149 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_editor_visualization.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_editor_visualization.js @@ -31,7 +31,7 @@ import { } from './lib/get_interval'; import { PANEL_TYPES } from '../../common/panel_types'; -const MIN_CHART_HEIGHT = 250; +const MIN_CHART_HEIGHT = 300; class VisEditorVisualizationUI extends Component { constructor(props) { diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/_vis_types.scss b/src/legacy/core_plugins/metrics/public/components/vis_types/_vis_types.scss index f4f9230b32dc2..90c2007b1c94a 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/_vis_types.scss +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/_vis_types.scss @@ -3,4 +3,8 @@ flex-direction: column; flex: 1 1 100%; padding: $euiSizeS; + + .tvbVisTimeSeries { + overflow: hidden; + } } diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/vis.js index 5363152f7e60c..5d6bb55f33db6 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/gauge/vis.js @@ -20,9 +20,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import { visWithSplits } from '../../vis_with_splits'; -import { tickFormatter } from '../../lib/tick_formatter'; +import { createTickFormatter } from '../../lib/tick_formatter'; import _, { get, isUndefined, assign, includes } from 'lodash'; -import { Gauge } from '../../../visualizations/components/gauge'; +import { Gauge } from '../../../visualizations/views/gauge'; import { getLastValue } from '../../../../common/get_last_value'; function getColors(props) { @@ -54,7 +54,7 @@ function GaugeVisualization(props) { const seriesDef = model.series.find(s => includes(row.id, s.id)); const newProps = {}; if (seriesDef) { - newProps.formatter = tickFormatter( + newProps.formatter = createTickFormatter( seriesDef.formatter, seriesDef.value_template, props.getConfig diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/metric/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/metric/vis.js index 6e3a7544397c2..f463a4494a189 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/metric/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/metric/vis.js @@ -20,9 +20,9 @@ import PropTypes from 'prop-types'; import React from 'react'; import { visWithSplits } from '../../vis_with_splits'; -import { tickFormatter } from '../../lib/tick_formatter'; +import { createTickFormatter } from '../../lib/tick_formatter'; import _, { get, isUndefined, assign, includes, pick } from 'lodash'; -import { Metric } from '../../../visualizations/components/metric'; +import { Metric } from '../../../visualizations/views/metric'; import { getLastValue } from '../../../../common/get_last_value'; import { isBackgroundInverted } from '../../../../common/set_is_reversed'; @@ -54,7 +54,7 @@ function MetricVisualization(props) { const seriesDef = model.series.find(s => includes(row.id, s.id)); const newProps = {}; if (seriesDef) { - newProps.formatter = tickFormatter( + newProps.formatter = createTickFormatter( seriesDef.formatter, seriesDef.value_template, props.getConfig diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/table/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/table/vis.js index adb92d4d880cb..c9b02af3d37fe 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/table/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/table/vis.js @@ -21,7 +21,7 @@ import _, { isArray, last, get } from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { fieldFormats } from 'ui/registry/field_formats'; -import { tickFormatter } from '../../lib/tick_formatter'; +import { createTickFormatter } from '../../lib/tick_formatter'; import { calculateLabel } from '../../../../common/calculate_label'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; @@ -68,7 +68,7 @@ export class TableVis extends Component { .map(item => { const column = this.visibleSeries.find(c => c.id === item.id); if (!column) return null; - const formatter = tickFormatter( + const formatter = createTickFormatter( column.formatter, column.value_template, this.props.getConfig diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/config.js b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/config.js index 352b01a832694..2daa2470bc611 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/config.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/config.js @@ -41,6 +41,9 @@ import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { getDefaultQueryLanguage } from '../../lib/get_default_query_language'; import { QueryBarWrapper } from '../../query_bar_wrapper'; +import { isPercentDisabled } from '../../lib/stacked'; +import { STACKED_OPTIONS } from '../../../visualizations/constants/chart'; + export const TimeseriesConfig = injectI18n(function(props) { const handleSelectChange = createSelectHandler(props.onChange); const handleTextChange = createTextHandler(props.onChange); @@ -53,7 +56,7 @@ export const TimeseriesConfig = injectI18n(function(props) { split_color_mode: 'gradient', axis_min: '', axis_max: '', - stacked: 'none', + stacked: STACKED_OPTIONS.NONE, steps: 0, }; const model = { ...defaults, ...props.model }; @@ -62,22 +65,23 @@ export const TimeseriesConfig = injectI18n(function(props) { const stackedOptions = [ { label: intl.formatMessage({ id: 'tsvb.timeSeries.noneLabel', defaultMessage: 'None' }), - value: 'none', + value: STACKED_OPTIONS.NONE, }, { label: intl.formatMessage({ id: 'tsvb.timeSeries.stackedLabel', defaultMessage: 'Stacked' }), - value: 'stacked', + value: STACKED_OPTIONS.STACKED, }, { label: intl.formatMessage({ id: 'tsvb.timeSeries.stackedWithinSeriesLabel', defaultMessage: 'Stacked within series', }), - value: 'stacked_within_series', + value: STACKED_OPTIONS.STACKED_WITHIN_SERIES, }, { label: intl.formatMessage({ id: 'tsvb.timeSeries.percentLabel', defaultMessage: 'Percent' }), - value: 'percent', + value: STACKED_OPTIONS.PERCENT, + disabled: isPercentDisabled(props.seriesQuantity[model.id]), }, ]; const selectedStackedOption = stackedOptions.find(option => { @@ -130,6 +134,7 @@ export const TimeseriesConfig = injectI18n(function(props) { }); let type; + if (model.chart_type === 'line') { type = ( @@ -282,7 +287,7 @@ export const TimeseriesConfig = injectI18n(function(props) { } > @@ -529,4 +534,5 @@ TimeseriesConfig.propTypes = { model: PropTypes.object, onChange: PropTypes.func, indexPatternForQuery: PropTypes.string, + seriesQuantity: PropTypes.object, }; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js index 43e1ac9f0eda5..c0a0fb744ce39 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js @@ -51,6 +51,7 @@ const TimeseriesSeriesUI = injectI18n(function(props) { intl, name, uiRestrictions, + seriesQuantity, } = props; const defaults = { @@ -87,6 +88,7 @@ const TimeseriesSeriesUI = injectI18n(function(props) { panel={panel} model={model} uiRestrictions={uiRestrictions} + seriesQuantity={seriesQuantity} />
@@ -98,6 +100,7 @@ const TimeseriesSeriesUI = injectI18n(function(props) { model={model} onChange={props.onChange} indexPatternForQuery={props.indexPatternForQuery} + seriesQuantity={seriesQuantity} /> ); } @@ -211,6 +214,7 @@ TimeseriesSeriesUI.propTypes = { uiRestrictions: PropTypes.object, dragHandleProps: PropTypes.object, indexPatternForQuery: PropTypes.string, + seriesQuantity: PropTypes.object, }; export const TimeseriesSeries = injectI18n(TimeseriesSeriesUI); diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/vis.js index 12f2d4ef66e04..982aca8d3b813 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/vis.js @@ -19,35 +19,90 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import reactCSS from 'reactcss'; + +import { startsWith, get, cloneDeep, map } from 'lodash'; import { toastNotifications } from 'ui/notify'; +import { htmlIdGenerator } from '@elastic/eui'; +import { ScaleType } from '@elastic/charts'; -import { tickFormatter } from '../../lib/tick_formatter'; -import _ from 'lodash'; -import { Timeseries } from '../../../visualizations/components/timeseries'; +import { createTickFormatter } from '../../lib/tick_formatter'; +import { TimeSeries } from '../../../visualizations/views/timeseries'; import { MarkdownSimple } from '../../../../../kibana_react/public'; import { replaceVars } from '../../lib/replace_vars'; import { getAxisLabelString } from '../../lib/get_axis_label_string'; import { getInterval } from '../../lib/get_interval'; +import { areFieldsDifferent } from '../../lib/charts'; import { createXaxisFormatter } from '../../lib/create_xaxis_formatter'; - -function hasSeparateAxis(row) { - return row.separate_axis; -} +import { isBackgroundDark } from '../../../../common/set_is_reversed'; +import { STACKED_OPTIONS } from '../../../visualizations/constants'; export class TimeseriesVisualization extends Component { - getInterval = () => { - const { visData, model } = this.props; - - return getInterval(visData, model); + static propTypes = { + model: PropTypes.object, + onBrush: PropTypes.func, + visData: PropTypes.object, + dateFormat: PropTypes.string, + getConfig: PropTypes.func, }; - xaxisFormatter = val => { + xAxisFormatter = interval => val => { const { scaledDataFormat, dateFormat } = this.props.visData; - if (!scaledDataFormat || !dateFormat) return val; - const formatter = createXaxisFormatter(this.getInterval(), scaledDataFormat, dateFormat); + + if (!scaledDataFormat || !dateFormat) { + return val; + } + + const formatter = createXaxisFormatter(interval, scaledDataFormat, dateFormat); + return formatter(val); }; + yAxisStackedByPercentFormatter = val => { + const n = Number(val) * 100; + + return `${(Number.isNaN(n) ? 0 : n).toFixed(0)}%`; + }; + + applyDocTo = template => doc => { + const vars = replaceVars(template, null, doc); + + if (vars instanceof Error) { + this.showToastNotification = vars.error.caused_by; + + return template; + } + + return vars; + }; + + static getYAxisDomain = model => { + const axisMin = get(model, 'axis_min', '').toString(); + const axisMax = get(model, 'axis_max', '').toString(); + + return { + min: axisMin.length ? Number(axisMin) : undefined, + max: axisMax.length ? Number(axisMax) : undefined, + }; + }; + + static addYAxis = (yAxis, { id, groupId, position, tickFormatter, domain, hide }) => { + yAxis.push({ + id, + groupId, + position, + tickFormatter, + domain, + hide, + }); + }; + + static getAxisScaleType = model => + get(model, 'axis_scale') === 'log' ? ScaleType.Log : ScaleType.Linear; + + static getTickFormatter = (model, getConfig) => + createTickFormatter(get(model, 'formatter'), get(model, 'value_template'), getConfig); + componentDidUpdate() { if ( this.showToastNotification && @@ -71,181 +126,129 @@ export class TimeseriesVisualization extends Component { } } - render() { - const { backgroundColor, model, visData } = this.props; - const series = _.get(visData, `${model.id}.series`, []); - let annotations; + prepareAnnotations = () => { + const { model, visData } = this.props; + + return map(model.annotations, ({ id, color, icon, template }) => { + const annotationData = get(visData, `${model.id}.annotations.${id}`, []); + const applyDocToTemplate = this.applyDocTo(template); + + return { + id, + color, + icon, + data: annotationData.map(({ docs, ...rest }) => ({ + ...rest, + docs: docs.map(applyDocToTemplate), + })), + }; + }); + }; - this.showToastNotification = null; + render() { + const { model, visData, onBrush } = this.props; + const styles = reactCSS({ + default: { + tvbVis: { + backgroundColor: get(model, 'background_color'), + }, + }, + }); + const series = get(visData, `${model.id}.series`, []); + const interval = getInterval(visData, model); + const yAxisIdGenerator = htmlIdGenerator('yaxis'); + const mainAxisGroupId = yAxisIdGenerator('main_group'); - if (model.annotations && Array.isArray(model.annotations)) { - annotations = model.annotations.map(annotation => { - const data = _.get(visData, `${model.id}.annotations.${annotation.id}`, []).map(item => [ - item.key, - item.docs, - ]); - return { - id: annotation.id, - color: annotation.color, - icon: annotation.icon, - series: data.map(s => { - return [ - s[0], - s[1].map(doc => { - const vars = replaceVars(annotation.template, null, doc); - - if (vars instanceof Error) { - this.showToastNotification = vars.error.caused_by; - - return annotation.template; - } - - return vars; - }), - ]; - }), - }; - }); - } - const seriesModel = model.series.map(s => _.cloneDeep(s)); + const seriesModel = model.series.filter(s => !s.hidden).map(s => cloneDeep(s)); + const enableHistogramMode = areFieldsDifferent('chart_type')(seriesModel); const firstSeries = seriesModel.find(s => s.formatter && !s.separate_axis); - const formatter = tickFormatter( - _.get(firstSeries, 'formatter'), - _.get(firstSeries, 'value_template'), + + const mainAxisScaleType = TimeseriesVisualization.getAxisScaleType(model); + const mainAxisDomain = TimeseriesVisualization.getYAxisDomain(model); + const tickFormatter = TimeseriesVisualization.getTickFormatter( + firstSeries, this.props.getConfig ); + const yAxis = []; + let mainDomainAdded = false; - const mainAxis = { - position: model.axis_position, - tickFormatter: formatter, - axisFormatter: _.get(firstSeries, 'formatter', 'number'), - axisFormatterTemplate: _.get(firstSeries, 'value_template'), - }; - - if (model.axis_min) mainAxis.min = model.axis_min; - if (model.axis_max) mainAxis.max = model.axis_max; - if (model.axis_scale === 'log') { - mainAxis.mode = 'log'; - mainAxis.transform = value => (value > 0 ? Math.log(value) / Math.LN10 : null); - mainAxis.inverseTransform = value => Math.pow(10, value); - } + this.showToastNotification = null; - const yaxes = [mainAxis]; + seriesModel.forEach(seriesGroup => { + const isStackedWithinSeries = seriesGroup.stacked === STACKED_OPTIONS.STACKED_WITHIN_SERIES; + const hasSeparateAxis = Boolean(seriesGroup.separate_axis); + const groupId = hasSeparateAxis || isStackedWithinSeries ? seriesGroup.id : mainAxisGroupId; + const domain = hasSeparateAxis + ? TimeseriesVisualization.getYAxisDomain(seriesGroup) + : undefined; + const isCustomDomain = groupId !== mainAxisGroupId; + const seriesGroupTickFormatter = TimeseriesVisualization.getTickFormatter( + seriesGroup, + this.props.getConfig + ); + const yScaleType = hasSeparateAxis + ? TimeseriesVisualization.getAxisScaleType(seriesGroup) + : mainAxisScaleType; + + if (seriesGroup.stacked === STACKED_OPTIONS.PERCENT) { + seriesGroup.separate_axis = true; + seriesGroup.axisFormatter = 'percent'; + seriesGroup.axis_min = seriesGroup.axis_min || 0; + seriesGroup.axis_max = seriesGroup.axis_max || 1; + seriesGroup.axis_position = model.axis_position; + } - seriesModel.forEach(s => { series - .filter(r => _.startsWith(r.id, s.id)) - .forEach( - r => - (r.tickFormatter = tickFormatter(s.formatter, s.value_template, this.props.getConfig)) - ); - - if (s.hide_in_legend) { - series.filter(r => _.startsWith(r.id, s.id)).forEach(r => delete r.label); - } - if (s.stacked !== 'none') { - series - .filter(r => _.startsWith(r.id, s.id)) - .forEach(row => { - row.data = row.data.map(point => { - if (!point[1]) return [point[0], 0]; - return point; - }); - }); - } - if (s.stacked === 'percent') { - s.separate_axis = true; - s.axisFormatter = 'percent'; - s.axis_min = 0; - s.axis_max = 1; - s.axis_position = model.axis_position; - const seriesData = series.filter(r => _.startsWith(r.id, s.id)); - const first = seriesData[0]; - if (first) { - first.data.forEach((row, index) => { - const rowSum = seriesData.reduce((acc, item) => { - return item.data[index][1] + acc; - }, 0); - seriesData.forEach(item => { - item.data[index][1] = (rowSum && item.data[index][1] / rowSum) || 0; - }); - }); - } + .filter(r => startsWith(r.id, seriesGroup.id)) + .forEach(seriesDataRow => { + seriesDataRow.tickFormatter = seriesGroupTickFormatter; + seriesDataRow.groupId = groupId; + seriesDataRow.yScaleType = yScaleType; + seriesDataRow.hideInLegend = Boolean(seriesGroup.hide_in_legend); + seriesDataRow.useDefaultGroupDomain = !isCustomDomain; + }); + + if (isCustomDomain) { + TimeseriesVisualization.addYAxis(yAxis, { + domain, + groupId, + id: yAxisIdGenerator(seriesGroup.id), + position: seriesGroup.axis_position, + hide: isStackedWithinSeries, + tickFormatter: + seriesGroup.stacked === STACKED_OPTIONS.PERCENT + ? this.yAxisStackedByPercentFormatter + : seriesGroupTickFormatter, + }); + } else if (!mainDomainAdded) { + TimeseriesVisualization.addYAxis(yAxis, { + tickFormatter, + id: yAxisIdGenerator('main'), + groupId: mainAxisGroupId, + position: model.axis_position, + domain: mainAxisDomain, + }); + + mainDomainAdded = true; } }); - const interval = this.getInterval(); - - let axisCount = 1; - if (seriesModel.some(hasSeparateAxis)) { - seriesModel.forEach(row => { - if (row.separate_axis) { - axisCount++; - - const formatter = tickFormatter(row.formatter, row.value_template, this.props.getConfig); - - const yaxis = { - alignTicksWithAxis: 1, - position: row.axis_position, - tickFormatter: formatter, - axisFormatter: row.axis_formatter, - axisFormatterTemplate: row.value_template, - }; - - if (row.axis_min != null) yaxis.min = row.axis_min; - if (row.axis_max != null) yaxis.max = row.axis_max; - - yaxes.push(yaxis); - - // Assign axis and formatter to each series - series - .filter(r => _.startsWith(r.id, row.id)) - .forEach(r => { - r.yaxis = axisCount; - }); - } - }); - } - - const panelBackgroundColor = model.background_color || backgroundColor; - const style = { backgroundColor: panelBackgroundColor }; - - const params = { - dateFormat: this.props.dateFormat, - crosshair: true, - tickFormatter: formatter, - legendPosition: model.legend_position || 'right', - backgroundColor: panelBackgroundColor, - series, - annotations, - yaxes, - showGrid: Boolean(model.show_grid), - legend: Boolean(model.show_legend), - xAxisFormatter: this.xaxisFormatter, - onBrush: ranges => { - if (this.props.onBrush) this.props.onBrush(ranges); - }, - }; - - if (interval) { - params.xaxisLabel = getAxisLabelString(interval); - } - return ( -
- +
+
); } } - -TimeseriesVisualization.propTypes = { - backgroundColor: PropTypes.string, - className: PropTypes.string, - model: PropTypes.object, - onBrush: PropTypes.func, - onChange: PropTypes.func, - visData: PropTypes.object, - dateFormat: PropTypes.string, - getConfig: PropTypes.func, -}; diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/vis.js b/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/vis.js index 57de7f018f134..7d09f33acdecc 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/vis.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/top_n/vis.js @@ -17,8 +17,8 @@ * under the License. */ -import { tickFormatter } from '../../lib/tick_formatter'; -import { TopN } from '../../../visualizations/components/top_n'; +import { createTickFormatter } from '../../lib/tick_formatter'; +import { TopN } from '../../../visualizations/views/top_n'; import { getLastValue } from '../../../../common/get_last_value'; import { isBackgroundInverted } from '../../../../common/set_is_reversed'; import { replaceVars } from '../../lib/replace_vars'; @@ -54,7 +54,7 @@ export function TopNVisualization(props) { const id = first(item.id.split(/:/)); const seriesConfig = model.series.find(s => s.id === id); if (seriesConfig) { - const formatter = tickFormatter( + const tickFormatter = createTickFormatter( seriesConfig.formatter, seriesConfig.value_template, props.getConfig @@ -73,7 +73,7 @@ export function TopNVisualization(props) { return { ...item, color, - tickFormatter: formatter, + tickFormatter, }; } return item; diff --git a/src/legacy/core_plugins/metrics/public/index.scss b/src/legacy/core_plugins/metrics/public/index.scss index 5083c5156be23..86fbfb52dbe64 100644 --- a/src/legacy/core_plugins/metrics/public/index.scss +++ b/src/legacy/core_plugins/metrics/public/index.scss @@ -17,4 +17,4 @@ @import './components/index'; // Visualizations -@import './visualizations/components/index'; +@import './visualizations/views/index'; diff --git a/src/legacy/core_plugins/metrics/public/legacy.ts b/src/legacy/core_plugins/metrics/public/legacy.ts index 49420e4b51273..8dc24503772db 100644 --- a/src/legacy/core_plugins/metrics/public/legacy.ts +++ b/src/legacy/core_plugins/metrics/public/legacy.ts @@ -20,12 +20,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { visualizations } from '../../visualizations/public'; +import { setup as setupVisualizations } from '../../visualizations/public/np_ready/public/legacy'; import { MetricsPluginSetupDependencies } from './plugin'; import { plugin } from '.'; const plugins: Readonly = { - visualizations, + visualizations: setupVisualizations, data: npSetup.plugins.data, }; diff --git a/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.js b/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.js index 4bd3bd641e9a6..0a7b10ff1aabb 100644 --- a/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.js +++ b/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.js @@ -19,10 +19,12 @@ import moment from 'moment'; -export const brushHandler = timefilter => ranges => { +const TIME_MODE = 'absolute'; + +export const createBrushHandler = timefilter => (from, to) => { timefilter.setTime({ - from: moment(ranges.xaxis.from).toISOString(), - to: moment(ranges.xaxis.to).toISOString(), - mode: 'absolute', + from: moment(from).toISOString(), + to: moment(to).toISOString(), + mode: TIME_MODE, }); }; diff --git a/src/legacy/core_plugins/metrics/public/lib/__tests__/create_brush_handler.test.js b/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.test.js similarity index 65% rename from src/legacy/core_plugins/metrics/public/lib/__tests__/create_brush_handler.test.js rename to src/legacy/core_plugins/metrics/public/lib/create_brush_handler.test.js index f78ed52ea52a4..76d88b6b8dfb9 100644 --- a/src/legacy/core_plugins/metrics/public/lib/__tests__/create_brush_handler.test.js +++ b/src/legacy/core_plugins/metrics/public/lib/create_brush_handler.test.js @@ -17,14 +17,12 @@ * under the License. */ -import { brushHandler } from '../create_brush_handler'; +import { createBrushHandler } from './create_brush_handler'; import moment from 'moment'; -import { expect } from 'chai'; describe('brushHandler', () => { let mockTimefilter; let onBrush; - let range; beforeEach(() => { mockTimefilter = { @@ -33,14 +31,15 @@ describe('brushHandler', () => { this.time = time; }, }; - onBrush = brushHandler(mockTimefilter); + onBrush = createBrushHandler(mockTimefilter); }); - test('returns brushHandler() that updates timefilter', () => { - range = { xaxis: { from: '2017-01-01T00:00:00Z', to: '2017-01-01T00:10:00Z' } }; - onBrush(range); - expect(mockTimefilter.time.from).to.equal(moment(range.xaxis.from).toISOString()); - expect(mockTimefilter.time.to).to.equal(moment(range.xaxis.to).toISOString()); - expect(mockTimefilter.time.mode).to.equal('absolute'); + it('returns brushHandler() that updates timefilter', () => { + const from = '2017-01-01T00:00:00Z'; + const to = '2017-01-01T00:10:00Z'; + onBrush(from, to); + expect(mockTimefilter.time.from).toEqual(moment(from).toISOString()); + expect(mockTimefilter.time.to).toEqual(moment(to).toISOString()); + expect(mockTimefilter.time.mode).toEqual('absolute'); }); }); diff --git a/src/legacy/core_plugins/metrics/public/metrics_type.ts b/src/legacy/core_plugins/metrics/public/metrics_type.ts index 35182fbe25265..00f54a90b3b0f 100644 --- a/src/legacy/core_plugins/metrics/public/metrics_type.ts +++ b/src/legacy/core_plugins/metrics/public/metrics_type.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; -import { visFactory } from '../../visualizations/public'; +import { visFactory } from '../../visualizations/public/np_ready/public'; // @ts-ignore import { createMetricsRequestHandler } from './request_handler'; diff --git a/src/legacy/core_plugins/metrics/public/plugin.ts b/src/legacy/core_plugins/metrics/public/plugin.ts index e1eca37970604..5c103a3ae4b08 100644 --- a/src/legacy/core_plugins/metrics/public/plugin.ts +++ b/src/legacy/core_plugins/metrics/public/plugin.ts @@ -18,7 +18,7 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { createMetricsFn } from './metrics_fn'; import { createMetricsTypeDefinition } from './metrics_type'; @@ -39,7 +39,7 @@ export class MetricsPlugin implements Plugin, void> { public async setup(core: CoreSetup, { data, visualizations }: MetricsPluginSetupDependencies) { data.expressions.registerFunction(createMetricsFn); - visualizations.types.VisTypesRegistryProvider.register(createMetricsTypeDefinition); + visualizations.types.registerVisualization(createMetricsTypeDefinition); } public start(core: CoreStart) { diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/_legend.scss b/src/legacy/core_plugins/metrics/public/visualizations/components/_legend.scss deleted file mode 100644 index 429e7c682f21e..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/_legend.scss +++ /dev/null @@ -1,92 +0,0 @@ -// LEGEND - -.tvbLegend { - @include euiFontSizeXS; - display: flex; - width: 200px; - padding: $euiSizeXS 0; - overflow: auto; -} - -.tvbLegend__toggle { - align-self: flex-start; - color: $tvbValueColor; - - .tvbVisTimeSeries--reversed & { - color: $tvbValueColorReversed; - } -} - -.tvbLegend__series { - flex-grow: 1; -} - -.tvbLegend__item { - cursor: pointer; - padding: $euiSizeXS; - border-bottom: 1px solid $tvbLineColor; - display: flex; - max-width: 170px; - - &.disabled { - opacity: .5; - } - - &:first-child { - border-top: 1px solid $tvbLineColor; - } - - .tvbVisTimeSeries--reversed & { - border-color: $tvbLineColorReversed; - } -} - -.tvbLegend__button { - text-align: left; - display: flex; - width: 100%; -} - -.tvbLegend__itemLabel { - @include euiTextTruncate; - flex-grow: 1; - - span { - color: $euiTextColor; - margin-left: $euiSizeXS; - - .tvbVisTimeSeries--reversed & { - color: $tvbTextColorReversed; - } - } -} - -.tvbLegend__itemValue { - font-weight: $euiFontWeightSemiBold; - color: $tvbValueColor; - margin-left: $euiSizeXS; - - .tvbVisTimeSeries--reversed & { - color: $tvbValueColorReversed; - } -} - -.tvbLegend--horizontal { - width: auto; - display: flex; - - .tvbLegend__series { - display: flex; - flex-wrap: wrap; - } - - .tvbLegend__item { - max-width: inherit; - margin-right: $euiSizeM; - border: none; - } - - .tvbLegend__itemLabel { - flex: 0 1 auto; - } -} diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/_timeseries_chart.scss b/src/legacy/core_plugins/metrics/public/visualizations/components/_timeseries_chart.scss deleted file mode 100644 index 7bda2cc78e05b..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/_timeseries_chart.scss +++ /dev/null @@ -1,132 +0,0 @@ -@import '@elastic/eui/src/components/tool_tip/variables'; -@import '@elastic/eui/src/components/tool_tip/mixins'; - -.tvbVisTimeSeries { - position: relative; - display: flex; - flex-direction: column; - flex: 1 0 auto; -} - -.tvbVisTimeSeries__content { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: flex; - flex: 1 0 auto; - // TODO: Remove once tooltips are portaled - overflow: visible; // Ensures the tooltip doesn't cause scrollbars -} - -.tvbVisTimeSeries__visualization { - cursor: crosshair; - display: flex; - flex-direction: column; - flex: 1 0 auto; - position: relative; - - > .tvbVisTimeSeries__container { - min-width: 1px; - width: 100%; - height: 100%; - } -} - -.tvbVisTimeSeries__container { - @include euiFontSizeXS; - position: relative; - display: flex; - flex-direction: column; - flex: 1 0 auto; -} - -.tvbVisTimeSeries__axisLabel { - font-weight: $euiFontWeightBold; - color: $tvbTextColor; - text-align: center; - min-height: 1.2em; - - &.tvbVisTimeSeries__axisLabel--reversed { - color: $tvbTextColorReversed; - } -} - -// TOOLTIP - -// EUITODO: Use EuiTooltip or somehow portal the current one -.tvbTooltip__container { - pointer-events: none; - position: absolute; - z-index: $euiZLevel9; - display: flex; - align-items: center; - padding: 0 $euiSizeS; - transform: translate(0, -50%); -} - -.tvbTooltip__container--right { - flex-direction: row-reverse; -} - -.tvbTooltip { - @include euiToolTipStyle; - @include euiFontSizeXS; - padding: $euiSizeS; -} - -.tvbTooltip__caret { - $tempArrowSize: $euiSizeM; - width: $tempArrowSize; - height: $tempArrowSize; - border-radius: $euiBorderRadius / 2; - background-color: tintOrShade($euiColorFullShade, 25%, 90%); - transform-origin: center; - transform: rotateZ(45deg); - - .tvbTooltip__container--left & { - margin-right: (($tempArrowSize/2) + 1px) * -1; - } - - .tvbTooltip__container--right & { - margin-left: (($tempArrowSize/2) + 1px) * -1; - } -} - -.tvbTooltip__item { - display: flex; -} - -/** - * 1. Ensure tvbTooltip__label text wraps nicely. - * 2. Create consistent space between the dot icon and the label. - */ -.tvbTooltip__labelContainer { - display: flex; - flex-wrap: wrap; - flex-grow: 1; - min-width: 1px; /* 1 */ - margin-left: $euiSizeXS; /* 2 */ -} - -/** - * 1. Ensure text wraps nicely. - */ -.tvbTooltip__label { - flex-grow: 1; - margin-right: $euiSizeXS; - word-wrap: break-word; /* 1 */ - overflow-wrap: break-word; /* 1 */ - min-width: 1px; /* 1 */ -} - -.tvbTooltip__icon, -.tvbTooltip__value { - flex-shrink: 0; -} - -.tvbTooltip__timestamp { - color: transparentize($euiColorGhost, .3); - white-space: nowrap; -} diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/flot_chart.js b/src/legacy/core_plugins/metrics/public/visualizations/components/flot_chart.js deleted file mode 100644 index bb900ee3f7b38..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/flot_chart.js +++ /dev/null @@ -1,346 +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, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { findDOMNode } from 'react-dom'; -import _ from 'lodash'; -import $ from 'ui/flot-charts'; -import { eventBus } from '../lib/events'; -import { Resize } from './resize'; -import { calculateBarWidth } from '../lib/calculate_bar_width'; -import { calculateFillColor } from '../lib/calculate_fill_color'; -import { COLORS } from '../lib/colors'; - -export class FlotChart extends Component { - constructor(props) { - super(props); - this.handleResize = this.handleResize.bind(this); - } - - shouldComponentUpdate(props) { - if (!this.plot) return true; - if (props.reversed !== this.props.reversed) { - return true; - } - - // if the grid changes we need to re-render - if (props.showGrid !== this.props.showGrid) return true; - - if (props.yaxes && this.props.yaxes) { - // We need to rerender if the axis change - const valuesChanged = props.yaxes.some((axis, i) => { - if (this.props.yaxes[i]) { - return ( - axis.position !== this.props.yaxes[i].position || - axis.max !== this.props.yaxes[i].max || - axis.min !== this.props.yaxes[i].min || - axis.axisFormatter !== this.props.yaxes[i].axisFormatter || - axis.mode !== this.props.yaxes[i].mode || - axis.axisFormatterTemplate !== this.props.yaxes[i].axisFormatterTemplate - ); - } - }); - if (props.yaxes.length !== this.props.yaxes.length || valuesChanged) { - return true; - } - } - return false; - } - - shutdownChart() { - if (!this.plot) return; - $(this.target).off('plothover', this.props.plothover); - if (this.props.onMouseOver) $(this.target).off('plothover', this.handleMouseOver); - if (this.props.onMouseLeave) $(this.target).off('mouseleave', this.handleMouseLeave); - if (this.props.onBrush) $(this.target).off('plotselected', this.brushChart); - this.plot.shutdown(); - if (this.props.crosshair) { - $(this.target).off('plothover', this.handlePlotover); - eventBus.off('thorPlotover', this.handleThorPlotover); - eventBus.off('thorPlotleave', this.handleThorPlotleave); - } - } - - componentWillUnmount() { - this.shutdownChart(); - } - - filterByShow(show) { - if (show) { - return metric => { - return show.some(id => _.startsWith(id, metric.id)); - }; - } - return () => true; - } - - componentWillReceiveProps(newProps) { - if (this.plot) { - const { series } = newProps; - const options = this.plot.getOptions(); - _.set(options, 'series.bars.barWidth', calculateBarWidth(series)); - _.set(options, 'xaxes[0].ticks', this.calculateTicks()); - this.plot.setData(this.calculateData(series, newProps.show)); - this.plot.setupGrid(); - this.plot.draw(); - if (!_.isEqual(this.props.series, newProps.series)) this.handleDraw(this.plot); - } else { - this.renderChart(); - } - } - - componentDidMount() { - this.renderChart(); - } - - componentDidUpdate() { - this.shutdownChart(); - this.renderChart(); - } - - calculateData(data, show) { - return _(data) - .filter(this.filterByShow(show)) - .map(set => { - if (_.isPlainObject(set)) { - return { - ...set, - lines: this.computeColor(set.lines, set.color), - bars: this.computeColor(set.bars, set.color), - }; - } - return { - color: '#990000', - data: set, - }; - }) - .reverse() - .value(); - } - - computeColor(style, color) { - if (style && style.show) { - const { fill, fillColor } = calculateFillColor(color, style.fill); - return { - ...style, - fill, - fillColor, - }; - } - return style; - } - - handleDraw(plot) { - if (this.props.onDraw) this.props.onDraw(plot); - } - - getOptions(props) { - const yaxes = props.yaxes || [{}]; - - const lineColor = COLORS.lineColor; - const textColor = props.reversed ? COLORS.textColorReversed : COLORS.textColor; - - const borderWidth = { bottom: 1, top: 0, left: 0, right: 0 }; - - if (yaxes.some(y => y.position === 'left')) borderWidth.left = 1; - if (yaxes.some(y => y.position === 'right')) borderWidth.right = 1; - - if (props.showGrid) { - borderWidth.top = 1; - borderWidth.left = 1; - borderWidth.right = 1; - } - - const opts = { - legend: { show: false }, - yaxes: yaxes.map(axis => { - axis.tickLength = props.showGrid ? null : 0; - return axis; - }), - yaxis: { - color: lineColor, - font: { color: textColor, size: 11 }, - tickFormatter: props.tickFormatter, - }, - xaxis: { - tickLength: props.showGrid ? null : 0, - color: lineColor, - timezone: 'browser', - mode: 'time', - font: { color: textColor, size: 11 }, - ticks: this.calculateTicks(), - }, - series: { - shadowSize: 0, - }, - grid: { - margin: 0, - borderWidth, - borderColor: lineColor, - hoverable: true, - mouseActiveRadius: 200, - }, - }; - - if (props.crosshair) { - _.set(opts, 'crosshair', { - mode: 'x', - color: '#C66', - lineWidth: 1, - }); - } - - if (props.onBrush) { - _.set(opts, 'selection', { mode: 'x', color: textColor }); - } - - if (props.xAxisFormatter) { - _.set(opts, 'xaxis.tickFormatter', props.xAxisFormatter); - } - - _.set(opts, 'series.bars.barWidth', calculateBarWidth(props.series)); - return _.assign(opts, props.options); - } - - calculateTicks() { - const sample = this.props.xAxisFormatter(new Date()); - const tickLetterWidth = 7; - const tickPadding = 45; - const ticks = Math.floor( - this.target.clientWidth / (sample.length * tickLetterWidth + tickPadding) - ); - return ticks; - } - - handleResize() { - const resize = findDOMNode(this.resize); - if (!this.rendered) { - this.renderChart(); - return; - } - - if (resize && resize.clientHeight > 0 && resize.clientHeight > 0) { - if (!this.plot) return; - const options = this.plot.getOptions(); - _.set(options, 'xaxes[0].ticks', this.calculateTicks()); - this.plot.resize(); - this.plot.setupGrid(); - this.plot.draw(); - this.handleDraw(this.plot); - } - } - - renderChart() { - const resize = findDOMNode(this.resize); - - if (resize.clientWidth > 0 && resize.clientHeight > 0) { - this.rendered = true; - const { series } = this.props; - const data = this.calculateData(series, this.props.show); - - this.plot = $.plot(this.target, data, this.getOptions(this.props)); - this.handleDraw(this.plot); - - _.defer(() => this.handleResize()); - - this.handleMouseOver = (...args) => { - if (this.props.onMouseOver) this.props.onMouseOver(...args, this.plot); - }; - - this.handleMouseLeave = (...args) => { - if (this.props.onMouseLeave) this.props.onMouseLeave(...args, this.plot); - }; - - $(this.target).on('plothover', this.handleMouseOver); - $(this.target).on('mouseleave', this.handleMouseLeave); - - if (this.props.crosshair) { - this.handleThorPlotover = (e, pos, item, originalPlot) => { - if (this.plot !== originalPlot) { - this.plot.setCrosshair({ x: _.get(pos, 'x') }); - this.props.plothover(e, pos, item); - } - }; - - this.handlePlotover = (e, pos, item) => - eventBus.trigger('thorPlotover', [pos, item, this.plot]); - this.handlePlotleave = () => eventBus.trigger('thorPlotleave'); - this.handleThorPlotleave = e => { - if (this.plot) this.plot.clearCrosshair(); - if (this.props.plothover) this.props.plothover(e); - }; - - $(this.target).on('plothover', this.handlePlotover); - $(this.target).on('mouseleave', this.handlePlotleave); - eventBus.on('thorPlotover', this.handleThorPlotover); - eventBus.on('thorPlotleave', this.handleThorPlotleave); - } - - if (_.isFunction(this.props.plothover)) { - $(this.target).bind('plothover', this.props.plothover); - } - - $(this.target).on('mouseleave', () => { - eventBus.trigger('thorPlotleave'); - }); - - if (_.isFunction(this.props.onBrush)) { - this.brushChart = (e, ranges) => { - this.props.onBrush(ranges); - this.plot.clearSelection(); - }; - - $(this.target).on('plotselected', this.brushChart); - } - } - } - - render() { - return ( - (this.resize = el)} - className="tvbVisTimeSeries__container" - > -
(this.target = el)} className="tvbVisTimeSeries__container" /> - - ); - } -} - -FlotChart.defaultProps = { - showGrid: true, -}; - -FlotChart.propTypes = { - crosshair: PropTypes.bool, - onBrush: PropTypes.func, - onPlotCreate: PropTypes.func, - onMouseOver: PropTypes.func, - onMouseLeave: PropTypes.func, - options: PropTypes.object, - plothover: PropTypes.func, - reversed: PropTypes.bool, - series: PropTypes.array, - show: PropTypes.array, - tickFormatter: PropTypes.func, - showGrid: PropTypes.bool, - yaxes: PropTypes.array, -}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/horizontal_legend.js b/src/legacy/core_plugins/metrics/public/visualizations/components/horizontal_legend.js deleted file mode 100644 index 3bdb904d8d9c1..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/horizontal_legend.js +++ /dev/null @@ -1,80 +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 PropTypes from 'prop-types'; -import React from 'react'; -import { createLegendSeries } from '../lib/create_legend_series'; -import reactcss from 'reactcss'; -import { htmlIdGenerator, EuiButtonIcon } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; - -export const HorizontalLegend = injectI18n(function(props) { - const rows = props.series.map(createLegendSeries(props)); - const htmlId = htmlIdGenerator(); - const styles = reactcss( - { - hideLegend: { - legend: { - display: 'none', - }, - }, - }, - { hideLegend: !props.showLegend } - ); - - let legendToggleIcon = 'arrowDown'; - if (!props.showLegend) { - legendToggleIcon = 'arrowUp'; - } - return ( -
- -
- {rows} -
-
- ); -}); - -HorizontalLegend.propTypes = { - legendPosition: PropTypes.string, - onClick: PropTypes.func, - onToggle: PropTypes.func, - series: PropTypes.array, - showLegend: PropTypes.bool, - seriesValues: PropTypes.object, - seriesFilter: PropTypes.array, - tickFormatter: PropTypes.func, -}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/resize.js b/src/legacy/core_plugins/metrics/public/visualizations/components/resize.js deleted file mode 100644 index e6aa9831cf1a8..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/resize.js +++ /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 PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { findDOMNode } from 'react-dom'; - -export class Resize extends Component { - constructor(props) { - super(props); - this.state = {}; - this.handleResize = this.handleResize.bind(this); - } - - checkSize() { - const el = findDOMNode(this.el); - if (!el) return; - this.timeout = setTimeout(() => { - const { currentHeight, currentWidth } = this.state; - if ( - currentHeight !== el.parentNode.clientHeight || - currentWidth !== el.parentNode.clientWidth - ) { - this.setState({ - currentWidth: el.parentNode.clientWidth, - currentHeight: el.parentNode.clientHeight, - }); - this.handleResize(); - } - clearTimeout(this.timeout); - this.checkSize(); - }, this.props.frequency); - } - - componentDidMount() { - this.checkSize(); - } - - componentWillUnmount() { - clearTimeout(this.timeout); - } - - handleResize() { - if (this.props.onResize) this.props.onResize(); - } - - render() { - const style = this.props.style || {}; - const className = this.props.className || ''; - return ( -
(this.el = el)}> - {this.props.children} -
- ); - } -} - -Resize.defaultProps = { - frequency: 500, -}; - -Resize.propTypes = { - frequency: PropTypes.number, - onResize: PropTypes.func, -}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries.js b/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries.js deleted file mode 100644 index 2fe676efa58aa..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries.js +++ /dev/null @@ -1,201 +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 PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import classNames from 'classnames'; -import _ from 'lodash'; -import { getLastValue } from '../../../common/get_last_value'; -import { isBackgroundInverted } from '../../../common/set_is_reversed'; -import { TimeseriesChart } from './timeseries_chart'; -import { Legend } from './legend'; -import { eventBus } from '../lib/events'; -import reactcss from 'reactcss'; - -export class Timeseries extends Component { - constructor(props) { - super(props); - const values = this.getLastValues(props); - this.state = { - showLegend: props.legend != null ? props.legend : true, - values: values || {}, - show: _.keys(values) || [], - ignoreLegendUpdates: false, - ignoreVisibilityUpdates: false, - }; - this.toggleFilter = this.toggleFilter.bind(this); - this.handleHideClick = this.handleHideClick.bind(this); - this.plothover = this.plothover.bind(this); - } - - filterLegend(id) { - if (!_.has(this.state.values, id)) return []; - const notAllShown = _.keys(this.state.values).length !== this.state.show.length; - const isCurrentlyShown = _.includes(this.state.show, id); - const show = []; - if (notAllShown && isCurrentlyShown) { - this.setState({ ignoreVisibilityUpdates: false, show: Object.keys(this.state.values) }); - } else { - show.push(id); - this.setState({ ignoreVisibilityUpdates: true, show: [id] }); - } - return show; - } - - toggleFilter(event, id) { - const show = this.filterLegend(id); - if (_.isFunction(this.props.onFilter)) { - this.props.onFilter(show); - } - eventBus.trigger('toggleFilter', id, this); - } - - getLastValues(props) { - const values = {}; - props.series.forEach(row => { - // we need a valid identifier - if (!row.id) row.id = row.label; - values[row.id] = getLastValue(row.data); - }); - return values; - } - - updateLegend(pos, item) { - const values = {}; - if (pos) { - this.props.series.forEach(row => { - if (row.data && Array.isArray(row.data)) { - if ( - item && - row.data[item.dataIndex] && - row.data[item.dataIndex][0] === item.datapoint[0] - ) { - values[row.id] = row.data[item.dataIndex][1]; - } else { - let closest; - for (let i = 0; i < row.data.length; i++) { - closest = i; - if (row.data[i] && pos.x < row.data[i][0]) break; - } - if (!row.data[closest]) return (values[row.id] = null); - const [, value] = row.data[closest]; - values[row.id] = (value != null && value) || null; - } - } - }); - } else { - _.assign(values, this.getLastValues(this.props)); - } - - this.setState({ values }); - } - - componentWillReceiveProps(props) { - if (props.legend !== this.props.legend) this.setState({ showLegend: props.legend }); - if (!this.state.ignoreLegendUpdates) { - const values = this.getLastValues(props); - const currentKeys = _.keys(this.state.values); - const keys = _.keys(values); - const diff = _.difference(keys, currentKeys); - const nextState = { values: values }; - if (diff.length && !this.state.ignoreVisibilityUpdates) { - nextState.show = keys; - } - this.setState(nextState); - } - } - - plothover(event, pos, item) { - this.updateLegend(pos, item); - } - - handleHideClick() { - this.setState({ showLegend: !this.state.showLegend }); - } - - render() { - const classes = classNames('tvbVisTimeSeries', { - 'tvbVisTimeSeries--reversed': isBackgroundInverted(this.props.backgroundColor), - }); - - const styles = reactcss( - { - bottomLegend: { - content: { - flexDirection: 'column', - }, - }, - }, - { bottomLegend: this.props.legendPosition === 'bottom' } - ); - return ( -
-
-
- -
- -
-
- ); - } -} - -Timeseries.defaultProps = { - legend: true, - showGrid: true, -}; - -Timeseries.propTypes = { - legend: PropTypes.bool, - legendPosition: PropTypes.string, - onFilter: PropTypes.func, - series: PropTypes.array, - annotations: PropTypes.array, - backgroundColor: PropTypes.string, - options: PropTypes.object, - tickFormatter: PropTypes.func, - showGrid: PropTypes.bool, - xaxisLabel: PropTypes.string, - dateFormat: PropTypes.string, -}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries_chart.js b/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries_chart.js deleted file mode 100644 index aa00c93ccf4b6..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/timeseries_chart.js +++ /dev/null @@ -1,229 +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 PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import classNames from 'classnames'; -import { isBackgroundInverted, isBackgroundDark } from '../../../common/set_is_reversed'; -import moment from 'moment'; -import reactcss from 'reactcss'; -import { FlotChart } from './flot_chart'; -import { Annotation } from './annotation'; -import { EuiIcon } from '@elastic/eui'; - -export function scaleUp(value) { - return window.devicePixelRatio * value; -} - -export function scaleDown(value) { - return value / window.devicePixelRatio; -} - -export class TimeseriesChart extends Component { - constructor(props) { - super(props); - this.state = { - annotations: [], - showTooltip: false, - mouseHoverTimer: false, - }; - this.handleMouseLeave = this.handleMouseLeave.bind(this); - this.handleMouseOver = this.handleMouseOver.bind(this); - this.renderAnnotations = this.renderAnnotations.bind(this); - this.handleDraw = this.handleDraw.bind(this); - } - - calculateLeftRight(item, plot) { - const canvas = plot.getCanvas(); - const point = plot.pointOffset({ x: item.datapoint[0], y: item.datapoint[1] }); - const edge = (scaleUp(point.left) + 10) / canvas.width; - let right; - let left; - if (edge > 0.5) { - right = scaleDown(canvas.width) - point.left; - left = null; - } else { - right = null; - left = point.left; - } - return [left, right]; - } - - handleDraw(plot) { - if (!plot || !this.props.annotations) return; - const annotations = this.props.annotations.reduce((acc, anno) => { - return acc.concat( - anno.series.map(series => { - return { - series, - plot, - key: `${anno.id}-${series[0]}`, - icon: anno.icon, - color: anno.color, - }; - }) - ); - }, []); - this.setState({ annotations }); - } - - handleMouseOver(e, pos, item, plot) { - if (typeof this.state.mouseHoverTimer === 'number') { - window.clearTimeout(this.state.mouseHoverTimer); - } - - if (item) { - const plotOffset = plot.getPlotOffset(); - const point = plot.pointOffset({ x: item.datapoint[0], y: item.datapoint[1] }); - const [left, right] = this.calculateLeftRight(item, plot); - const top = point.top; - this.setState({ - showTooltip: true, - item, - left, - right, - top: top + 10, - bottom: plotOffset.bottom, - }); - } - } - - handleMouseLeave() { - this.state.mouseHoverTimer = window.setTimeout(() => { - this.setState({ showTooltip: false }); - }, 250); - } - - renderAnnotations(annotation) { - return ( - - ); - } - - render() { - const { item, right, top, left } = this.state; - const { series } = this.props; - let tooltip; - - const styles = reactcss( - { - showTooltip: { - tooltipContainer: { - top: top - 8, - left, - right, - }, - }, - hideTooltip: { - tooltipContainer: { display: 'none' }, - }, - }, - { - showTooltip: this.state.showTooltip, - hideTooltip: !this.state.showTooltip, - } - ); - - if (item) { - const metric = series.find(r => r.id === item.series.id); - const formatter = (metric && metric.tickFormatter) || this.props.tickFormatter || (v => v); - const value = item.datapoint[2] ? item.datapoint[1] - item.datapoint[2] : item.datapoint[1]; - tooltip = ( -
- -
-
- {moment(item.datapoint[0]).format(this.props.dateFormat)} -
-
- -
-
{item.series.label}
-
{formatter(value)}
-
-
-
-
- ); - } - - const params = { - crosshair: this.props.crosshair, - onPlotCreate: this.handlePlotCreate, - onBrush: this.props.onBrush, - onMouseLeave: this.handleMouseLeave, - onMouseOver: this.handleMouseOver, - onDraw: this.handleDraw, - options: this.props.options, - plothover: this.props.plothover, - reversed: isBackgroundDark(this.props.backgroundColor), - series: this.props.series, - annotations: this.props.annotations, - showGrid: this.props.showGrid, - show: this.props.show, - tickFormatter: this.props.tickFormatter, - yaxes: this.props.yaxes, - xAxisFormatter: this.props.xAxisFormatter, - }; - - const annotations = this.state.annotations.map(this.renderAnnotations); - const axisLabelClass = classNames('tvbVisTimeSeries__axisLabel', { - 'tvbVisTimeSeries__axisLabel--reversed': isBackgroundInverted(this.props.backgroundColor), - }); - - return ( -
(this.container = el)} className="tvbVisTimeSeries__container"> - {tooltip} - {annotations} - -
{this.props.xaxisLabel}
-
- ); - } -} - -TimeseriesChart.defaultProps = { - showGrid: true, - dateFormat: 'll LTS', -}; - -TimeseriesChart.propTypes = { - crosshair: PropTypes.bool, - onBrush: PropTypes.func, - options: PropTypes.object, - plothover: PropTypes.func, - backgroundColor: PropTypes.string, - series: PropTypes.array, - annotations: PropTypes.array, - show: PropTypes.array, - tickFormatter: PropTypes.func, - yaxes: PropTypes.array, - showGrid: PropTypes.bool, - xaxisLabel: PropTypes.string, - dateFormat: PropTypes.string, -}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/vertical_legend.js b/src/legacy/core_plugins/metrics/public/visualizations/components/vertical_legend.js deleted file mode 100644 index 47c3c357d6f1b..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/vertical_legend.js +++ /dev/null @@ -1,91 +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 PropTypes from 'prop-types'; -import React from 'react'; -import { createLegendSeries } from '../lib/create_legend_series'; -import reactcss from 'reactcss'; -import { htmlIdGenerator, EuiButtonIcon } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; - -export const VerticalLegend = injectI18n(function(props) { - const rows = props.series.map(createLegendSeries(props)); - const htmlId = htmlIdGenerator(); - const hideLegend = !props.showLegend; - const leftLegend = props.legendPosition === 'left'; - - const styles = reactcss( - { - default: { - legend: { width: 200 }, - }, - leftLegend: { - legend: { order: '-1' }, - control: { order: '2' }, - }, - hideLegend: { - legend: { width: 24 }, - series: { display: 'none' }, - }, - }, - { hideLegend, leftLegend } - ); - - const openIcon = leftLegend ? 'arrowRight' : 'arrowLeft'; - const closeIcon = leftLegend ? 'arrowLeft' : 'arrowRight'; - const legendToggleIcon = hideLegend ? `${openIcon}` : `${closeIcon}`; - - return ( -
- - -
- {rows} -
-
- ); -}); - -VerticalLegend.propTypes = { - legendPosition: PropTypes.string, - onClick: PropTypes.func, - onToggle: PropTypes.func, - series: PropTypes.array, - showLegend: PropTypes.bool, - seriesValues: PropTypes.object, - seriesFilter: PropTypes.array, - tickFormatter: PropTypes.func, -}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_bar_width.js b/src/legacy/core_plugins/metrics/public/visualizations/constants/chart.js similarity index 59% rename from src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_bar_width.js rename to src/legacy/core_plugins/metrics/public/visualizations/constants/chart.js index a58246c446882..2e7eae438de12 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_bar_width.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/constants/chart.js @@ -17,15 +17,25 @@ * under the License. */ -import _ from 'lodash'; -// bar sizes are measured in milliseconds so this assumes that the different -// between timestamps is in milliseconds. A normal bar size is 70% which gives -// enough spacing for the bar. -export const calculateBarWidth = (series, multiplier = 0.7) => { - const first = _.first(series); - try { - return (first.data[1][0] - first.data[0][0]) * multiplier; - } catch (e) { - return 1000; // 1000 ms - } +export const COLORS = { + LINE_COLOR: 'rgba(105,112,125,0.2)', + TEXT_COLOR: 'rgba(0,0,0,0.4)', + TEXT_COLOR_REVERSED: 'rgba(255,255,255,0.5)', + VALUE_COLOR: 'rgba(0,0,0,0.7)', + VALUE_COLOR_REVERSED: 'rgba(255,255,255,0.8)', +}; + +export const GRID_LINE_CONFIG = { + stroke: 'rgba(125,125,125,0.1)', +}; + +export const X_ACCESSOR_INDEX = 0; +export const STACK_ACCESSORS = [0]; +export const Y_ACCESSOR_INDEXES = [1]; + +export const STACKED_OPTIONS = { + NONE: 'none', + PERCENT: 'percent', + STACKED: 'stacked', + STACKED_WITHIN_SERIES: 'stacked_within_series', }; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/constants/icons.js b/src/legacy/core_plugins/metrics/public/visualizations/constants/icons.js new file mode 100644 index 0000000000000..4c55a9dfbd387 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/constants/icons.js @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { bombIcon } from '../../components/svg/bomb_icon'; +import { fireIcon } from '../../components/svg/fire_icon'; + +export const ICON_NAMES = { + ASTERISK: 'fa-asterisk', + BELL: 'fa-bell', + BOLT: 'fa-bolt', + BOMB: 'fa-bomb', + BUG: 'fa-bug', + COMMENT: 'fa-comment', + EXCLAMATION_CIRCLE: 'fa-exclamation-circle', + EXCLAMATION_TRIANGLE: 'fa-exclamation-triangle', + FIRE: 'fa-fire', + FLAG: 'fa-flag', + HEART: 'fa-heart', + MAP_MARKER: 'fa-map-marker', + MAP_PIN: 'fa-map-pin', + STAR: 'fa-star', + TAG: 'fa-tag', +}; + +export const ICON_TYPES_MAP = { + [ICON_NAMES.ASTERISK]: 'asterisk', + [ICON_NAMES.BELL]: 'bell', + [ICON_NAMES.BOLT]: 'bolt', + [ICON_NAMES.BOMB]: bombIcon, + [ICON_NAMES.BUG]: 'bug', + [ICON_NAMES.COMMENT]: 'editorComment', + [ICON_NAMES.EXCLAMATION_CIRCLE]: 'alert', // TODO: Change as an exclamation mark is added + [ICON_NAMES.EXCLAMATION_TRIANGLE]: 'alert', + [ICON_NAMES.FIRE]: fireIcon, + [ICON_NAMES.FLAG]: 'flag', + [ICON_NAMES.HEART]: 'heart', + [ICON_NAMES.MAP_MARKER]: 'mapMarker', + [ICON_NAMES.MAP_PIN]: 'pinFilled', + [ICON_NAMES.STAR]: 'starFilled', + [ICON_NAMES.TAG]: 'tag', +}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/events.js b/src/legacy/core_plugins/metrics/public/visualizations/constants/index.js similarity index 93% rename from src/legacy/core_plugins/metrics/public/visualizations/lib/events.js rename to src/legacy/core_plugins/metrics/public/visualizations/constants/index.js index 3bfcfa72032fd..0264b0bcfa9e0 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/events.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/constants/index.js @@ -17,6 +17,5 @@ * under the License. */ -import $ from 'jquery'; - -export const eventBus = $({}); +export * from './chart'; +export * from './icons'; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calculate_fill_color.test.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calculate_fill_color.test.js deleted file mode 100644 index c43c0a16f24dc..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calculate_fill_color.test.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 { expect } from 'chai'; -import { calculateFillColor } from '../calculate_fill_color'; - -describe('calculateFillColor(color, fill)', () => { - it('should return "fill" and "fillColor" properties', () => { - const color = 'rgb(255,0,0)'; - const fill = 1; - const data = calculateFillColor(color, fill); - - expect(data.fill).to.be.true; - expect(data.fillColor).to.be.a('string'); - }); - - it('should set "fill" property to false in case of 0 opacity', () => { - const color = 'rgb(255, 0, 0)'; - const fill = 0; - const data = calculateFillColor(color, fill); - - expect(data.fill).to.be.false; - }); - - it('should return the opacity less than 1', () => { - const color = 'rgba(255, 0, 0, 0.9)'; - const fill = 10; - const data = calculateFillColor(color, fill); - - expect(data.fillColor).to.equal('rgba(255, 0, 0, 0.9)'); - }); - - it('should sum fill and color opacity', () => { - const color = 'rgba(255, 0, 0, 0.5)'; - const fill = 0.5; - const data = calculateFillColor(color, fill); - - expect(data.fillColor).to.equal('rgba(255, 0, 0, 0.25)'); - }); -}); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/get_value_by.test.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/get_value_by.test.js deleted file mode 100644 index 6acbb1196d123..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/get_value_by.test.js +++ /dev/null @@ -1,36 +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 { getValueBy } from '../get_value_by'; -import { expect } from 'chai'; - -describe('getValueBy(fn, data)', () => { - it("returns max for getValueBy('max', data) ", () => { - const data = [[0, 5], [1, 3], [2, 4], [3, 6], [4, 5]]; - expect(getValueBy('max', data)).to.equal(6); - }); - it('returns 0 if data is not array', () => { - const data = '1'; - expect(getValueBy('max', data)).to.equal(0); - }); - it('returns value if data is number', () => { - const data = 1; - expect(getValueBy('max', data)).to.equal(1); - }); -}); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/active_cursor.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/active_cursor.js new file mode 100644 index 0000000000000..427ced4dc3f2a --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/lib/active_cursor.js @@ -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. + */ + +// TODO: Remove bus when action/triggers are available with LegacyPluginApi or metric is converted to Embeddable +import $ from 'jquery'; + +export const ACTIVE_CURSOR = 'ACTIVE_CURSOR'; + +export const eventBus = $({}); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/create_legend_series.js b/src/legacy/core_plugins/metrics/public/visualizations/lib/create_legend_series.js deleted file mode 100644 index 1e4e35f8a366a..0000000000000 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/create_legend_series.js +++ /dev/null @@ -1,48 +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 _ from 'lodash'; -import { EuiIcon } from '@elastic/eui'; - -export const createLegendSeries = props => (row, index = 0) => { - function tickFormatter(value) { - if (_.isFunction(props.tickFormatter)) return props.tickFormatter(value); - return value; - } - const key = `tvbLegend__item${row.id}${index}`; - const formatter = row.tickFormatter || tickFormatter; - const value = formatter(props.seriesValues[row.id]); - const classes = ['tvbLegend__item']; - - if (!_.includes(props.seriesFilter, row.id)) classes.push('disabled'); - if (row.label == null || row.legend === false) - return
; - return ( -
- -
- ); -}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/_annotation.scss b/src/legacy/core_plugins/metrics/public/visualizations/views/_annotation.scss similarity index 100% rename from src/legacy/core_plugins/metrics/public/visualizations/components/_annotation.scss rename to src/legacy/core_plugins/metrics/public/visualizations/views/_annotation.scss diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/_gauge.scss b/src/legacy/core_plugins/metrics/public/visualizations/views/_gauge.scss similarity index 100% rename from src/legacy/core_plugins/metrics/public/visualizations/components/_gauge.scss rename to src/legacy/core_plugins/metrics/public/visualizations/views/_gauge.scss diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/_index.scss b/src/legacy/core_plugins/metrics/public/visualizations/views/_index.scss similarity index 62% rename from src/legacy/core_plugins/metrics/public/visualizations/components/_index.scss rename to src/legacy/core_plugins/metrics/public/visualizations/views/_index.scss index c5ce0ff8c8513..ddd5604801806 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/_index.scss +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/_index.scss @@ -1,6 +1,5 @@ @import './annotation'; @import './gauge'; @import './metric'; -@import './legend'; -@import './timeseries_chart'; + @import './top_n'; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/_metric.scss b/src/legacy/core_plugins/metrics/public/visualizations/views/_metric.scss similarity index 100% rename from src/legacy/core_plugins/metrics/public/visualizations/components/_metric.scss rename to src/legacy/core_plugins/metrics/public/visualizations/views/_metric.scss diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/_top_n.scss b/src/legacy/core_plugins/metrics/public/visualizations/views/_top_n.scss similarity index 91% rename from src/legacy/core_plugins/metrics/public/visualizations/components/_top_n.scss rename to src/legacy/core_plugins/metrics/public/visualizations/views/_top_n.scss index 6392e437bc0a6..fb6be95dba2a2 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/_top_n.scss +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/_top_n.scss @@ -35,7 +35,12 @@ } .tvbVisTopN__innerBar { - min-height: $euiSize; + position: relative; + + > div { + width: 100%; + min-height: $euiSize; + } } .tvbVisTopN--reversed { diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/annotation.js b/src/legacy/core_plugins/metrics/public/visualizations/views/annotation.js similarity index 100% rename from src/legacy/core_plugins/metrics/public/visualizations/components/annotation.js rename to src/legacy/core_plugins/metrics/public/visualizations/views/annotation.js diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/gauge.js b/src/legacy/core_plugins/metrics/public/visualizations/views/gauge.js similarity index 100% rename from src/legacy/core_plugins/metrics/public/visualizations/components/gauge.js rename to src/legacy/core_plugins/metrics/public/visualizations/views/gauge.js diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/gauge_vis.js b/src/legacy/core_plugins/metrics/public/visualizations/views/gauge_vis.js similarity index 98% rename from src/legacy/core_plugins/metrics/public/visualizations/components/gauge_vis.js rename to src/legacy/core_plugins/metrics/public/visualizations/views/gauge_vis.js index 28f00bddd4e57..aa4ac99243397 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/gauge_vis.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/gauge_vis.js @@ -22,7 +22,7 @@ import React, { Component } from 'react'; import _ from 'lodash'; import reactcss from 'reactcss'; import { calculateCoordinates } from '../lib/calculate_coordinates'; -import { COLORS } from '../lib/colors'; +import { COLORS } from '../constants/chart'; export class GaugeVis extends Component { constructor(props) { @@ -118,7 +118,7 @@ export class GaugeVis extends Component { cx: 60, cy: 60, fill: 'rgba(0,0,0,0)', - stroke: COLORS.lineColor, + stroke: COLORS.LINE_COLOR, strokeDasharray: `${sliceSize * size} ${size}`, strokeWidth: this.props.innerLine, }, diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/metric.js b/src/legacy/core_plugins/metrics/public/visualizations/views/metric.js similarity index 100% rename from src/legacy/core_plugins/metrics/public/visualizations/components/metric.js rename to src/legacy/core_plugins/metrics/public/visualizations/views/metric.js diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_fill_color.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/__mocks__/@elastic/charts.js similarity index 61% rename from src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_fill_color.js rename to src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/__mocks__/@elastic/charts.js index 878cc518ef384..cb59bef63681b 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/calculate_fill_color.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/__mocks__/@elastic/charts.js @@ -17,16 +17,29 @@ * under the License. */ -import Color from 'color'; +export const CurveType = { + CURVE_CARDINAL: 0, + CURVE_NATURAL: 1, + CURVE_MONOTONE_X: 2, + CURVE_MONOTONE_Y: 3, + CURVE_BASIS: 4, + CURVE_CATMULL_ROM: 5, + CURVE_STEP: 6, + CURVE_STEP_AFTER: 7, + CURVE_STEP_BEFORE: 8, + LINEAR: 9, +}; -export const calculateFillColor = (color, fill = 1) => { - const initialColor = new Color(color).rgb(); +export const ScaleType = { + Linear: 'linear', + Ordinal: 'ordinal', + Log: 'log', + Sqrt: 'sqrt', + Time: 'time', +}; - const opacity = Math.min(Number(fill), 1) * initialColor.valpha; - const [r, g, b] = initialColor.color; +export const getSpecId = x => `id:${x}`; +export const getGroupId = x => `groupId:${x}`; - return { - fill: opacity > 0, - fillColor: new Color([r, g, b, Number(opacity.toFixed(2))]).string(), - }; -}; +export const BarSeries = () => null; +export const AreaSeries = () => null; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap new file mode 100644 index 0000000000000..822de4cef0813 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/__snapshots__/area_decorator.test.js.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.js should render and match a snapshot 1`] = ` + "rgb(0, 156, 224)", + } + } + data={ + Array [ + Array [ + 1556917200000, + 7, + ], + Array [ + 1557003600000, + 9, + ], + ] + } + enableHistogramMode={true} + groupId="groupId:yaxis_main_group" + hideInLegend={false} + histogramModeAlignment="center" + id="id:61ca57f1-469d-11e7-af02-69e470af7417:Rome" + name="Rome" + stackAsPercentage={false} + timeZone="local" + xAccessor={0} + xScaleType="time" + yAccessors={ + Array [ + 1, + ] + } + yScaleType="linear" +/> +`; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap new file mode 100644 index 0000000000000..78133f2dda7cc --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/__snapshots__/bar_decorator.test.js.snap @@ -0,0 +1,55 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.js should render and match a snapshot 1`] = ` + "rgb(0, 156, 224)", + } + } + data={ + Array [ + Array [ + 1556917200000, + 7, + ], + Array [ + 1557003600000, + 9, + ], + ] + } + enableHistogramMode={true} + groupId="groupId:yaxis_main_group" + hideInLegend={false} + histogramModeAlignment="center" + id="id:61ca57f1-469d-11e7-af02-69e470af7417:Rome" + name="Rome" + stackAsPercentage={false} + timeZone="local" + xAccessor={0} + xScaleType="time" + yAccessors={ + Array [ + 1, + ] + } + yScaleType="linear" +/> +`; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.js new file mode 100644 index 0000000000000..536064139e6ea --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.js @@ -0,0 +1,81 @@ +/* + * 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 { getSpecId, getGroupId, ScaleType, AreaSeries } from '@elastic/charts'; +import { getSeriesColors, getAreaStyles } from '../utils/series_styles'; +import { ChartsEntities } from '../model/charts'; +import { X_ACCESSOR_INDEX, Y_ACCESSOR_INDEXES } from '../../../constants'; + +export function AreaSeriesDecorator({ + seriesId, + seriesGroupId, + name, + data, + hideInLegend, + lines, + color, + stackAccessors, + stackAsPercentage, + points, + xScaleType, + yScaleType, + timeZone, + enableHistogramMode, + useDefaultGroupDomain, + sortIndex, +}) { + const id = getSpecId(seriesId); + const groupId = getGroupId(seriesGroupId); + const customSeriesColors = getSeriesColors(color, id); + const areaSeriesStyle = getAreaStyles({ points, lines, color }); + + const seriesSettings = { + id, + name, + groupId, + data, + customSeriesColors, + hideInLegend, + xAccessor: X_ACCESSOR_INDEX, + yAccessors: Y_ACCESSOR_INDEXES, + stackAccessors, + stackAsPercentage, + xScaleType, + yScaleType, + timeZone, + enableHistogramMode, + useDefaultGroupDomain, + sortIndex, + ...areaSeriesStyle, + }; + + if (enableHistogramMode) { + seriesSettings.histogramModeAlignment = 'center'; + } + + return ; +} + +AreaSeriesDecorator.propTypes = ChartsEntities.AreaChart; + +AreaSeriesDecorator.defaultProps = { + yScaleType: ScaleType.Linear, + xScaleType: ScaleType.Time, +}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.test.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.test.js new file mode 100644 index 0000000000000..f58abc7f1f724 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.test.js @@ -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 React from 'react'; +import { shallow } from 'enzyme'; +import { AreaSeriesDecorator } from './area_decorator'; + +describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/area_decorator.js', () => { + let props; + + beforeEach(() => { + props = { + lines: { + fill: 1, + lineWidth: 2, + show: true, + steps: false, + }, + points: { + lineWidth: 5, + radius: 1, + show: false, + }, + color: 'rgb(0, 156, 224)', + data: [[1556917200000, 7], [1557003600000, 9]], + hideInLegend: false, + stackAsPercentage: false, + seriesId: '61ca57f1-469d-11e7-af02-69e470af7417:Rome', + seriesGroupId: 'yaxis_main_group', + name: 'Rome', + stack: false, + timeZone: 'local', + enableHistogramMode: true, + }; + }); + + describe('', () => { + test('should render and match a snapshot', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.js new file mode 100644 index 0000000000000..3dbe04dca06b8 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.js @@ -0,0 +1,80 @@ +/* + * 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 { getSpecId, getGroupId, ScaleType, BarSeries } from '@elastic/charts'; +import { getSeriesColors, getBarStyles } from '../utils/series_styles'; +import { ChartsEntities } from '../model/charts'; +import { X_ACCESSOR_INDEX, Y_ACCESSOR_INDEXES } from '../../../constants'; + +export function BarSeriesDecorator({ + seriesId, + seriesGroupId, + name, + data, + hideInLegend, + bars, + color, + stackAccessors, + stackAsPercentage, + xScaleType, + yScaleType, + timeZone, + enableHistogramMode, + useDefaultGroupDomain, + sortIndex, +}) { + const id = getSpecId(seriesId); + const groupId = getGroupId(seriesGroupId); + const customSeriesColors = getSeriesColors(color, id); + const barSeriesStyle = getBarStyles(bars, color); + + const seriesSettings = { + id, + name, + groupId, + data, + customSeriesColors, + hideInLegend, + xAccessor: X_ACCESSOR_INDEX, + yAccessors: Y_ACCESSOR_INDEXES, + stackAccessors, + stackAsPercentage, + xScaleType, + yScaleType, + timeZone, + enableHistogramMode, + useDefaultGroupDomain, + sortIndex, + ...barSeriesStyle, + }; + + if (enableHistogramMode) { + seriesSettings.histogramModeAlignment = 'center'; + } + + return ; +} + +BarSeriesDecorator.propTypes = ChartsEntities.BarChart; + +BarSeriesDecorator.defaultProps = { + yScaleType: ScaleType.Linear, + xScaleType: ScaleType.Time, +}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.test.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.test.js new file mode 100644 index 0000000000000..8432814870fbf --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.test.js @@ -0,0 +1,50 @@ +/* + * 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 { BarSeriesDecorator } from './bar_decorator'; + +describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/decorators/bar_decorator.js', () => { + let props; + + beforeEach(() => { + props = { + bars: { show: true, fill: 0.5, lineWidth: 2 }, + color: 'rgb(0, 156, 224)', + data: [[1556917200000, 7], [1557003600000, 9]], + hideInLegend: false, + stackAsPercentage: false, + seriesId: '61ca57f1-469d-11e7-af02-69e470af7417:Rome', + seriesGroupId: 'yaxis_main_group', + name: 'Rome', + stack: false, + timeZone: 'local', + enableHistogramMode: true, + }; + }); + + describe('', () => { + test('should render and match a snapshot', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/index.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/index.js new file mode 100644 index 0000000000000..a02ea83e5104b --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/index.js @@ -0,0 +1,257 @@ +/* + * 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 PropTypes from 'prop-types'; + +import { + Axis, + Chart, + Position, + Settings, + getAxisId, + getGroupId, + DARK_THEME, + LIGHT_THEME, + getAnnotationId, + AnnotationDomainTypes, + LineAnnotation, + TooltipType, +} from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; + +import { timezoneProvider } from 'ui/vis/lib/timezone'; +import { eventBus, ACTIVE_CURSOR } from '../../lib/active_cursor'; +import chrome from 'ui/chrome'; +import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constants'; +import { AreaSeriesDecorator } from './decorators/area_decorator'; +import { BarSeriesDecorator } from './decorators/bar_decorator'; +import { getStackAccessors } from './utils/stack_format'; + +const generateAnnotationData = (values, formatter) => + values.map(({ key, docs }) => ({ + dataValue: key, + details: docs[0], + header: formatter({ + value: key, + }), + })); + +const decorateFormatter = formatter => ({ value }) => formatter(value); + +const handleCursorUpdate = cursor => { + eventBus.trigger(ACTIVE_CURSOR, cursor); +}; + +export const TimeSeries = ({ + isDarkMode, + showGrid, + legend, + legendPosition, + xAxisLabel, + series, + yAxis, + onBrush, + xAxisFormatter, + annotations, + enableHistogramMode, +}) => { + const chartRef = useRef(); + const updateCursor = (_, cursor) => { + if (chartRef.current) { + chartRef.current.dispatchExternalCursorEvent(cursor); + } + }; + + useEffect(() => { + eventBus.on(ACTIVE_CURSOR, updateCursor); + + return () => { + eventBus.off(ACTIVE_CURSOR, undefined, updateCursor); + }; + }, []); // eslint-disable-line + + const tooltipFormatter = decorateFormatter(xAxisFormatter); + const uiSettings = chrome.getUiSettingsClient(); + const timeZone = timezoneProvider(uiSettings)(); + const hasBarChart = series.some(({ bars }) => bars.show); + + return ( + + + + {annotations.map(({ id, data, icon, color }) => { + const dataValues = generateAnnotationData(data, tooltipFormatter); + const style = { line: { stroke: color } }; + + return ( + } + hideLinesTooltips={true} + style={style} + /> + ); + })} + + {series.map( + ( + { + id, + label, + bars, + lines, + data, + hideInLegend, + xScaleType, + yScaleType, + groupId, + color, + stack, + points, + useDefaultGroupDomain, + }, + sortIndex + ) => { + const stackAccessors = getStackAccessors(stack); + const isPercentage = stack === STACKED_OPTIONS.PERCENT; + const key = `${id}-${label}`; + + if (bars.show) { + return ( + + ); + } + + if (lines.show) { + return ( + + ); + } + + return null; + } + )} + + {yAxis.map(({ id, groupId, position, tickFormatter, domain, hide }) => ( + + ))} + + + + ); +}; + +TimeSeries.defaultProps = { + showGrid: true, + legend: true, + legendPosition: 'right', +}; + +TimeSeries.propTypes = { + isDarkMode: PropTypes.bool, + showGrid: PropTypes.bool, + legend: PropTypes.bool, + legendPosition: PropTypes.string, + xAxisLabel: PropTypes.string, + series: PropTypes.array, + yAxis: PropTypes.array, + onBrush: PropTypes.func, + xAxisFormatter: PropTypes.func, + annotations: PropTypes.array, + enableHistogramMode: PropTypes.bool.isRequired, +}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap new file mode 100644 index 0000000000000..541265c05057a --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/__snapshots__/charts.test.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.js ChartsEntities should match a snapshot of ChartsEntities 1`] = ` +Object { + "AreaChart": Object { + "color": [Function], + "data": [Function], + "enableHistogramMode": [Function], + "hideInLegend": [Function], + "lines": [Function], + "name": [Function], + "points": [Function], + "seriesGroupId": [Function], + "seriesId": [Function], + "sortIndex": [Function], + "stackAccessors": [Function], + "stackAsPercentage": [Function], + "timeZone": [Function], + "useDefaultGroupDomain": [Function], + "xScaleType": [Function], + "yScaleType": [Function], + }, + "BarChart": Object { + "bars": [Function], + "color": [Function], + "data": [Function], + "enableHistogramMode": [Function], + "hideInLegend": [Function], + "name": [Function], + "seriesGroupId": [Function], + "seriesId": [Function], + "sortIndex": [Function], + "stackAccessors": [Function], + "stackAsPercentage": [Function], + "timeZone": [Function], + "useDefaultGroupDomain": [Function], + "xScaleType": [Function], + "yScaleType": [Function], + }, +} +`; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.js new file mode 100644 index 0000000000000..b14b84dcd1fe4 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.js @@ -0,0 +1,70 @@ +/* + * 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 PropTypes from 'prop-types'; + +const Chart = { + seriesId: PropTypes.string.isRequired, + seriesGroupId: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + /** + * @example + * [[1556917200000, 6], [1556231200000, 16]] + */ + data: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)).isRequired, + hideInLegend: PropTypes.bool.isRequired, + color: PropTypes.string.isRequired, + stackAsPercentage: PropTypes.bool.isRequired, + stackAccessors: PropTypes.arrayOf(PropTypes.number), + xScaleType: PropTypes.string, + yScaleType: PropTypes.string, + timeZone: PropTypes.string.isRequired, + enableHistogramMode: PropTypes.bool.isRequired, + useDefaultGroupDomain: PropTypes.bool, + sortIndex: PropTypes.number, +}; + +const BarChart = { + ...Chart, + bars: PropTypes.shape({ + fill: PropTypes.number, + lineWidth: PropTypes.number, + show: PropTypes.boolean, + }).isRequired, +}; + +const AreaChart = { + ...Chart, + lines: PropTypes.shape({ + fill: PropTypes.number, + lineWidth: PropTypes.number, + show: PropTypes.bool, + steps: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), + }).isRequired, + points: PropTypes.shape({ + lineWidth: PropTypes.number, + radius: PropTypes.number, + show: PropTypes.bool, + }).isRequired, +}; + +export const ChartsEntities = { + BarChart, + AreaChart, +}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.test.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.test.js new file mode 100644 index 0000000000000..80bc78111ca03 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.test.js @@ -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 { ChartsEntities } from './charts'; + +describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/model/charts.js', () => { + describe('ChartsEntities', () => { + test('should match a snapshot of ChartsEntities', () => { + expect(ChartsEntities).toMatchSnapshot(); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap new file mode 100644 index 0000000000000..607580d1f37f8 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/__snapshots__/series_styles.test.js.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getAreaStyles() should match a snapshot 1`] = ` +Object { + "areaSeriesStyle": Object { + "area": Object { + "fill": "rgb(224, 0, 221)", + "opacity": 0, + "visible": true, + }, + "line": Object { + "stroke": "rgb(224, 0, 221)", + "strokeWidth": 1, + "visible": true, + }, + "point": Object { + "radius": 1, + "stroke": "rgb(224, 0, 221)", + "strokeWidth": 1, + "visible": true, + }, + }, + "curve": 6, +} +`; + +exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getAreaStyles() should set default values if points, lines and color are empty 1`] = ` +Object { + "areaSeriesStyle": Object { + "area": Object { + "fill": "", + "opacity": undefined, + "visible": false, + }, + "line": Object { + "stroke": "", + "strokeWidth": 0, + "visible": false, + }, + "point": Object { + "radius": 0.5, + "stroke": "#000", + "strokeWidth": 5, + "visible": false, + }, + }, + "curve": 9, +} +`; + +exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getBarStyles() should match a snapshot 1`] = ` +Object { + "barSeriesStyle": Object { + "rect": Object { + "fill": "rgb(224, 0, 221)", + "opacity": 0.5, + }, + "rectBorder": Object { + "stroke": "rgb(224, 0, 221)", + "strokeWidth": 2, + "visible": true, + }, + }, +} +`; + +exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getBarStyles() should set default values if bars and colors are empty 1`] = ` +Object { + "barSeriesStyle": Object { + "rect": Object { + "fill": "#000", + "opacity": 1, + }, + "rectBorder": Object { + "stroke": "#000", + "strokeWidth": 0, + "visible": true, + }, + }, +} +`; + +exports[`src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js getSeriesColors() should match a snapshot 1`] = ` +Map { + Object { + "colorValues": Array [], + "specId": "IT", + } => "rgb(224, 0, 221)", +} +`; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js new file mode 100644 index 0000000000000..63be14790c6c5 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js @@ -0,0 +1,67 @@ +/* + * 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 { CurveType } from '@elastic/charts'; + +const DEFAULT_COLOR = '#000'; + +export const getAreaStyles = ({ points, lines, color }) => ({ + areaSeriesStyle: { + line: { + stroke: color, + strokeWidth: Number(lines.lineWidth) || 0, + visible: Boolean(lines.show && lines.lineWidth), + }, + area: { + fill: color, + opacity: lines.fill <= 0 ? 0 : lines.fill, + visible: Boolean(lines.show), + }, + point: { + radius: points.radius || 0.5, + stroke: color || DEFAULT_COLOR, + strokeWidth: points.lineWidth || 5, + visible: points.lineWidth > 0 && Boolean(points.show), + }, + }, + curve: lines.steps ? CurveType.CURVE_STEP : CurveType.LINEAR, +}); + +export const getBarStyles = ({ show = true, lineWidth = 0, fill = 1 }, color) => ({ + barSeriesStyle: { + rectBorder: { + stroke: color || DEFAULT_COLOR, + strokeWidth: lineWidth, + visible: show, + }, + rect: { + fill: color || DEFAULT_COLOR, + opacity: fill, + }, + }, +}); + +export const getSeriesColors = (color, specId) => { + const map = new Map(); + const seriesColorsValues = { specId, colorValues: [] }; + + map.set(seriesColorsValues, color); + + return map; +}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.test.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.test.js new file mode 100644 index 0000000000000..ac0a7610f2660 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.test.js @@ -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 { getBarStyles, getSeriesColors, getAreaStyles } from './series_styles'; + +describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/series_styles.js', () => { + let bars; + let color; + let specId; + let points; + let lines; + + beforeEach(() => { + bars = { + fill: 0.5, + lineWidth: 2, + show: true, + }; + color = 'rgb(224, 0, 221)'; + specId = 'IT'; + points = { + lineWidth: 1, + show: true, + radius: 1, + }; + lines = { + fill: 0, + lineWidth: 1, + show: true, + steps: true, + }; + }); + + describe('getBarStyles()', () => { + test('should match a snapshot', () => { + expect(getBarStyles(bars, color)).toMatchSnapshot(); + }); + + test('should set default values if bars and colors are empty', () => { + bars = {}; + color = ''; + + expect(getBarStyles(bars, color)).toMatchSnapshot(); + }); + }); + + describe('getSeriesColors()', () => { + test('should match a snapshot', () => { + expect(getSeriesColors(color, specId)).toMatchSnapshot(); + }); + }); + + describe('getAreaStyles()', () => { + test('should match a snapshot', () => { + expect(getAreaStyles({ points, lines, color })).toMatchSnapshot(); + }); + + test('should set default values if points, lines and color are empty', () => { + points = {}; + lines = {}; + color = ''; + + expect(getAreaStyles({ points, lines, color })).toMatchSnapshot(); + }); + }); +}); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/stack_format.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/stack_format.js new file mode 100644 index 0000000000000..20c655c995a5e --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/stack_format.js @@ -0,0 +1,31 @@ +/* + * 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 { STACK_ACCESSORS, STACKED_OPTIONS } from '../../../constants'; + +export const getStackAccessors = stack => { + switch (stack) { + case STACKED_OPTIONS.STACKED: + case STACKED_OPTIONS.STACKED_WITHIN_SERIES: + case STACKED_OPTIONS.PERCENT: + return STACK_ACCESSORS; + default: + return undefined; + } +}; diff --git a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calcualte_bar_width.test.js b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/stack_format.test.js similarity index 50% rename from src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calcualte_bar_width.test.js rename to src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/stack_format.test.js index bf5fd0174e96b..aecdb9324d958 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/lib/__tests__/calcualte_bar_width.test.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/stack_format.test.js @@ -17,17 +17,21 @@ * under the License. */ -import { expect } from 'chai'; -import { calculateBarWidth } from '../calculate_bar_width'; +import { getStackAccessors } from './stack_format'; +import { X_ACCESSOR_INDEX, STACKED_OPTIONS } from '../../../constants'; -describe('calculateBarWidth(series, divisor, multiplier)', () => { - it('returns default bar width', () => { - const series = [{ data: [[100, 100], [200, 100]] }]; - expect(calculateBarWidth(series)).to.equal(70); - }); +describe('src/legacy/core_plugins/metrics/public/visualizations/views/timeseries/utils/stack_format.js', () => { + describe('getStackAccessors()', () => { + test('should return an accessor if the stack is stacked', () => { + expect(getStackAccessors(STACKED_OPTIONS.STACKED)).toEqual([X_ACCESSOR_INDEX]); + }); + + test('should return an accessor if the stack is percent', () => { + expect(getStackAccessors(STACKED_OPTIONS.PERCENT)).toEqual([X_ACCESSOR_INDEX]); + }); - it('returns custom bar width', () => { - const series = [{ data: [[100, 100], [200, 100]] }]; - expect(calculateBarWidth(series, 2)).to.equal(200); + test('should return undefined if the stack does not match with STACKED and PERCENT', () => { + expect(getStackAccessors(STACKED_OPTIONS.NONE)).toBeUndefined(); + }); }); }); diff --git a/src/legacy/core_plugins/metrics/public/visualizations/components/top_n.js b/src/legacy/core_plugins/metrics/public/visualizations/views/top_n.js similarity index 63% rename from src/legacy/core_plugins/metrics/public/visualizations/components/top_n.js rename to src/legacy/core_plugins/metrics/public/visualizations/views/top_n.js index ea8f795b7620d..5734d101a09d2 100644 --- a/src/legacy/core_plugins/metrics/public/visualizations/components/top_n.js +++ b/src/legacy/core_plugins/metrics/public/visualizations/views/top_n.js @@ -22,6 +22,12 @@ import React, { Component } from 'react'; import { getLastValue } from '../../../common/get_last_value'; import reactcss from 'reactcss'; +const RENDER_MODES = { + POSITIVE: 'positive', + NEGATIVE: 'negative', + MIXED: 'mixed', +}; + export class TopN extends Component { constructor(props) { super(props); @@ -59,20 +65,61 @@ export class TopN extends Component { }; } - renderRow(maxValue) { + static getRenderMode = (min, max) => { + if (min >= 0) { + return RENDER_MODES.POSITIVE; + } else if (max < 0) { + return RENDER_MODES.NEGATIVE; + } + return RENDER_MODES.MIXED; + }; + + static calcInnerBarStyles = (renderMode, isPositive) => { + if (renderMode === RENDER_MODES.MIXED) { + return { + [isPositive ? 'marginLeft' : 'marginRight']: '50%', + }; + } + return {}; + }; + + static calcInnerBarDivStyles = (item, width, isPositive) => { + return { + backgroundColor: item.color, + width: width + '%', + float: isPositive ? 'left' : 'right', + }; + }; + + static calcDomain = (renderMode, min, max) => { + if (renderMode === RENDER_MODES.MIXED) { + return Math.max(max, Math.abs(min)); + } else if (renderMode === RENDER_MODES.NEGATIVE) { + return Math.abs(min); + } + + return max; + }; + + renderRow({ min, max }) { return item => { + const renderMode = TopN.getRenderMode(min, max); const key = `${item.id || item.label}`; const lastValue = getLastValue(item.data); const formatter = item.tickFormatter || this.props.tickFormatter; - const value = formatter(lastValue); - const width = `${100 * (lastValue / maxValue)}%`; - const backgroundColor = item.color; + const isPositiveValue = lastValue >= 0; + + const intervalLength = TopN.calcDomain(renderMode, min, max); + const width = 100 * (Math.abs(lastValue) / intervalLength); + const styles = reactcss( { default: { innerBar: { - width, - backgroundColor, + ...TopN.calcInnerBarStyles(renderMode, isPositiveValue), + }, + innerBarValue: { + ...TopN.calcInnerBarDivStyles(item, width, isPositiveValue), }, label: { maxWidth: this.state.labelMaxWidth, @@ -92,10 +139,12 @@ export class TopN extends Component { {item.label} -
+
+
+
- {value} + {formatter(lastValue)} ); @@ -104,12 +153,20 @@ export class TopN extends Component { render() { if (!this.props.series) return null; - const maxValue = this.props.series.reduce((max, series) => { - const lastValue = getLastValue(series.data); - return lastValue > max ? lastValue : max; - }, 0); - const rows = this.props.series.map(this.renderRow(maxValue)); + const intervalSettings = this.props.series.reduce( + (acc, series, index) => { + const value = getLastValue(series.data); + + return { + min: !index || value < acc.min ? value : acc.min, + max: !index || value > acc.max ? value : acc.max, + }; + }, + { min: undefined, max: undefined } + ); + + const rows = this.props.series.map(this.renderRow(intervalSettings)); let className = 'tvbVisTopN'; if (this.props.reversed) { className += ' tvbVisTopN--reversed'; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_default_decoration.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_default_decoration.js index c9c4f24fa566c..5cc94dda6d21a 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_default_decoration.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/__tests__/helpers/get_default_decoration.js @@ -22,35 +22,34 @@ import { getDefaultDecoration } from '../../helpers/get_default_decoration'; describe('getDefaultDecoration', () => { describe('stack option', () => { - it('should set a stack option to false', () => { + it('should set a stack option to none', () => { const series = { id: 'test_id', + stacked: 'none', }; - expect(getDefaultDecoration(series)).to.have.property('stack', false); - - series.stacked = 'none'; - expect(getDefaultDecoration(series)).to.have.property('stack', false); + expect(getDefaultDecoration(series)).to.have.property('stack', 'none'); }); - it('should set a stack option to true', () => { + it('should set a stack option to stacked/percent', () => { const series = { stacked: 'stacked', id: 'test_id', }; - expect(getDefaultDecoration(series)).to.have.property('stack', true); + expect(getDefaultDecoration(series)).to.have.property('stack', 'stacked'); series.stacked = 'percent'; - expect(getDefaultDecoration(series)).to.have.property('stack', true); + + expect(getDefaultDecoration(series)).to.have.property('stack', 'percent'); }); - it('should set a stack option to be series id', () => { + it('should set a stack option to stacked_within_series', () => { const series = { stacked: 'stacked_within_series', id: 'test_id', }; - expect(getDefaultDecoration(series)).to.have.property('stack', series.id); + expect(getDefaultDecoration(series)).to.have.property('stack', 'stacked_within_series'); }); }); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_default_decoration.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_default_decoration.js index 2f3b8959d5d68..b63eb5488a755 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_default_decoration.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_default_decoration.js @@ -21,21 +21,10 @@ export const getDefaultDecoration = series => { const pointSize = series.point_size != null ? Number(series.point_size) : Number(series.line_width); const showPoints = series.chart_type === 'line' && pointSize !== 0; - let stack; - switch (series.stacked) { - case 'stacked': - case 'percent': - stack = true; - break; - case 'stacked_within_series': - stack = series.id; - break; - default: - stack = false; - } return { - stack, + seriesId: series.id, + stack: series.stacked, lines: { show: series.chart_type === 'line' && series.line_width !== 0, fill: Number(series.fill), diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/series_agg.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/series_agg.js index 2bdcdf0fdabd3..17dbde16e306e 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/series_agg.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/series_agg.js @@ -109,6 +109,7 @@ describe('seriesAgg(resp, panel, series)', () => { color: '#F00', label: 'Total CPU', stack: false, + seriesId: 'test', lines: { show: true, fill: 0, lineWidth: 1, steps: false }, points: { show: true, radius: 1, lineWidth: 1 }, bars: { fill: 0, lineWidth: 1, show: false }, diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js index af24eb1be81c4..f8c98adc5d023 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/__tests__/std_sibling.js @@ -98,6 +98,7 @@ describe('stdSibling(resp, panel, series)', () => { label: 'Overall Std. Deviation of Average of cpu', color: 'rgb(255, 0, 0)', stack: false, + seriesId: 'test', lines: { show: true, fill: 0, lineWidth: 1, steps: false }, points: { show: true, radius: 1, lineWidth: 1 }, bars: { fill: 0, lineWidth: 1, show: false }, 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 a5c2296bd8b99..b0b40f8ff704e 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 @@ -39,7 +39,7 @@ import afterdatachangePng from './afterdatachange.png'; import afterdatachangeandresizePng from './afterdatachangeandresize.png'; import aftercolorchangePng from './aftercolorchange.png'; import changestartupPng from './changestartup.png'; -import { visualizations } from '../../../visualizations/public'; +import { setup } from '../../../visualizations/public/np_ready/public/legacy'; import { createRegionMapVisualization } from '../region_map_visualization'; import { createRegionMapTypeDefinition } from '../region_map_type'; @@ -48,7 +48,6 @@ const THRESHOLD = 0.45; const PIXEL_DIFF = 96; describe('RegionMapsVisualizationTests', function () { - let domNode; let RegionMapsVisualization; let Vis; @@ -61,83 +60,87 @@ describe('RegionMapsVisualizationTests', function () { const _makeJsonAjaxCallOld = ChoroplethLayer.prototype._makeJsonAjaxCall; const dummyTableGroup = { - columns: [{ - 'id': 'col-0', - 'aggConfig': { - 'id': '2', - 'enabled': true, - 'type': 'terms', - 'schema': 'segment', - 'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' } - }, 'title': 'geo.dest: Descending' - }, { - 'id': 'col-1', - 'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} }, - 'title': 'Count' - }], + columns: [ + { + id: 'col-0', + aggConfig: { + id: '2', + enabled: true, + type: 'terms', + schema: 'segment', + params: { field: 'geo.dest', size: 5, order: 'desc', orderBy: '1' }, + }, + title: 'geo.dest: Descending', + }, + { + id: 'col-1', + aggConfig: { id: '1', enabled: true, type: 'count', schema: 'metric', params: {} }, + title: 'Count', + }, + ], rows: [ { 'col-0': 'CN', 'col-1': 26 }, { 'col-0': 'IN', 'col-1': 17 }, { 'col-0': 'US', 'col-1': 6 }, { 'col-0': 'DE', 'col-1': 4 }, - { 'col-0': 'BR', 'col-1': 3 } - ] + { 'col-0': 'BR', 'col-1': 3 }, + ], }; beforeEach(ngMock.module('kibana')); let getManifestStub; - beforeEach(ngMock.inject((Private, $injector) => { - const serviceSettings = $injector.get('serviceSettings'); - const uiSettings = $injector.get('config'); - const regionmapsConfig = { - includeElasticMapsService: true, - layers: [] - }; - - dependencies = { - serviceSettings, - $injector, - regionmapsConfig, - uiSettings - }; - - visualizations.types.VisTypesRegistryProvider.register(() => - createRegionMapTypeDefinition(dependencies) - ); - - Vis = Private(visModule.VisProvider); - RegionMapsVisualization = createRegionMapVisualization(dependencies); - indexPattern = Private(LogstashIndexPatternStubProvider); - - ChoroplethLayer.prototype._makeJsonAjaxCall = async function () { - //simulate network call - return new Promise((resolve)=> { - setTimeout(() => { - resolve(worldJson); - }, 10); - }); - }; - - getManifestStub = serviceSettings.__debugStubManifestCalls(async (url) => { - //simulate network calls - if (url.startsWith('https://foobar')) { - return EMS_CATALOGUE; - } else if (url.startsWith('https://tiles.foobar')) { - return EMS_TILES; - } else if (url.startsWith('https://files.foobar')) { - return EMS_FILES; - } else if (url.startsWith('https://raster-style.foobar')) { - if (url.includes('osm-bright-desaturated')) { - return EMS_STYLE_ROAD_MAP_DESATURATED; - } else if (url.includes('osm-bright')) { - return EMS_STYLE_ROAD_MAP_BRIGHT; - } else if (url.includes('dark-matter')) { - return EMS_STYLE_DARK_MAP; + beforeEach( + ngMock.inject((Private, $injector) => { + const serviceSettings = $injector.get('serviceSettings'); + const uiSettings = $injector.get('config'); + const regionmapsConfig = { + includeElasticMapsService: true, + layers: [], + }; + + dependencies = { + serviceSettings, + $injector, + regionmapsConfig, + uiSettings, + }; + + setup.types.registerVisualization(() => createRegionMapTypeDefinition(dependencies)); + + Vis = Private(visModule.VisProvider); + RegionMapsVisualization = createRegionMapVisualization(dependencies); + indexPattern = Private(LogstashIndexPatternStubProvider); + + ChoroplethLayer.prototype._makeJsonAjaxCall = async function () { + //simulate network call + return new Promise(resolve => { + setTimeout(() => { + resolve(worldJson); + }, 10); + }); + }; + + getManifestStub = serviceSettings.__debugStubManifestCalls(async url => { + //simulate network calls + if (url.startsWith('https://foobar')) { + return EMS_CATALOGUE; + } else if (url.startsWith('https://tiles.foobar')) { + return EMS_TILES; + } else if (url.startsWith('https://files.foobar')) { + return EMS_FILES; + } else if (url.startsWith('https://raster-style.foobar')) { + if (url.includes('osm-bright-desaturated')) { + return EMS_STYLE_ROAD_MAP_DESATURATED; + } else if (url.includes('osm-bright')) { + return EMS_STYLE_ROAD_MAP_BRIGHT; + } else if (url.includes('dark-matter')) { + return EMS_STYLE_DARK_MAP; + } } - } - }); - })); + }); + }) + ); afterEach(function () { ChoroplethLayer.prototype._makeJsonAjaxCall = _makeJsonAjaxCallOld; @@ -145,15 +148,13 @@ describe('RegionMapsVisualizationTests', function () { }); describe('RegionMapVisualization - basics', function () { - beforeEach(async function () { setupDOM('512px', '512px'); imageComparator = new ImageComparator(); - vis = new Vis(indexPattern, { - type: 'region_map' + type: 'region_map', }); vis.params.bucket = { @@ -163,19 +164,25 @@ describe('RegionMapsVisualizationTests', function () { accessor: 1, }; - vis.params.selectedJoinField = { 'name': 'iso2', 'description': 'Two letter abbreviation' }; + vis.params.selectedJoinField = { name: 'iso2', description: 'Two letter abbreviation' }; vis.params.selectedLayer = { - 'attribution': '

Made with NaturalEarth | Elastic Maps Service

', - 'name': 'World Countries', - 'format': 'geojson', - 'url': 'https://vector-staging.maps.elastic.co/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1', - 'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, { - 'name': 'iso3', - 'description': 'Three letter abbreviation' - }, { 'name': 'name', 'description': 'Country name' }], - 'created_at': '2017-07-31T16:00:19.996450', - 'id': 5715999101812736, - 'layerId': 'elastic_maps_service.World Countries' + attribution: + '

Made with NaturalEarth | Elastic Maps Service

', + name: 'World Countries', + format: 'geojson', + url: + 'https://vector-staging.maps.elastic.co/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1', + fields: [ + { name: 'iso2', description: 'Two letter abbreviation' }, + { + name: 'iso3', + description: 'Three letter abbreviation', + }, + { name: 'name', description: 'Country name' }, + ], + created_at: '2017-07-31T16:00:19.996450', + id: 5715999101812736, + layerId: 'elastic_maps_service.World Countries', }; }); @@ -184,7 +191,6 @@ describe('RegionMapsVisualizationTests', function () { imageComparator.destroy(); }); - it('should instantiate at zoom level 2', async function () { const regionMapsVisualization = new RegionMapsVisualization(domNode, vis); await regionMapsVisualization.render(dummyTableGroup, vis.params, { @@ -192,7 +198,7 @@ describe('RegionMapsVisualizationTests', function () { params: true, aggs: true, data: true, - uiState: false + uiState: false, }); const mismatchedPixels = await compareImage(initialPng); regionMapsVisualization.destroy(); @@ -206,35 +212,33 @@ describe('RegionMapsVisualizationTests', function () { params: true, aggs: true, data: true, - uiState: false + uiState: false, }); //this will actually create an empty image - vis.params.selectedJoinField = { 'name': 'iso3', 'description': 'Three letter abbreviation' }; - vis.params.isDisplayWarning = false;//so we don't get notifications + vis.params.selectedJoinField = { name: 'iso3', description: 'Three letter abbreviation' }; + vis.params.isDisplayWarning = false; //so we don't get notifications await regionMapsVisualization.render(dummyTableGroup, vis.params, { resize: false, params: true, aggs: false, data: false, - uiState: false + uiState: false, }); const mismatchedPixels = await compareImage(toiso3Png); regionMapsVisualization.destroy(); expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); it('should resize', async function () { - const regionMapsVisualization = new RegionMapsVisualization(domNode, vis); await regionMapsVisualization.render(dummyTableGroup, vis.params, { resize: false, params: true, aggs: true, data: true, - uiState: false + uiState: false, }); domNode.style.width = '256px'; @@ -244,7 +248,7 @@ describe('RegionMapsVisualizationTests', function () { params: false, aggs: false, data: false, - uiState: false + uiState: false, }); const mismatchedPixelsAfterFirstResize = await compareImage(afterresizePng); @@ -255,7 +259,7 @@ describe('RegionMapsVisualizationTests', function () { params: false, aggs: false, data: false, - uiState: false + uiState: false, }); const mismatchedPixelsAfterSecondResize = await compareImage(initialPng); @@ -265,32 +269,31 @@ describe('RegionMapsVisualizationTests', function () { }); it('should redo data', async function () { - const regionMapsVisualization = new RegionMapsVisualization(domNode, vis); await regionMapsVisualization.render(dummyTableGroup, vis.params, { resize: false, params: true, aggs: true, data: true, - uiState: false + uiState: false, }); const newTableGroup = _.cloneDeep(dummyTableGroup); - newTableGroup.rows.pop();//remove one shape + newTableGroup.rows.pop(); //remove one shape await regionMapsVisualization.render(newTableGroup, vis.params, { resize: false, params: false, aggs: false, data: true, - uiState: false + uiState: false, }); const mismatchedPixelsAfterDataChange = await compareImage(afterdatachangePng); const anotherTableGroup = _.cloneDeep(newTableGroup); - anotherTableGroup.rows.pop();//remove one shape + anotherTableGroup.rows.pop(); //remove one shape domNode.style.width = '412px'; domNode.style.height = '112px'; await regionMapsVisualization.render(anotherTableGroup, vis.params, { @@ -298,9 +301,11 @@ describe('RegionMapsVisualizationTests', function () { params: false, aggs: false, data: true, - uiState: false + uiState: false, }); - const mismatchedPixelsAfterDataChangeAndResize = await compareImage(afterdatachangeandresizePng); + const mismatchedPixelsAfterDataChangeAndResize = await compareImage( + afterdatachangeandresizePng + ); regionMapsVisualization.destroy(); expect(mismatchedPixelsAfterDataChange).to.be.lessThan(PIXEL_DIFF); @@ -314,18 +319,18 @@ describe('RegionMapsVisualizationTests', function () { params: true, aggs: true, data: true, - uiState: false + uiState: false, }); const newTableGroup = _.cloneDeep(dummyTableGroup); - newTableGroup.rows.pop();//remove one shape + newTableGroup.rows.pop(); //remove one shape vis.params.colorSchema = 'Blues'; await regionMapsVisualization.render(newTableGroup, vis.params, { resize: false, params: true, aggs: false, data: true, - uiState: false + uiState: false, }); const mismatchedPixelsAfterDataAndColorChange = await compareImage(aftercolorchangePng); @@ -342,7 +347,7 @@ describe('RegionMapsVisualizationTests', function () { params: true, aggs: true, data: true, - uiState: false + uiState: false, }); const mismatchedPixels = await compareImage(changestartupPng); @@ -350,7 +355,6 @@ describe('RegionMapsVisualizationTests', function () { expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); }); - }); async function compareImage(expectedImageSource) { diff --git a/src/legacy/core_plugins/region_map/public/legacy.ts b/src/legacy/core_plugins/region_map/public/legacy.ts index 5e01e7ee56e2f..7adbc2117d7ee 100644 --- a/src/legacy/core_plugins/region_map/public/legacy.ts +++ b/src/legacy/core_plugins/region_map/public/legacy.ts @@ -20,7 +20,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { visualizations } from '../../visualizations/public'; +import { setup as setupVisualizations } from '../../visualizations/public/np_ready/public/legacy'; import { RegionMapPluginSetupDependencies, RegionMapsConfig } from './plugin'; import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; @@ -30,7 +30,7 @@ const regionmapsConfig = npSetup.core.injectedMetadata.getInjectedVar( ) as RegionMapsConfig; const plugins: Readonly = { - visualizations, + visualizations: setupVisualizations, data: npSetup.plugins.data, // Temporary solution diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/legacy/core_plugins/region_map/public/plugin.ts index 995a96b7a19c7..4afee7269c578 100644 --- a/src/legacy/core_plugins/region_map/public/plugin.ts +++ b/src/legacy/core_plugins/region_map/public/plugin.ts @@ -24,7 +24,7 @@ import { UiSettingsClientContract, } from '../../../../core/public'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; @@ -70,7 +70,7 @@ export class RegionMapPlugin implements Plugin, void> { data.expressions.registerFunction(createRegionMapFn); - visualizations.types.VisTypesRegistryProvider.register(() => + visualizations.types.registerVisualization(() => createRegionMapTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/region_map/public/region_map_type.js b/src/legacy/core_plugins/region_map/public/region_map_type.js index 10aaeb1060bc0..9b28aa96eccbc 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_type.js +++ b/src/legacy/core_plugins/region_map/public/region_map_type.js @@ -25,7 +25,7 @@ import { createRegionMapVisualization } from './region_map_visualization'; import { Status } from 'ui/vis/update_status'; import { RegionMapOptions } from './components/region_map_options'; -import { visFactory } from '../../visualizations/public'; +import { visFactory } from '../../visualizations/public/np_ready/public'; // TODO: reference to TILE_MAP plugin should be removed import { ORIGIN } from '../../tile_map/common/origin'; @@ -40,8 +40,11 @@ export function createRegionMapTypeDefinition(dependencies) { return visFactory.createBaseVisualization({ name: 'region_map', title: i18n.translate('regionMap.mapVis.regionMapTitle', { defaultMessage: 'Region Map' }), - description: i18n.translate('regionMap.mapVis.regionMapDescription', { defaultMessage: 'Show metrics on a thematic map. Use one of the \ -provided base maps, or add your own. Darker colors represent higher values.' }), + description: i18n.translate('regionMap.mapVis.regionMapDescription', { + defaultMessage: + 'Show metrics on a thematic map. Use one of the \ +provided base maps, or add your own. Darker colors represent higher values.', + }), icon: 'visMapRegion', visConfig: { defaults: { @@ -56,45 +59,59 @@ provided base maps, or add your own. Darker colors represent higher values.' }), mapZoom: 2, mapCenter: [0, 0], outlineWeight: 1, - showAllShapes: true//still under consideration - } + showAllShapes: true, //still under consideration + }, }, requiresUpdateStatus: [Status.AGGS, Status.PARAMS, Status.RESIZE, Status.DATA, Status.UI_STATE], visualization, editorConfig: { - optionsTemplate: (props) => - ( ( + ), + /> + ), collections: { colorSchemas, vectorLayers, - tmsLayers: [] + tmsLayers: [], }, schemas: new Schemas([ { group: 'metrics', name: 'metric', - title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.metricTitle', { defaultMessage: 'Value' }), + title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.metricTitle', { + defaultMessage: 'Value', + }), min: 1, max: 1, - aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits', - 'sum_bucket', 'min_bucket', 'max_bucket', 'avg_bucket'], - defaults: [ - { schema: 'metric', type: 'count' } - ] + aggFilter: [ + 'count', + 'avg', + 'sum', + 'min', + 'max', + 'cardinality', + 'top_hits', + 'sum_bucket', + 'min_bucket', + 'max_bucket', + 'avg_bucket', + ], + defaults: [{ schema: 'metric', type: 'count' }], }, { group: 'buckets', name: 'segment', - title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.segmentTitle', { defaultMessage: 'Shape field' }), + title: i18n.translate('regionMap.mapVis.regionMapEditorConfig.schemas.segmentTitle', { + defaultMessage: 'Shape field', + }), min: 1, max: 1, - aggFilter: ['terms'] - } - ]) - } + aggFilter: ['terms'], + }, + ]), + }, }); } diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 53b627d39595e..5f2abd8c9e083 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -85,6 +85,7 @@ const coreSystem = new CoreSystem({ injectedMetadata: { version: '1.2.3', buildNumber: 1234, + legacyMode: true, legacyMetadata: { nav: [], version: '1.2.3', 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 6b3bdd6c8f993..e32df04a583c2 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 @@ -33,7 +33,7 @@ import EMS_TILES from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_ import EMS_STYLE_ROAD_MAP_BRIGHT from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_bright'; import EMS_STYLE_ROAD_MAP_DESATURATED from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated'; import EMS_STYLE_DARK_MAP from '../../../../ui/public/vis/__tests__/map/ems_mocks/sample_style_dark'; -import { visualizations } from '../../../visualizations/public'; +import { setup } from '../../../visualizations/public/np_ready/public/legacy'; import { createTileMapVisualization } from '../tile_map_visualization'; import { createTileMapTypeDefinition } from '../tile_map_type'; @@ -62,7 +62,6 @@ const THRESHOLD = 0.45; const PIXEL_DIFF = 64; describe('CoordinateMapsVisualizationTest', function () { - let domNode; let CoordinateMapsVisualization; let Vis; @@ -72,59 +71,57 @@ describe('CoordinateMapsVisualizationTest', function () { let imageComparator; - let getManifestStub; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject((Private, $injector) => { - const serviceSettings = $injector.get('serviceSettings'); - const uiSettings = $injector.get('config'); - - dependencies = { - serviceSettings, - uiSettings, - $injector - }; - - visualizations.types.VisTypesRegistryProvider.register(() => - createTileMapTypeDefinition(dependencies) - ); - - Vis = Private(visModule.VisProvider); - CoordinateMapsVisualization = createTileMapVisualization(dependencies); - indexPattern = Private(LogstashIndexPatternStubProvider); - - getManifestStub = serviceSettings.__debugStubManifestCalls(async (url) => { - //simulate network calls - if (url.startsWith('https://foobar')) { - return EMS_CATALOGUE; - } else if (url.startsWith('https://tiles.foobar')) { - return EMS_TILES; - } else if (url.startsWith('https://files.foobar')) { - return EMS_FILES; - } else if (url.startsWith('https://raster-style.foobar')) { - if (url.includes('osm-bright-desaturated')) { - return EMS_STYLE_ROAD_MAP_DESATURATED; - } else if (url.includes('osm-bright')) { - return EMS_STYLE_ROAD_MAP_BRIGHT; - } else if (url.includes('dark-matter')) { - return EMS_STYLE_DARK_MAP; + beforeEach( + ngMock.inject((Private, $injector) => { + const serviceSettings = $injector.get('serviceSettings'); + const uiSettings = $injector.get('config'); + + dependencies = { + serviceSettings, + uiSettings, + $injector, + }; + + setup.types.registerVisualization(() => createTileMapTypeDefinition(dependencies)); + + Vis = Private(visModule.VisProvider); + CoordinateMapsVisualization = createTileMapVisualization(dependencies); + indexPattern = Private(LogstashIndexPatternStubProvider); + + getManifestStub = serviceSettings.__debugStubManifestCalls(async url => { + //simulate network calls + if (url.startsWith('https://foobar')) { + return EMS_CATALOGUE; + } else if (url.startsWith('https://tiles.foobar')) { + return EMS_TILES; + } else if (url.startsWith('https://files.foobar')) { + return EMS_FILES; + } else if (url.startsWith('https://raster-style.foobar')) { + if (url.includes('osm-bright-desaturated')) { + return EMS_STYLE_ROAD_MAP_DESATURATED; + } else if (url.includes('osm-bright')) { + return EMS_STYLE_ROAD_MAP_BRIGHT; + } else if (url.includes('dark-matter')) { + return EMS_STYLE_DARK_MAP; + } } - } - }); - })); + }); + }) + ); afterEach(() => { getManifestStub.removeStub(); }); describe('CoordinateMapsVisualization - basics', function () { - beforeEach(async function () { setupDOM('512px', '512px'); imageComparator = new ImageComparator(); vis = new Vis(indexPattern, { - type: 'tile_map' + type: 'tile_map', }); vis.params = { mapType: 'Scaled Circle Markers', @@ -136,32 +133,33 @@ describe('CoordinateMapsVisualizationTest', function () { mapCenter: [0, 0], }; const mockAggs = { - byType: (type) => { + byType: type => { return mockAggs.aggs.find(agg => agg.type.type === type); }, aggs: [ { type: { - type: 'metrics' + type: 'metrics', }, - fieldFormatter: (x) => { + fieldFormatter: x => { return x; }, makeLabel: () => { return 'foobar'; - } - }, { + }, + }, + { type: { - type: 'buckets' + type: 'buckets', }, - params: { useGeoCentroid: true } - }] + params: { useGeoCentroid: true }, + }, + ], }; vis.getAggConfig = function () { return mockAggs; }; vis.aggs = mockAggs; - }); afterEach(function () { @@ -176,7 +174,7 @@ describe('CoordinateMapsVisualizationTest', function () { params: true, aggs: true, data: true, - uiState: false + uiState: false, }); const mismatchedPixels = await compareImage(initial, 0); @@ -184,43 +182,38 @@ describe('CoordinateMapsVisualizationTest', function () { expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); }); - it('should toggle to Heatmap OK', async function () { - const coordinateMapVisualization = new CoordinateMapsVisualization(domNode, vis); await coordinateMapVisualization.render(dummyESResponse, vis.params, { resize: false, params: true, aggs: true, data: true, - uiState: false + uiState: false, }); - vis.params.mapType = 'Heatmap'; await coordinateMapVisualization.render(dummyESResponse, vis.params, { resize: false, params: true, aggs: false, data: false, - uiState: false + uiState: false, }); const mismatchedPixels = await compareImage(heatmapRaw, 1); coordinateMapVisualization.destroy(); expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); it('should toggle back&forth OK between mapTypes', async function () { - const coordinateMapVisualization = new CoordinateMapsVisualization(domNode, vis); await coordinateMapVisualization.render(dummyESResponse, vis.params, { resize: false, params: true, aggs: true, data: true, - uiState: false + uiState: false, }); vis.params.mapType = 'Heatmap'; @@ -229,7 +222,7 @@ describe('CoordinateMapsVisualizationTest', function () { params: true, aggs: false, data: false, - uiState: false + uiState: false, }); vis.params.mapType = 'Scaled Circle Markers'; @@ -238,54 +231,48 @@ describe('CoordinateMapsVisualizationTest', function () { params: true, aggs: false, data: false, - uiState: false + uiState: false, }); const mismatchedPixels = await compareImage(initial, 0); coordinateMapVisualization.destroy(); expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); it('should toggle to different color schema ok', async function () { - const coordinateMapVisualization = new CoordinateMapsVisualization(domNode, vis); await coordinateMapVisualization.render(dummyESResponse, vis.params, { resize: false, params: true, aggs: true, data: true, - uiState: false + uiState: false, }); - vis.params.colorSchema = 'Blues'; await coordinateMapVisualization.render(dummyESResponse, vis.params, { resize: false, params: true, aggs: false, data: false, - uiState: false + uiState: false, }); const mismatchedPixels = await compareImage(blues, 0); coordinateMapVisualization.destroy(); expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); it('should toggle to different color schema and maptypes ok', async function () { - const coordinateMapVisualization = new CoordinateMapsVisualization(domNode, vis); await coordinateMapVisualization.render(dummyESResponse, vis.params, { resize: false, params: true, aggs: true, data: true, - uiState: false + uiState: false, }); - vis.params.colorSchema = 'Greens'; vis.params.mapType = 'Shaded Geohash Grid'; await coordinateMapVisualization.render(dummyESResponse, vis.params, { @@ -293,26 +280,21 @@ describe('CoordinateMapsVisualizationTest', function () { params: true, aggs: false, data: false, - uiState: false + uiState: false, }); const mismatchedPixels = await compareImage(shadedGeohashGrid, 0); coordinateMapVisualization.destroy(); expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); - - }); - async function compareImage(expectedImageSource, index) { const elementList = domNode.querySelectorAll('canvas'); const firstCanvasOnMap = elementList[index]; return imageComparator.compareImage(firstCanvasOnMap, expectedImageSource, THRESHOLD); } - function setupDOM(width, height) { domNode = document.createElement('div'); domNode.style.top = '0'; @@ -329,6 +311,4 @@ describe('CoordinateMapsVisualizationTest', function () { domNode.innerHTML = ''; document.body.removeChild(domNode); } - }); - diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx index d0cff4013b0e7..096266502417d 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx @@ -31,6 +31,7 @@ import { } from '../../../kbn_vislib_vis_types/public/components'; import { WmsOptions } from './wms_options'; import { TileMapVisParams } from '../types'; +import { MapTypes } from '../map_types'; export type TileMapOptionsProps = { serviceSettings: ServiceSettings } & VisOptionsProps< TileMapVisParams @@ -58,17 +59,7 @@ function TileMapOptions(props: TileMapOptionsProps) { setValue={setValue} /> - - - {stateParams.mapType === 'Heatmap' && ( + {stateParams.mapType === MapTypes.Heatmap ? ( + ) : ( + )} diff --git a/src/legacy/core_plugins/tile_map/public/geohash_layer.js b/src/legacy/core_plugins/tile_map/public/geohash_layer.js index 5ed00ee942f2a..287f4bb3b15ae 100644 --- a/src/legacy/core_plugins/tile_map/public/geohash_layer.js +++ b/src/legacy/core_plugins/tile_map/public/geohash_layer.js @@ -26,6 +26,7 @@ import { HeatmapMarkers } from './markers/heatmap'; import { ScaledCirclesMarkers } from './markers/scaled_circles'; import { ShadedCirclesMarkers } from './markers/shaded_circles'; import { GeohashGridMarkers } from './markers/geohash_grid'; +import { MapTypes } from './map_types'; export class GeohashLayer extends KibanaMapLayer { @@ -54,19 +55,19 @@ export class GeohashLayer extends KibanaMapLayer { colorRamp: this._geohashOptions.colorRamp }; switch (this._geohashOptions.mapType) { - case 'Scaled Circle Markers': + case MapTypes.ScaledCircleMarkers: this._geohashMarkers = new ScaledCirclesMarkers(this._featureCollection, this._featureCollectionMetaData, markerOptions, this._zoom, this._kibanaMap); break; - case 'Shaded Circle Markers': + case MapTypes.ShadedCircleMarkers: this._geohashMarkers = new ShadedCirclesMarkers(this._featureCollection, this._featureCollectionMetaData, markerOptions, this._zoom, this._kibanaMap); break; - case 'Shaded Geohash Grid': + case MapTypes.ShadedGeohashGrid: this._geohashMarkers = new GeohashGridMarkers(this._featureCollection, this._featureCollectionMetaData, markerOptions, this._zoom, this._kibanaMap); break; - case 'Heatmap': + case MapTypes.Heatmap: let radius = 15; if (this._featureCollectionMetaData.geohashGridDimensionsAtEquator) { diff --git a/src/legacy/core_plugins/tile_map/public/legacy.ts b/src/legacy/core_plugins/tile_map/public/legacy.ts index 3237ab4373720..cf2e702cd09b6 100644 --- a/src/legacy/core_plugins/tile_map/public/legacy.ts +++ b/src/legacy/core_plugins/tile_map/public/legacy.ts @@ -20,13 +20,13 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { visualizations } from '../../visualizations/public'; +import { setup as setupVisualizations } from '../../visualizations/public/np_ready/public/legacy'; import { TileMapPluginSetupDependencies } from './plugin'; import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const plugins: Readonly = { - visualizations, + visualizations: setupVisualizations, data: npSetup.plugins.data, // Temporary solution diff --git a/src/legacy/core_plugins/tile_map/public/map_types.ts b/src/legacy/core_plugins/tile_map/public/map_types.ts new file mode 100644 index 0000000000000..d4240fc8a579a --- /dev/null +++ b/src/legacy/core_plugins/tile_map/public/map_types.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. + */ + +export enum MapTypes { + ScaledCircleMarkers = 'Scaled Circle Markers', + ShadedCircleMarkers = 'Shaded Circle Markers', + ShadedGeohashGrid = 'Shaded Geohash Grid', + Heatmap = 'Heatmap', +} diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/legacy/core_plugins/tile_map/public/plugin.ts index f5ce0df297125..1309c37a4c73a 100644 --- a/src/legacy/core_plugins/tile_map/public/plugin.ts +++ b/src/legacy/core_plugins/tile_map/public/plugin.ts @@ -24,7 +24,7 @@ import { UiSettingsClientContract, } from '../../../../core/public'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; @@ -64,7 +64,7 @@ export class TileMapPlugin implements Plugin, void> { data.expressions.registerFunction(() => createTileMapFn(visualizationDependencies)); - visualizations.types.VisTypesRegistryProvider.register(() => + visualizations.types.registerVisualization(() => createTileMapTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index 05dbe0fa9f9df..d96d8ad9be786 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -27,8 +27,9 @@ import { colorSchemas } from 'ui/vislib/components/color/truncated_colormaps'; import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson'; import { createTileMapVisualization } from './tile_map_visualization'; -import { visFactory } from '../../visualizations/public'; +import { visFactory } from '../../visualizations/public/np_ready/public'; import { TileMapOptions } from './components/tile_map_options'; +import { MapTypes } from './map_types'; export function createTileMapTypeDefinition(dependencies) { const CoordinateMapsVisualization = createTileMapVisualization(dependencies); @@ -64,48 +65,53 @@ export function createTileMapTypeDefinition(dependencies) { editorConfig: { collections: { colorSchemas, - legendPositions: [{ - value: 'bottomleft', - text: i18n.translate('tileMap.vis.editorConfig.legendPositions.bottomLeftText', { - defaultMessage: 'Bottom left', - }), - }, { - value: 'bottomright', - text: i18n.translate('tileMap.vis.editorConfig.legendPositions.bottomRightText', { - defaultMessage: 'Bottom right', - }), - }, { - value: 'topleft', - text: i18n.translate('tileMap.vis.editorConfig.legendPositions.topLeftText', { - defaultMessage: 'Top left', - }), - }, { - value: 'topright', - text: i18n.translate('tileMap.vis.editorConfig.legendPositions.topRightText', { - defaultMessage: 'Top right', - }), - }], + legendPositions: [ + { + value: 'bottomleft', + text: i18n.translate('tileMap.vis.editorConfig.legendPositions.bottomLeftText', { + defaultMessage: 'Bottom left', + }), + }, + { + value: 'bottomright', + text: i18n.translate('tileMap.vis.editorConfig.legendPositions.bottomRightText', { + defaultMessage: 'Bottom right', + }), + }, + { + value: 'topleft', + text: i18n.translate('tileMap.vis.editorConfig.legendPositions.topLeftText', { + defaultMessage: 'Top left', + }), + }, + { + value: 'topright', + text: i18n.translate('tileMap.vis.editorConfig.legendPositions.topRightText', { + defaultMessage: 'Top right', + }), + }, + ], mapTypes: [ { - value: 'Scaled Circle Markers', + value: MapTypes.ScaledCircleMarkers, text: i18n.translate('tileMap.vis.editorConfig.mapTypes.scaledCircleMarkersText', { defaultMessage: 'Scaled circle markers', }), }, { - value: 'Shaded Circle Markers', + value: MapTypes.ShadedCircleMarkers, text: i18n.translate('tileMap.vis.editorConfig.mapTypes.shadedCircleMarkersText', { defaultMessage: 'Shaded circle markers', }), }, { - value: 'Shaded Geohash Grid', + value: MapTypes.ShadedGeohashGrid, text: i18n.translate('tileMap.vis.editorConfig.mapTypes.shadedGeohashGridText', { defaultMessage: 'Shaded geohash grid', }), }, { - value: 'Heatmap', + value: MapTypes.Heatmap, text: i18n.translate('tileMap.vis.editorConfig.mapTypes.heatmapText', { defaultMessage: 'Heatmap', }), @@ -113,7 +119,7 @@ export function createTileMapTypeDefinition(dependencies) { ], tmsLayers: [], }, - optionsTemplate: (props) => , + optionsTemplate: props => , schemas: new Schemas([ { group: 'metrics', @@ -124,9 +130,7 @@ export function createTileMapTypeDefinition(dependencies) { min: 1, max: 1, aggFilter: ['count', 'avg', 'sum', 'min', 'max', 'cardinality', 'top_hits'], - defaults: [ - { schema: 'metric', type: 'count' }, - ], + defaults: [{ schema: 'metric', type: 'count' }], }, { group: 'buckets', diff --git a/src/legacy/core_plugins/tile_map/public/types.ts b/src/legacy/core_plugins/tile_map/public/types.ts index dbaa80217879a..5f1c3f9b03c9e 100644 --- a/src/legacy/core_plugins/tile_map/public/types.ts +++ b/src/legacy/core_plugins/tile_map/public/types.ts @@ -18,6 +18,7 @@ */ import { TmsLayer } from 'ui/vis/map/service_settings'; +import { MapTypes } from './map_types'; export interface WMSOptions { selectedTmsLayer?: TmsLayer; @@ -35,7 +36,7 @@ export interface WMSOptions { export interface TileMapVisParams { colorSchema: string; - mapType: 'Scaled Circle Markers' | 'Shaded Circle Markers' | 'Shaded geohash grid' | 'Heatmap'; + mapType: MapTypes; isDesaturated: boolean; addTooltip: boolean; heatClusterSize: number; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts b/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts index 769fc72b4c661..539b4e1bdfb45 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/legacy.ts @@ -20,12 +20,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { visualizations } from '../../visualizations/public'; +import { setup as setupVisualizations } from '../../visualizations/public/np_ready/public/legacy'; import { MarkdownPluginSetupDependencies } from './plugin'; import { plugin } from '.'; const plugins: Readonly = { - visualizations, + visualizations: setupVisualizations, data: npSetup.plugins.data, }; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts index 7b2f8f6c236b2..2f1a46f1d8f3f 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/markdown_vis.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { visFactory, DefaultEditorSize } from '../../visualizations/public'; +import { visFactory, DefaultEditorSize } from '../../visualizations/public/np_ready/public'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; diff --git a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts index 286b233280382..5f9c82ca89290 100644 --- a/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_markdown/public/plugin.ts @@ -19,7 +19,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { markdownVis } from './markdown_vis'; import { createMarkdownVisFn } from './markdown_fn'; @@ -39,7 +39,7 @@ export class MarkdownPlugin implements Plugin { } public setup(core: CoreSetup, { data, visualizations }: MarkdownPluginSetupDependencies) { - visualizations.types.VisTypesRegistryProvider.register(() => markdownVis); + visualizations.types.registerVisualization(() => markdownVis); data.expressions.registerFunction(createMarkdownVisFn); } 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 7adf7007e8603..ce77c64d77651 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 @@ -24,7 +24,7 @@ import expect from '@kbn/expect'; import { VisProvider } from 'ui/vis'; import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern'; -import { visualizations } from '../../../visualizations/public'; +import { setup as setupVisualizations } from '../../../visualizations/public/np_ready/public/legacy'; import { createMetricVisTypeDefinition } from '../metric_vis_type'; describe('metric_vis - createMetricVisTypeDefinition', () => { @@ -32,48 +32,52 @@ describe('metric_vis - createMetricVisTypeDefinition', () => { let vis; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject((Private) => { - setup = () => { - const Vis = Private(VisProvider); - const metricVisType = createMetricVisTypeDefinition(); - - visualizations.types.VisTypesRegistryProvider.register(() => - metricVisType - ); - - const indexPattern = Private(LogstashIndexPatternStubProvider); - - indexPattern.stubSetFieldFormat('ip', 'url', { - urlTemplate: 'http://ip.info?address={{value}}', - labelTemplate: 'ip[{{value}}]' - }); - - vis = new Vis(indexPattern, { - type: 'metric', - aggs: [{ id: '1', type: 'top_hits', schema: 'metric', params: { field: 'ip' } }], - }); - - vis.params.dimensions = { - metrics: [{ - accessor: 0, format: { - id: 'url', params: { - urlTemplate: 'http://ip.info?address={{value}}', - labelTemplate: 'ip[{{value}}]' - } - } - }] - }; - - const el = document.createElement('div'); - const Controller = metricVisType.visualization; - const controller = new Controller(el, vis); - const render = (esResponse) => { - controller.render(esResponse, vis.params); + beforeEach( + ngMock.inject(Private => { + setup = () => { + const Vis = Private(VisProvider); + const metricVisType = createMetricVisTypeDefinition(); + + setupVisualizations.types.registerVisualization(() => metricVisType); + + const indexPattern = Private(LogstashIndexPatternStubProvider); + + indexPattern.stubSetFieldFormat('ip', 'url', { + urlTemplate: 'http://ip.info?address={{value}}', + labelTemplate: 'ip[{{value}}]', + }); + + vis = new Vis(indexPattern, { + type: 'metric', + aggs: [{ id: '1', type: 'top_hits', schema: 'metric', params: { field: 'ip' } }], + }); + + vis.params.dimensions = { + metrics: [ + { + accessor: 0, + format: { + id: 'url', + params: { + urlTemplate: 'http://ip.info?address={{value}}', + labelTemplate: 'ip[{{value}}]', + }, + }, + }, + ], + }; + + const el = document.createElement('div'); + const Controller = metricVisType.visualization; + const controller = new Controller(el, vis); + const render = esResponse => { + controller.render(esResponse, vis.params); + }; + + return { el, render }; }; - - return { el, render }; - }; - })); + }) + ); it('renders html value from field formatter', () => { const { el, render } = setup(); @@ -81,12 +85,14 @@ describe('metric_vis - createMetricVisTypeDefinition', () => { const ip = '235.195.237.208'; render({ columns: [{ id: 'col-0', name: 'ip' }], - rows: [{ 'col-0': ip }] + rows: [{ 'col-0': ip }], }); const $link = $(el) .find('a[href]') - .filter(function () { return this.href.includes('ip.info'); }); + .filter(function () { + return this.href.includes('ip.info'); + }); expect($link).to.have.length(1); expect($link.text()).to.be(`ip[${ip}]`); diff --git a/src/legacy/core_plugins/vis_type_metric/public/legacy.ts b/src/legacy/core_plugins/vis_type_metric/public/legacy.ts index 4ab399977d7b1..2eea4c70309de 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/legacy.ts @@ -20,13 +20,13 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { visualizations } from '../../visualizations/public'; +import { setup as setupVisualizations } from '../../visualizations/public/np_ready/public/legacy'; import { MetricVisPluginSetupDependencies } from './plugin'; import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const plugins: Readonly = { - visualizations, + visualizations: setupVisualizations, data: npSetup.plugins.data, // Temporary solution diff --git a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts index 76e88888ef7f2..eba61edeeb84a 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/metric_vis_type.ts @@ -26,7 +26,7 @@ import { vislibColorMaps } from 'ui/vislib/components/color/colormaps'; // @ts-ignore import { MetricVisComponent } from './components/metric_vis_controller'; -import { visFactory } from '../../visualizations/public'; +import { visFactory } from '../../visualizations/public/np_ready/public'; export const createMetricVisTypeDefinition = () => { return visFactory.createReactVisualization({ diff --git a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts index d99df03fcc560..66a3bb007e379 100644 --- a/src/legacy/core_plugins/vis_type_metric/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_metric/public/plugin.ts @@ -20,7 +20,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; import { LegacyDependenciesPlugin } from './shim'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { createMetricVisFn } from './metric_vis_fn'; // @ts-ignore @@ -48,7 +48,7 @@ export class MetricVisPlugin implements Plugin { __LEGACY.setup(); data.expressions.registerFunction(createMetricVisFn); - visualizations.types.VisTypesRegistryProvider.register(createMetricVisTypeDefinition); + visualizations.types.registerVisualization(createMetricVisTypeDefinition); } public start(core: CoreStart) { 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 4253efd4689cf..9110c8dcb8486 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 @@ -28,7 +28,7 @@ import { AppStateProvider } from 'ui/state_management/app_state'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { createTableVisTypeDefinition } from '../table_vis_type'; -import { visualizations } from '../../../visualizations/public'; +import { setup as setupVisualizations } from '../../../visualizations/public/np_ready/public/legacy'; describe('Table Vis - Controller', async function () { let $rootScope; @@ -44,68 +44,73 @@ describe('Table Vis - Controller', async function () { let legacyDependencies; beforeEach(ngMock.module('kibana', 'kibana/table_vis')); - beforeEach(ngMock.inject(function ($injector) { - Private = $injector.get('Private'); - legacyDependencies = { - // eslint-disable-next-line new-cap - createAngularVisualization: VisFactoryProvider(Private).createAngularVisualization - }; - - visualizations.types.VisTypesRegistryProvider.register(() => - createTableVisTypeDefinition(legacyDependencies) - ); - - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - fixtures = require('fixtures/fake_hierarchical_data'); - AppState = Private(AppStateProvider); - Vis = Private(VisProvider); - tableAggResponse = legacyResponseHandlerProvider().handler; - })); + beforeEach( + ngMock.inject(function ($injector) { + Private = $injector.get('Private'); + legacyDependencies = { + // eslint-disable-next-line new-cap + createAngularVisualization: VisFactoryProvider(Private).createAngularVisualization, + }; + + setupVisualizations.types.registerVisualization(() => + createTableVisTypeDefinition(legacyDependencies) + ); + + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); + fixtures = require('fixtures/fake_hierarchical_data'); + AppState = Private(AppStateProvider); + Vis = Private(VisProvider); + tableAggResponse = legacyResponseHandlerProvider().handler; + }) + ); function OneRangeVis(params) { - return new Vis( - Private(FixturesStubbedLogstashIndexPatternProvider), - { - type: 'table', - params: params || {}, - aggs: [ - { type: 'count', schema: 'metric' }, - { - type: 'range', - schema: 'bucket', - params: { - field: 'bytes', - ranges: [ - { from: 0, to: 1000 }, - { from: 1000, to: 2000 } - ] - } - } - ] - } - ); + return new Vis(Private(FixturesStubbedLogstashIndexPatternProvider), { + type: 'table', + params: params || {}, + aggs: [ + { type: 'count', schema: 'metric' }, + { + type: 'range', + schema: 'bucket', + params: { + field: 'bytes', + ranges: [{ from: 0, to: 1000 }, { from: 1000, to: 2000 }], + }, + }, + ], + }); } const dimensions = { - buckets: [{ - accessor: 0, - }], metrics: [{ - accessor: 1, - format: { id: 'range' }, - }] + buckets: [ + { + accessor: 0, + }, + ], + metrics: [ + { + accessor: 1, + format: { id: 'range' }, + }, + ], }; // basically a parameterized beforeEach function initController(vis) { - vis.aggs.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); }); + vis.aggs.aggs.forEach(function (agg, i) { + agg.id = 'agg_' + (i + 1); + }); tabifiedResponse = tabifyAggResponse(vis.aggs, fixtures.oneRangeBucket); $rootScope.vis = vis; $rootScope.visParams = vis.params; $rootScope.uiState = new AppState({ uiState: {} }).makeStateful('uiState'); $rootScope.renderComplete = () => {}; - $rootScope.newScope = function (scope) { $scope = scope; }; + $rootScope.newScope = function (scope) { + $scope = scope; + }; $el = $('
') .attr('ng-controller', 'KbnTableVisController') @@ -157,7 +162,7 @@ describe('Table Vis - Controller', async function () { it('sets the sort on the scope when it is passed as a vis param', async function () { const sortObj = { columnIndex: 1, - direction: 'asc' + direction: 'asc', }; const vis = new OneRangeVis({ sort: sortObj }); initController(vis); @@ -181,7 +186,6 @@ describe('Table Vis - Controller', async function () { }); it('passes partialRows:true to tabify based on the vis params', function () { - const vis = new OneRangeVis({ showPartialRows: true }); initController(vis); @@ -189,7 +193,6 @@ describe('Table Vis - Controller', async function () { }); it('passes partialRows:false to tabify based on the vis params', function () { - const vis = new OneRangeVis({ showPartialRows: false }); initController(vis); 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 b0476ae224866..8521ee729f313 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 @@ -31,10 +31,9 @@ import { round } from 'lodash'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { createTableVisTypeDefinition } from '../../table_vis_type'; -import { visualizations } from '../../../../visualizations/public'; +import { setup } from '../../../../visualizations/public/np_ready/public/legacy'; describe('Table Vis - AggTable Directive', function () { - let $rootScope; let $compile; let Vis; @@ -51,19 +50,21 @@ describe('Table Vis - AggTable Directive', function () { const vis2 = new Vis(indexPattern, { type: 'table', params: { - showMetricsAtAllLevels: true + showMetricsAtAllLevels: true, }, aggs: [ { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, { type: 'terms', schema: 'bucket', params: { field: 'extension' } }, { type: 'terms', schema: 'bucket', params: { field: 'geo.src' } }, - { type: 'terms', schema: 'bucket', params: { field: 'machine.os' } } - ] + { type: 'terms', schema: 'bucket', params: { field: 'machine.os' } }, + ], }); vis2.aggs.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); }); - tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets, { metricsAtAllLevels: true }); + tabifiedData.threeTermBuckets = tabifyAggResponse(vis2.aggs, fixtures.threeTermBuckets, { + metricsAtAllLevels: true, + }); const vis3 = new Vis(indexPattern, { type: 'table', @@ -71,41 +72,54 @@ describe('Table Vis - AggTable Directive', function () { { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, { type: 'min', schema: 'metric', params: { field: '@timestamp' } }, { type: 'terms', schema: 'bucket', params: { field: 'extension' } }, - { type: 'date_histogram', schema: 'bucket', params: { field: '@timestamp', interval: 'd' } }, - { type: 'derivative', schema: 'metric', - params: { metricAgg: 'custom', customMetric: { id: '5-orderAgg', type: 'count' } } }, - { type: 'top_hits', schema: 'metric', params: { field: 'bytes', aggregate: { val: 'min' }, size: 1 } } - ] + { + type: 'date_histogram', + schema: 'bucket', + params: { field: '@timestamp', interval: 'd' }, + }, + { + type: 'derivative', + schema: 'metric', + params: { metricAgg: 'custom', customMetric: { id: '5-orderAgg', type: 'count' } }, + }, + { + type: 'top_hits', + schema: 'metric', + params: { field: 'bytes', aggregate: { val: 'min' }, size: 1 }, + }, + ], }); vis3.aggs.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); }); - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = - tabifyAggResponse(vis3.aggs, fixtures.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative); + tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = tabifyAggResponse( + vis3.aggs, + fixtures.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative + ); }; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($injector, Private, config) { - legacyDependencies = { - // eslint-disable-next-line new-cap - createAngularVisualization: VisFactoryProvider(Private).createAngularVisualization - }; + beforeEach( + ngMock.inject(function ($injector, Private, config) { + legacyDependencies = { + // eslint-disable-next-line new-cap + createAngularVisualization: VisFactoryProvider(Private).createAngularVisualization, + }; - visualizations.types.VisTypesRegistryProvider.register(() => - createTableVisTypeDefinition(legacyDependencies) - ); + setup.types.registerVisualization(() => createTableVisTypeDefinition(legacyDependencies)); - tableAggResponse = legacyResponseHandlerProvider().handler; - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - Vis = Private(VisProvider); - settings = config; + tableAggResponse = legacyResponseHandlerProvider().handler; + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + Vis = Private(VisProvider); + settings = config; - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); - init(); - })); + init(); + }) + ); let $scope; beforeEach(function () { @@ -115,12 +129,16 @@ describe('Table Vis - AggTable Directive', function () { $scope.$destroy(); }); - it('renders a simple response properly', async function () { - $scope.dimensions = { metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], buckets: [] }; + $scope.dimensions = { + metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], + buckets: [], + }; $scope.table = (await tableAggResponse(tabifiedData.metricOnly, $scope.dimensions)).tables[0]; - const $el = $compile('')($scope); + const $el = $compile('')( + $scope + ); $scope.$digest(); expect($el.find('tbody').length).to.be(1); @@ -131,7 +149,9 @@ describe('Table Vis - AggTable Directive', function () { it('renders nothing if the table is empty', function () { $scope.dimensions = {}; $scope.table = null; - const $el = $compile('')($scope); + const $el = $compile('')( + $scope + ); $scope.$digest(); expect($el.find('tbody').length).to.be(0); @@ -139,10 +159,21 @@ describe('Table Vis - AggTable Directive', function () { it('renders a complex response properly', async function () { $scope.dimensions = { - buckets: [{ accessor: 0, params: {} }, { accessor: 2, params: {} }, { accessor: 4, params: {} }], - metrics: [{ accessor: 1, params: {} }, { accessor: 3, params: {} }, { accessor: 5, params: {} }] + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], }; - $scope.table = (await tableAggResponse(tabifiedData.threeTermBuckets, $scope.dimensions)).tables[0]; + $scope.table = (await tableAggResponse( + tabifiedData.threeTermBuckets, + $scope.dimensions + )).tables[0]; const $el = $(''); $compile($el)($scope); $scope.$digest(); @@ -165,7 +196,9 @@ describe('Table Vis - AggTable Directive', function () { expect($cells.length).to.be(6); const txts = $cells.map(function () { - return $(this).text().trim(); + return $(this) + .text() + .trim(); }); // two character country code @@ -184,7 +217,6 @@ describe('Table Vis - AggTable Directive', function () { describe('renders totals row', function () { async function totalsRowTest(totalFunc, expected) { - function setDefaultTimezone() { moment.tz.setDefault(settings.get('dateFormat:tz')); } @@ -197,15 +229,18 @@ describe('Table Vis - AggTable Directive', function () { buckets: [ { accessor: 0, params: {} }, { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], metrics: [ + ], + metrics: [ { accessor: 2, format: { id: 'number' } }, { accessor: 3, format: { id: 'date' } }, { accessor: 4, format: { id: 'number' } }, { accessor: 5, format: { id: 'number' } }, - ] + ], }; const response = await tableAggResponse( - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative, $scope.dimensions); + tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative, + $scope.dimensions + ); $scope.table = response.tables[0]; $scope.showTotal = true; $scope.totalFunc = totalFunc; @@ -226,7 +261,11 @@ describe('Table Vis - AggTable Directive', function () { expect($cells.length).to.be(6); for (let i = 0; i < 6; i++) { - expect($($cells[i]).text().trim()).to.be(expected[i]); + expect( + $($cells[i]) + .text() + .trim() + ).to.be(expected[i]); } settings.set('dateFormat:tz', oldTimezoneSetting); off(); @@ -241,7 +280,7 @@ describe('Table Vis - AggTable Directive', function () { '9,283', 'Sep 28, 2014 @ 00:00:00.000', '1', - '11' + '11', ]); }); it('as max', async function () { @@ -251,34 +290,22 @@ describe('Table Vis - AggTable Directive', function () { '220,943', 'Oct 3, 2014 @ 00:00:00.000', '239', - '837' + '837', ]); }); it('as avg', async function () { - await totalsRowTest('avg', [ - '', - '', - '87,221.5', - '', - '64.667', - '206.833' - ]); + await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); }); it('as sum', async function () { - await totalsRowTest('sum', [ - '', - '', - '1,569,987', - '', - '1,164', - '3,723' - ]); + await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); }); }); describe('aggTable.toCsv()', function () { it('escapes rows and columns properly', function () { - const $el = $compile('')($scope); + const $el = $compile('')( + $scope + ); $scope.$digest(); const $tableScope = $el.isolateScope(); @@ -289,25 +316,35 @@ describe('Table Vis - AggTable Directive', function () { { id: 'b', name: 'two' }, { id: 'c', name: 'with double-quotes(")' }, ], - rows: [ - { a: 1, b: 2, c: '"foobar"' }, - ], + rows: [{ a: 1, b: 2, c: '"foobar"' }], }; expect(aggTable.toCsv()).to.be( - 'one,two,"with double-quotes("")"' + '\r\n' + - '1,2,"""foobar"""' + '\r\n' + 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' ); }); it('exports rows and columns properly', async function () { $scope.dimensions = { - buckets: [{ accessor: 0, params: {} }, { accessor: 2, params: {} }, { accessor: 4, params: {} }], - metrics: [{ accessor: 1, params: {} }, { accessor: 3, params: {} }, { accessor: 5, params: {} }] + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], }; - $scope.table = (await tableAggResponse(tabifiedData.threeTermBuckets, $scope.dimensions)).tables[0]; + $scope.table = (await tableAggResponse( + tabifiedData.threeTermBuckets, + $scope.dimensions + )).tables[0]; - const $el = $compile('')($scope); + const $el = $compile('')( + $scope + ); $scope.$digest(); const $tableScope = $el.isolateScope(); @@ -316,30 +353,56 @@ describe('Table Vis - AggTable Directive', function () { const raw = aggTable.toCsv(false); expect(raw).to.be( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + '\r\n' + - 'png,IT,win,412032,9299,0' + '\r\n' + - 'png,IT,mac,412032,9299,9299' + '\r\n' + - 'png,US,linux,412032,8293,3992' + '\r\n' + - 'png,US,mac,412032,8293,3029' + '\r\n' + - 'css,MX,win,412032,9299,4992' + '\r\n' + - 'css,MX,mac,412032,9299,5892' + '\r\n' + - 'css,US,linux,412032,8293,3992' + '\r\n' + - 'css,US,mac,412032,8293,3029' + '\r\n' + - 'html,CN,win,412032,9299,4992' + '\r\n' + - 'html,CN,mac,412032,9299,5892' + '\r\n' + - 'html,FR,win,412032,8293,3992' + '\r\n' + - 'html,FR,mac,412032,8293,3029' + '\r\n' + '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + + '\r\n' + + 'png,IT,win,412032,9299,0' + + '\r\n' + + 'png,IT,mac,412032,9299,9299' + + '\r\n' + + 'png,US,linux,412032,8293,3992' + + '\r\n' + + 'png,US,mac,412032,8293,3029' + + '\r\n' + + 'css,MX,win,412032,9299,4992' + + '\r\n' + + 'css,MX,mac,412032,9299,5892' + + '\r\n' + + 'css,US,linux,412032,8293,3992' + + '\r\n' + + 'css,US,mac,412032,8293,3029' + + '\r\n' + + 'html,CN,win,412032,9299,4992' + + '\r\n' + + 'html,CN,mac,412032,9299,5892' + + '\r\n' + + 'html,FR,win,412032,8293,3992' + + '\r\n' + + 'html,FR,mac,412032,8293,3029' + + '\r\n' ); }); it('exports formatted rows and columns properly', async function () { $scope.dimensions = { - buckets: [{ accessor: 0, params: {} }, { accessor: 2, params: {} }, { accessor: 4, params: {} }], - metrics: [{ accessor: 1, params: {} }, { accessor: 3, params: {} }, { accessor: 5, params: {} }] + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], }; - $scope.table = (await tableAggResponse(tabifiedData.threeTermBuckets, $scope.dimensions)).tables[0]; + $scope.table = (await tableAggResponse( + tabifiedData.threeTermBuckets, + $scope.dimensions + )).tables[0]; - const $el = $compile('')($scope); + const $el = $compile('')( + $scope + ); $scope.$digest(); const $tableScope = $el.isolateScope(); @@ -351,19 +414,32 @@ describe('Table Vis - AggTable Directive', function () { const formatted = aggTable.toCsv(true); expect(formatted).to.be( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + '\r\n' + - '"png_formatted",IT,win,412032,9299,0' + '\r\n' + - '"png_formatted",IT,mac,412032,9299,9299' + '\r\n' + - '"png_formatted",US,linux,412032,8293,3992' + '\r\n' + - '"png_formatted",US,mac,412032,8293,3029' + '\r\n' + - '"css_formatted",MX,win,412032,9299,4992' + '\r\n' + - '"css_formatted",MX,mac,412032,9299,5892' + '\r\n' + - '"css_formatted",US,linux,412032,8293,3992' + '\r\n' + - '"css_formatted",US,mac,412032,8293,3029' + '\r\n' + - '"html_formatted",CN,win,412032,9299,4992' + '\r\n' + - '"html_formatted",CN,mac,412032,9299,5892' + '\r\n' + - '"html_formatted",FR,win,412032,8293,3992' + '\r\n' + - '"html_formatted",FR,mac,412032,8293,3029' + '\r\n' + '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + + '\r\n' + + '"png_formatted",IT,win,412032,9299,0' + + '\r\n' + + '"png_formatted",IT,mac,412032,9299,9299' + + '\r\n' + + '"png_formatted",US,linux,412032,8293,3992' + + '\r\n' + + '"png_formatted",US,mac,412032,8293,3029' + + '\r\n' + + '"css_formatted",MX,win,412032,9299,4992' + + '\r\n' + + '"css_formatted",MX,mac,412032,9299,5892' + + '\r\n' + + '"css_formatted",US,linux,412032,8293,3992' + + '\r\n' + + '"css_formatted",US,mac,412032,8293,3029' + + '\r\n' + + '"html_formatted",CN,win,412032,9299,4992' + + '\r\n' + + '"html_formatted",CN,mac,412032,9299,5892' + + '\r\n' + + '"html_formatted",FR,win,412032,8293,3992' + + '\r\n' + + '"html_formatted",FR,mac,412032,8293,3029' + + '\r\n' ); }); }); @@ -452,9 +528,7 @@ describe('Table Vis - AggTable Directive', function () { { id: 'b', name: 'two' }, { id: 'c', name: 'with double-quotes(")' }, ], - rows: [ - { a: 1, b: 2, c: '"foobar"' }, - ], + rows: [{ a: 1, b: 2, c: '"foobar"' }], }; aggTable.csv.filename = 'somefilename.csv'; @@ -464,25 +538,26 @@ describe('Table Vis - AggTable Directive', function () { const call = saveAs.getCall(0); expect(call.args[0]).to.be.a(FakeBlob); expect(call.args[0].slices).to.eql([ - 'one,two,"with double-quotes("")"' + '\r\n' + - '1,2,"""foobar"""' + '\r\n' + 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', ]); expect(call.args[0].opts).to.eql({ - type: 'text/plain;charset=utf-8' + type: 'text/plain;charset=utf-8', }); expect(call.args[1]).to.be('somefilename.csv'); }); it('should use the export-title attribute', function () { const expected = 'export file name'; - const $el = $compile(``)($scope); + const $el = $compile( + `` + )($scope); $scope.$digest(); const $tableScope = $el.isolateScope(); const aggTable = $tableScope.aggTable; $tableScope.table = { columns: [], - rows: [] + rows: [], }; $tableScope.exportTitle = expected; $scope.$digest(); 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 5b9974d6ff4d5..7998a92a4759f 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 @@ -28,7 +28,7 @@ import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { VisFactoryProvider } from 'ui/vis/vis_factory'; import { createTableVisTypeDefinition } from '../../table_vis_type'; -import { visualizations } from '../../../../visualizations/public'; +import { setup } from '../../../../visualizations/public/np_ready/public/legacy'; describe('Table Vis - AggTableGroup Directive', function () { let $rootScope; @@ -49,8 +49,8 @@ describe('Table Vis - AggTableGroup Directive', function () { { type: 'avg', schema: 'metric', params: { field: 'bytes' } }, { type: 'terms', schema: 'split', params: { field: 'extension' } }, { type: 'terms', schema: 'segment', params: { field: 'geo.src' } }, - { type: 'terms', schema: 'segment', params: { field: 'machine.os' } } - ] + { type: 'terms', schema: 'segment', params: { field: 'machine.os' } }, + ], }); vis2.aggs.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); @@ -59,25 +59,25 @@ describe('Table Vis - AggTableGroup Directive', function () { }; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject(function ($injector, Private) { - legacyDependencies = { - // eslint-disable-next-line new-cap - createAngularVisualization: VisFactoryProvider(Private).createAngularVisualization - }; + beforeEach( + ngMock.inject(function ($injector, Private) { + legacyDependencies = { + // eslint-disable-next-line new-cap + createAngularVisualization: VisFactoryProvider(Private).createAngularVisualization, + }; - visualizations.types.VisTypesRegistryProvider.register(() => - createTableVisTypeDefinition(legacyDependencies) - ); + setup.types.registerVisualization(() => createTableVisTypeDefinition(legacyDependencies)); - tableAggResponse = legacyResponseHandlerProvider().handler; - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - Vis = Private(VisProvider); + tableAggResponse = legacyResponseHandlerProvider().handler; + indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); + Vis = Private(VisProvider); - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); - init(); - })); + init(); + }) + ); let $scope; beforeEach(function () { @@ -88,13 +88,18 @@ describe('Table Vis - AggTableGroup Directive', function () { }); it('renders a simple split response properly', async function () { - $scope.dimensions = { metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], buckets: [] }; + $scope.dimensions = { + metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], + buckets: [], + }; $scope.group = await tableAggResponse(tabifiedData.metricOnly, $scope.dimensions); $scope.sort = { columnIndex: null, - direction: null + direction: null, }; - const $el = $(''); + const $el = $( + '' + ); $compile($el)($scope); $scope.$digest(); @@ -104,10 +109,12 @@ describe('Table Vis - AggTableGroup Directive', function () { }); it('renders nothing if the table list is empty', function () { - const $el = $(''); + const $el = $( + '' + ); $scope.group = { - tables: [] + tables: [], }; $compile($el)($scope); @@ -121,10 +128,19 @@ describe('Table Vis - AggTableGroup Directive', function () { $scope.dimensions = { splitRow: [{ accessor: 0, params: {} }], buckets: [{ accessor: 2, params: {} }, { accessor: 4, params: {} }], - metrics: [{ accessor: 1, params: {} }, { accessor: 3, params: {} }, { accessor: 5, params: {} }] + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], }; - const group = $scope.group = await tableAggResponse(tabifiedData.threeTermBuckets, $scope.dimensions); - const $el = $(''); + const group = ($scope.group = await tableAggResponse( + tabifiedData.threeTermBuckets, + $scope.dimensions + )); + const $el = $( + '' + ); $compile($el)($scope); $scope.$digest(); diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy.ts b/src/legacy/core_plugins/vis_type_table/public/legacy.ts index fded5690a362d..8139a70552c48 100644 --- a/src/legacy/core_plugins/vis_type_table/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_table/public/legacy.ts @@ -22,11 +22,11 @@ import { npSetup, npStart } from 'ui/new_platform'; import { plugin } from '.'; import { TablePluginSetupDependencies } from './plugin'; -import { visualizations } from '../../visualizations/public'; +import { setup as setupVisualizations } from '../../visualizations/public/np_ready/public/legacy'; import { LegacyDependenciesPlugin } from './shim'; const plugins: Readonly = { - visualizations, + visualizations: setupVisualizations, data: npSetup.plugins.data, // Temporary solution diff --git a/src/legacy/core_plugins/vis_type_table/public/plugin.ts b/src/legacy/core_plugins/vis_type_table/public/plugin.ts index 21b6e21d6d639..6a39e8079a9fd 100644 --- a/src/legacy/core_plugins/vis_type_table/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_table/public/plugin.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; import { @@ -63,7 +63,7 @@ export class TableVisPlugin implements Plugin, void> { data.expressions.registerFunction(createTableVisFn); - visualizations.types.VisTypesRegistryProvider.register(() => + visualizations.types.registerVisualization(() => createTableVisTypeDefinition(visualizationDependencies) ); } diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts index 01b2d99ce509c..6a5e06b6e6978 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts @@ -20,12 +20,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { visualizations } from '../../visualizations/public'; +import { setup as setupVisualizations } from '../../visualizations/public/np_ready/public/legacy'; import { TagCloudPluginSetupDependencies } from './plugin'; import { plugin } from '.'; const plugins: Readonly = { - visualizations, + visualizations: setupVisualizations, data: npSetup.plugins.data, }; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts index e7f633b4af377..e13e9896e3940 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts @@ -19,7 +19,7 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { createTagCloudFn } from './tag_cloud_fn'; import { createTagCloudTypeDefinition } from './tag_cloud_type'; @@ -40,7 +40,7 @@ export class TagCloudPlugin implements Plugin { public setup(core: CoreSetup, { data, visualizations }: TagCloudPluginSetupDependencies) { data.expressions.registerFunction(createTagCloudFn); - visualizations.types.VisTypesRegistryProvider.register(createTagCloudTypeDefinition); + visualizations.types.registerVisualization(createTagCloudTypeDefinition); } public start(core: CoreStart) { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 0b4d90522cc44..421821d93b045 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -23,7 +23,7 @@ import { Status } from 'ui/vis/update_status'; import { Schemas } from 'ui/vis/editors/default/schemas'; import { TagCloudOptions } from './components/tag_cloud_options'; -import { visFactory } from '../../visualizations/public'; +import { visFactory } from '../../visualizations/public/np_ready/public'; // @ts-ignore import { TagCloudVisualization } from './components/tag_cloud_visualization'; 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 681f4486a02d7..012f144983e98 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 @@ -41,10 +41,10 @@ import vegaMapImage256 from './vega_map_image_256.png'; import { VegaParser } from '../data_model/vega_parser'; import { SearchCache } from '../data_model/search_cache'; -import { visualizations } from '../../../visualizations/public'; +import { setup } from '../../../visualizations/public/np_ready/public/legacy'; import { createVegaTypeDefinition } from '../vega_type'; -const THRESHOLD = 0.10; +const THRESHOLD = 0.1; const PIXEL_DIFF = 30; describe('VegaVisualizations', () => { @@ -57,22 +57,24 @@ describe('VegaVisualizations', () => { let vegaVisualizationDependencies; beforeEach(ngMock.module('kibana')); - beforeEach(ngMock.inject((Private, $injector) => { - vegaVisualizationDependencies = { - es: $injector.get('es'), - serviceSettings: $injector.get('serviceSettings'), - uiSettings: $injector.get('config'), - }; + beforeEach( + ngMock.inject((Private, $injector) => { + vegaVisualizationDependencies = { + es: $injector.get('es'), + serviceSettings: $injector.get('serviceSettings'), + uiSettings: $injector.get('config'), + }; - visualizations.types.VisTypesRegistryProvider.register(() => - createVegaTypeDefinition(vegaVisualizationDependencies) - ); + setup.types.registerVisualization(() => + createVegaTypeDefinition(vegaVisualizationDependencies) + ); - Vis = Private(visModule.VisProvider); + Vis = Private(visModule.VisProvider); - VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); - indexPattern = Private(LogstashIndexPatternStubProvider); - })); + VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); + indexPattern = Private(LogstashIndexPatternStubProvider); + }) + ); describe('VegaVisualization - basics', () => { beforeEach(async function () { @@ -105,15 +107,12 @@ describe('VegaVisualizations', () => { await vegaVis.render(vegaParser, vis.params, { resize: true }); const mismatchedPixels2 = await compareImage(vegaliteImage256); expect(mismatchedPixels2).to.be.lessThan(PIXEL_DIFF); - } finally { vegaVis.destroy(); } - }); it('should show vega graph', async function () { - let vegaVis; try { vegaVis = new VegaVisualization(domNode, vis); @@ -127,20 +126,16 @@ describe('VegaVisualizations', () => { } finally { vegaVis.destroy(); } - }); it('should show vegatooltip on mouseover over a vega graph', async () => { - let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); const vegaParser = new VegaParser(vegaTooltipGraph, new SearchCache()); await vegaParser.parseAsync(); await vegaVis.render(vegaParser, vis.params, { data: true }); - const $el = $(domNode); const offset = $el.offset(); @@ -160,27 +155,24 @@ describe('VegaVisualizations', () => { expect(tooltip).to.be.ok(); expect(tooltip.innerHTML).to.be( '

This is a long title

' + - '' + - '' + - '' + - '
fieldA:value of fld1
fld2:42
'); + '' + + '' + + '' + + '
fieldA:value of fld1
fld2:42
' + ); vegaVis.destroy(); tooltip = document.getElementById('vega-kibana-tooltip'); expect(tooltip).to.not.be.ok(); - } finally { vegaVis.destroy(); } - }); it('should show vega blank rectangle on top of a map (vegamap)', async () => { - let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); const vegaParser = new VegaParser(vegaMapGraph, new SearchCache()); await vegaParser.parseAsync(); @@ -191,19 +183,17 @@ describe('VegaVisualizations', () => { await vegaVis.render(vegaParser, vis.params, { data: true }); const mismatchedPixels = await compareImage(vegaMapImage256); expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - } finally { vegaVis.destroy(); } - }); it('should add a small subpixel value to the height of the canvas to avoid getting it set to 0', async () => { let vegaVis; try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser(`{ + const vegaParser = new VegaParser( + `{ "$schema": "https://vega.github.io/schema/vega/v3.json", "marks": [ { @@ -222,7 +212,9 @@ describe('VegaVisualizations', () => { } } ] - }`, new SearchCache()); + }`, + new SearchCache() + ); await vegaParser.parseAsync(); domNode.style.width = '256px'; @@ -240,10 +232,8 @@ describe('VegaVisualizations', () => { vegaVis.destroy(); } }); - }); - async function compareImage(expectedImageSource) { const elementList = domNode.querySelectorAll('canvas'); expect(elementList.length).to.equal(1); @@ -267,5 +257,4 @@ describe('VegaVisualizations', () => { domNode.innerHTML = ''; document.body.removeChild(domNode); } - }); diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts index 5136046b31a97..15cf97beb5717 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts @@ -20,13 +20,13 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; -import { visualizations } from '../../visualizations/public'; +import { setup as setupVisualizations } from '../../visualizations/public/np_ready/public/legacy'; import { VegaPluginSetupDependencies } from './plugin'; import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const plugins: Readonly = { - visualizations, + visualizations: setupVisualizations, data: npSetup.plugins.data, // Temporary solution diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts index 039ef49cb2289..b2a6fade883ca 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts @@ -25,7 +25,7 @@ import { } from '../../../../core/public'; import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizationsSetup } from '../../visualizations/public/np_ready/public'; import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; @@ -61,7 +61,7 @@ export class VegaPlugin implements Plugin, void> { data.expressions.registerFunction(() => createVegaFn(visualizationDependencies)); - visualizations.types.VisTypesRegistryProvider.register(() => + visualizations.types.registerVisualization(() => createVegaTypeDefinition(visualizationDependencies) ); } 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 a930e369c3e1c..22a71bd999d54 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 @@ -17,7 +17,8 @@ * under the License. */ import { Filter } from '@kbn/es-query'; -import { timefilter, TimeRange } from 'ui/timefilter'; +import { timefilter } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { Query } from 'src/legacy/core_plugins/data/public'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts index 6ffcd8867ffea..d1f04c794e3c6 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts @@ -25,7 +25,7 @@ import { DefaultEditorSize } from 'ui/vis/editor_size'; import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; import vegaEditorTemplate from './vega_editor_template.html'; -import { visFactory } from '../../visualizations/public'; +import { visFactory } from '../../visualizations/public/np_ready/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; diff --git a/src/legacy/core_plugins/visualizations/index.ts b/src/legacy/core_plugins/visualizations/index.ts index bb9ef1588bdc2..3642071667f48 100644 --- a/src/legacy/core_plugins/visualizations/index.ts +++ b/src/legacy/core_plugins/visualizations/index.ts @@ -25,7 +25,7 @@ export default function VisualizationsPlugin(kibana: any) { const config: Legacy.PluginSpecOptions = { id: 'visualizations', require: ['data'], - publicDir: resolve(__dirname, 'public'), + publicDir: resolve(__dirname, 'public/np_ready/public'), config: (Joi: any) => { return Joi.object({ enabled: Joi.boolean().default(true), diff --git a/src/legacy/core_plugins/visualizations/public/index.ts b/src/legacy/core_plugins/visualizations/public/index.ts deleted file mode 100644 index 10fef3112335b..0000000000000 --- a/src/legacy/core_plugins/visualizations/public/index.ts +++ /dev/null @@ -1,71 +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 { FiltersService, FiltersSetup } from './filters'; -import { TypesService, TypesSetup } from './types'; - -class VisualizationsPlugin { - private readonly filters: FiltersService; - private readonly types: TypesService; - - constructor() { - this.filters = new FiltersService(); - this.types = new TypesService(); - } - - public setup() { - return { - filters: this.filters.setup(), - types: this.types.setup(), - }; - } - - public stop() { - this.filters.stop(); - this.types.stop(); - } -} - -/** - * We export visualizations here so that users importing from 'plugins/visualizations' - * will automatically receive the response value of the `setup` contract, mimicking - * the data that will eventually be injected by the new platform. - */ -export const visualizations = new VisualizationsPlugin().setup(); - -/** @public */ -export interface VisualizationsSetup { - filters: FiltersSetup; - types: TypesSetup; -} - -/** @public types */ -export { - Vis, - visFactory, - DefaultEditorSize, - VisParams, - VisProvider, - VisState, - // VisualizationController, - // VisType, - VisTypeAlias, - VisTypesRegistry, - Status, -} from './types'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json b/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json new file mode 100644 index 0000000000000..8ecf3dfce6e94 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "visualizations", + "version": "kibana", + "requiredPlugins": [ + ], + "server": false, + "ui": true + } + \ No newline at end of file diff --git a/src/legacy/core_plugins/visualizations/public/filters/filters_service.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/filters_service.ts similarity index 87% rename from src/legacy/core_plugins/visualizations/public/filters/filters_service.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/filters/filters_service.ts index 60c26d7cbdc1d..51709f365dbbd 100644 --- a/src/legacy/core_plugins/visualizations/public/filters/filters_service.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/filters_service.ts @@ -17,8 +17,10 @@ * under the License. */ -// @ts-ignore -import { VisFiltersProvider, createFilter } from 'ui/vis/vis_filters'; +interface SetupDependecies { + VisFiltersProvider: any; + createFilter: any; +} /** * Vis Filters Service @@ -26,7 +28,7 @@ import { VisFiltersProvider, createFilter } from 'ui/vis/vis_filters'; * @internal */ export class FiltersService { - public setup() { + public setup({ VisFiltersProvider, createFilter }: SetupDependecies) { return { VisFiltersProvider, createFilter, diff --git a/src/legacy/core_plugins/visualizations/public/filters/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/filters/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts diff --git a/src/dev/build/tasks/nodejs/__tests__/node_shasums.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts similarity index 57% rename from src/dev/build/tasks/nodejs/__tests__/node_shasums.js rename to src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 3e400283dbd80..d38acaa3cf3f2 100644 --- a/src/dev/build/tasks/nodejs/__tests__/node_shasums.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -17,13 +17,29 @@ * under the License. */ -import expect from '@kbn/expect'; +import { PluginInitializerContext } from 'src/core/public'; +import { VisualizationsPublicPlugin, Setup } from './plugin'; -import { getNodeShasums } from '../node_shasums'; +/** @public */ +export type VisualizationsSetup = Setup; -describe('src/dev/build/tasks/nodejs/node_shasums', () => { - it('resolves to an object with shasums for node downloads for version', async () => { - const shasums = await getNodeShasums('8.9.4'); - expect(shasums).to.have.property('node-v8.9.4.tar.gz'); - }); -}); +/** @public types */ +export { + Vis, + visFactory, + DefaultEditorSize, + VisParams, + VisProvider, + VisState, + // VisualizationController, + // VisType, + VisTypeAlias, + VisTypesRegistry, + Status, +} from './types'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new VisualizationsPublicPlugin(initializerContext); +} + +export { VisualizationsPublicPlugin as Plugin }; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts new file mode 100644 index 0000000000000..04a49294bd0c6 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/legacy.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { npSetup, npStart } from 'ui/new_platform'; +// @ts-ignore +import { VisFiltersProvider, createFilter } from 'ui/vis/vis_filters'; +// @ts-ignore +import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; +// @ts-ignore +import { VisProvider as Vis } from 'ui/vis/index.js'; +// @ts-ignore +import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ + +import { visTypeAliasRegistry } from './types/vis_type_alias_registry'; + +import { plugin } from '.'; + +const pluginInstance = plugin({} as any); + +export const setup = pluginInstance.setup(npSetup.core, { + __LEGACY: { + VisFiltersProvider, + createFilter, + + Vis, + VisFactoryProvider, + VisTypesRegistryProvider, + defaultFeedbackMessage, + visTypeAliasRegistry, + }, +}); +export const start = pluginInstance.start(npStart.core); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts new file mode 100644 index 0000000000000..df5e4d25dedcc --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/mocks.ts @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +jest.mock('ui/vis/vis_filters'); +jest.mock('ui/vis/default_feedback_message'); +jest.mock('ui/vis/index.js'); +jest.mock('ui/vis/vis_factory'); +jest.mock('ui/registry/vis_types'); +// @ts-ignore +import { VisFiltersProvider, createFilter } from 'ui/vis/vis_filters'; +// @ts-ignore +import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; +// @ts-ignore +import { VisProvider as Vis } from 'ui/vis/index.js'; +// @ts-ignore +import { VisFactoryProvider } from 'ui/vis/vis_factory'; +import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ +jest.mock('./types/vis_type_alias_registry'); +import { visTypeAliasRegistry } from './types/vis_type_alias_registry'; + +import { Plugin } from '.'; +import { coreMock } from '../../../../../../core/public/mocks'; + +export type Setup = jest.Mocked>; +export type Start = jest.Mocked>; + +const createSetupContract = (): Setup => ({ + filters: { + VisFiltersProvider: jest.fn(), + createFilter: jest.fn(), + }, + types: { + Vis, + VisFactoryProvider: jest.fn(), + registerVisualization: jest.fn(), + defaultFeedbackMessage, + visTypeAliasRegistry: { + add: jest.fn(), + get: jest.fn(), + }, + }, +}); + +const createStartContract = (): Start => {}; + +const createInstance = () => { + const plugin = new Plugin({} as any); + + const setup = plugin.setup(coreMock.createSetup(), { + __LEGACY: { + VisFiltersProvider, + createFilter, + + Vis, + VisFactoryProvider, + VisTypesRegistryProvider, + defaultFeedbackMessage, + visTypeAliasRegistry, + }, + }); + const doStart = () => plugin.start(coreMock.createStart()); + + return { + plugin, + setup, + doStart, + }; +}; + +export const visualizationsPluginMock = { + createSetupContract, + createStartContract, + createInstance, +}; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts new file mode 100644 index 0000000000000..abf5974b77532 --- /dev/null +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/plugin.ts @@ -0,0 +1,88 @@ +/* + * 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, CoreSetup, CoreStart, Plugin } from 'src/core/public'; + +import { FiltersService, FiltersSetup } from './filters'; +import { TypesService, TypesSetup } from './types'; +import { VisTypeAliasRegistry } from './types/vis_type_alias_registry'; + +interface SetupDependencies { + __LEGACY: { + VisFiltersProvider: any; + createFilter: any; + + Vis: any; + VisFactoryProvider: any; + VisTypesRegistryProvider: any; + defaultFeedbackMessage: any; + visTypeAliasRegistry: VisTypeAliasRegistry; + }; +} + +export interface Setup { + filters: FiltersSetup; + types: TypesSetup; +} + +export type Start = void; + +export class VisualizationsPublicPlugin implements Plugin { + private readonly filters: FiltersService; + private readonly types: TypesService; + + constructor(initializerContext: PluginInitializerContext) { + this.filters = new FiltersService(); + this.types = new TypesService(); + } + + public setup(core: CoreSetup, { __LEGACY }: SetupDependencies) { + const { + VisFiltersProvider, + createFilter, + Vis, + VisFactoryProvider, + VisTypesRegistryProvider, + defaultFeedbackMessage, + visTypeAliasRegistry, + } = __LEGACY; + + return { + filters: this.filters.setup({ + VisFiltersProvider, + createFilter, + }), + types: this.types.setup({ + Vis, + VisFactoryProvider, + VisTypesRegistryProvider, + defaultFeedbackMessage, + visTypeAliasRegistry, + }), + }; + } + + public start(core: CoreStart) { + // Do nothing yet... + } + + public stop() { + this.filters.stop(); + this.types.stop(); + } +} diff --git a/src/legacy/core_plugins/visualizations/public/types/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/index.ts similarity index 100% rename from src/legacy/core_plugins/visualizations/public/types/index.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/types/index.ts diff --git a/src/legacy/core_plugins/visualizations/public/types/types_service.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/types_service.ts similarity index 72% rename from src/legacy/core_plugins/visualizations/public/types/types_service.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/types/types_service.ts index cb5328812b406..28574917ba1bb 100644 --- a/src/legacy/core_plugins/visualizations/public/types/types_service.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/types_service.ts @@ -17,18 +17,23 @@ * under the License. */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ // @ts-ignore -import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message'; -// @ts-ignore -import { VisProvider as Vis } from 'ui/vis/index.js'; -// @ts-ignore -import { VisFactoryProvider, visFactory } from 'ui/vis/vis_factory'; +import { visFactory } from 'ui/vis/vis_factory'; // @ts-ignore import { DefaultEditorSize } from 'ui/vis/editor_size'; -import { VisTypesRegistryProvider } from 'ui/registry/vis_types'; import * as types from 'ui/vis/vis'; +/* eslint-enable @kbn/eslint/no-restricted-paths */ -import { visTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry'; +import { VisTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry'; + +interface SetupDependencies { + Vis: any; + VisFactoryProvider: any; + VisTypesRegistryProvider: any; + defaultFeedbackMessage: any; + visTypeAliasRegistry: VisTypeAliasRegistry; +} /** * Vis Types Service @@ -36,11 +41,19 @@ import { visTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry'; * @internal */ export class TypesService { - public setup() { + public setup({ + Vis, + VisFactoryProvider, + VisTypesRegistryProvider, + defaultFeedbackMessage, + visTypeAliasRegistry, + }: SetupDependencies) { return { Vis, VisFactoryProvider, - VisTypesRegistryProvider, + registerVisualization: (registerFn: () => any) => { + VisTypesRegistryProvider.register(registerFn); + }, defaultFeedbackMessage, // make default in base vis type, or move? visTypeAliasRegistry, }; diff --git a/src/legacy/core_plugins/visualizations/public/types/vis_type_alias_registry.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts similarity index 90% rename from src/legacy/core_plugins/visualizations/public/types/vis_type_alias_registry.ts rename to src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts index 91040cf966567..eb84f93a0d3ba 100644 --- a/src/legacy/core_plugins/visualizations/public/types/vis_type_alias_registry.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/vis_type_alias_registry.ts @@ -52,7 +52,12 @@ export interface VisTypeAlias { const registry: VisTypeAlias[] = []; -export const visTypeAliasRegistry = { +export interface VisTypeAliasRegistry { + get: () => VisTypeAlias[]; + add: (newVisTypeAlias: VisTypeAlias) => void; +} + +export const visTypeAliasRegistry: VisTypeAliasRegistry = { get: () => [...registry], add: (newVisTypeAlias: VisTypeAlias) => { if (registry.find(visTypeAlias => visTypeAlias.name === newVisTypeAlias.name)) { diff --git a/src/legacy/ui/field_formats/__tests__/field_format.js b/src/legacy/ui/field_formats/__tests__/field_format.js deleted file mode 100644 index bf8ff9f33b18c..0000000000000 --- a/src/legacy/ui/field_formats/__tests__/field_format.js +++ /dev/null @@ -1,180 +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 expect from '@kbn/expect'; -import { asPrettyString } from '../../../core_plugins/kibana/common/utils/as_pretty_string'; -import { FieldFormat } from '../field_format'; - -describe('FieldFormat class', function () { - - let TestFormat; - - beforeEach(function () { - TestFormat = class _TestFormat extends FieldFormat { - static id = 'test-format'; - static title = 'Test Format'; - _convert(val) { - return asPrettyString(val); - } - }; - }); - - describe('params', function () { - it('accepts its params via the constructor', function () { - const f = new TestFormat({ foo: 'bar' }); - expect(f.param('foo')).to.be('bar'); - }); - - it('allows reading a clone of the params', function () { - const params = { foo: 'bar' }; - const f = new TestFormat(params); - const output = f.params(); - expect(output).to.eql(params); - expect(output).to.not.be(params); - }); - }); - - describe('type', function () { - it('links the constructor class to instances as the `type`', function () { - const f = new TestFormat(); - expect(f.type).to.be(TestFormat); - }); - }); - - describe('toJSON', function () { - it('serializes to a version a basic id and param pair', function () { - const f = new TestFormat({ foo: 'bar' }); - const ser = JSON.parse(JSON.stringify(f)); - expect(ser).to.eql({ id: 'test-format', params: { foo: 'bar' } }); - }); - - it('removes param values that match the defaults', function () { - TestFormat.prototype.getParamDefaults = function () { - return { foo: 'bar' }; - }; - - const f = new TestFormat({ foo: 'bar', baz: 'bar' }); - const ser = JSON.parse(JSON.stringify(f)); - expect(ser.params).to.eql({ baz: 'bar' }); - }); - - it('removes the params entirely if they are empty', function () { - const f = new TestFormat(); - const ser = JSON.parse(JSON.stringify(f)); - expect(ser).to.not.have.property('params'); - }); - }); - - describe('converters', function () { - describe('#getConverterFor', function () { - it('returns a converter for a specific content type', function () { - const f = new TestFormat(); - expect(f.getConverterFor('html')()).to.be.a('string'); - expect(f.getConverterFor('text')()).to.be.a('string'); - }); - }); - - describe('#_convert, the instance method or methods used to format values', function () { - it('can be a function, which gets converted to a text and html converter', function () { - TestFormat.prototype._convert = function () { - return 'formatted'; - }; - - const f = new TestFormat(); - const text = f.getConverterFor('text'); - const html = f.getConverterFor('html'); - expect(text).to.not.be(html); - expect(text('formatted')).to.be('formatted'); - expect(html('formatted')).to.be('formatted'); - }); - - it('can be an object, with separate text and html converter', function () { - TestFormat.prototype._convert = { - text: _.constant('formatted text'), - html: _.constant('formatted html'), - }; - - const f = new TestFormat(); - const text = f.getConverterFor('text'); - const html = f.getConverterFor('html'); - expect(text).to.not.be(html); - expect(text('formatted text')).to.be('formatted text'); - expect(html('formatted html')).to.be('formatted html'); - }); - - it('does not escape the output of the text converter', function () { - TestFormat.prototype._convert = _.constant(''); - const f = new TestFormat(); - expect(f.convert('', 'text')).to.contain('<'); - }); - - it('does escape the output of the text converter if used in an html context', function () { - TestFormat.prototype._convert = _.constant(''); - const f = new TestFormat(); - expect(_.trimRight(_.trimLeft(f.convert('', 'html'), ''), '')) - .to.not.contain('<'); - }); - - it('does not escape the output of an html specific converter', function () { - TestFormat.prototype._convert = { - text: _.constant(''), - html: _.constant(''), - }; - - const f = new TestFormat(); - expect(f.convert('', 'text')).to.be(''); - expect(f.convert('', 'html')).to.be(''); - }); - }); - - describe('#convert', function () { - it('formats a value, defaulting to text content type', function () { - TestFormat.prototype._convert = { - text: _.constant('text'), - html: _.constant('html'), - }; - - const f = new TestFormat(); - expect(f.convert('val')).to.be('text'); - }); - - it('formats a value as html, when specified via second param', function () { - TestFormat.prototype._convert = { - text: _.constant('text'), - html: _.constant('html'), - }; - - const f = new TestFormat(); - expect(f.convert('val', 'html')).to.be('html'); - }); - - it('formats a value as " - " when no value is specified', function () { - const f = new TestFormat(); - expect(f.convert()).to.be(' - '); - }); - - it('formats a list of values as text', function () { - const f = new TestFormat(); - expect(f.convert(['one', 'two', 'three'])).to.be('["one","two","three"]'); - }); - }); - - }); -}); diff --git a/src/legacy/ui/field_formats/__tests__/field_formats_mixin.js b/src/legacy/ui/field_formats/__tests__/field_formats_mixin.js deleted file mode 100644 index 8f513fe28c98b..0000000000000 --- a/src/legacy/ui/field_formats/__tests__/field_formats_mixin.js +++ /dev/null @@ -1,83 +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 { FieldFormat } from '../field_format'; -import * as FieldFormatsServiceNS from '../field_formats_service'; -import { fieldFormatsMixin } from '../field_formats_mixin'; - -describe('server.registerFieldFormat(createFormat)', () => { - const sandbox = sinon.createSandbox(); - - let registerFieldFormat; - let fieldFormatServiceFactory; - const serverMock = { decorate() {} }; - beforeEach(async () => { - sandbox.stub(serverMock); - await fieldFormatsMixin({}, serverMock); - [[,, fieldFormatServiceFactory], [,, registerFieldFormat]] = serverMock.decorate.args; - }); - - afterEach(() => sandbox.restore()); - - it('throws if createFormat is not a function', () => { - expect(() => registerFieldFormat()).to.throwError(error => { - expect(error.message).to.match(/createFormat is not a function/i); - }); - }); - - it('calls the createFormat() function with the FieldFormat class', () => { - const createFormat = sinon.stub(); - registerFieldFormat(createFormat); - sinon.assert.calledOnce(createFormat); - sinon.assert.calledWithExactly(createFormat, sinon.match.same(FieldFormat)); - }); - - it('passes the returned class to the FieldFormatsService', async () => { - const { FieldFormatsService: ActualFFS } = FieldFormatsServiceNS; - sandbox.stub(FieldFormatsServiceNS, 'FieldFormatsService').callsFake((...args) => { - return new ActualFFS(...args); - }); - - const { FieldFormatsService } = FieldFormatsServiceNS; - class FooFormat { - static id = 'foo' - } - registerFieldFormat(() => FooFormat); - - const fieldFormats = await fieldFormatServiceFactory({ - getAll: () => ({}), - getDefaults: () => ({}) - }); - - sinon.assert.calledOnce(FieldFormatsService); - sinon.assert.calledWithExactly( - FieldFormatsService, - // array of fieldFormat classes - [sinon.match.same(FooFormat)], - // getConfig() function - sinon.match.func - ); - - const format = fieldFormats.getInstance({ id: 'foo' }); - expect(format).to.be.a(FooFormat); - }); -}); diff --git a/src/legacy/ui/field_formats/__tests__/field_formats_service.js b/src/legacy/ui/field_formats/__tests__/field_formats_service.js deleted file mode 100644 index 9ae80462c2a07..0000000000000 --- a/src/legacy/ui/field_formats/__tests__/field_formats_service.js +++ /dev/null @@ -1,52 +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 { FieldFormat } from '../field_format'; -import { FieldFormatsService } from '../field_formats_service'; -import { createNumberFormat } from '../../../core_plugins/kibana/common/field_formats/types/number'; - -describe('FieldFormatsService', function () { - - const config = {}; - config['format:defaultTypeMap'] = { - 'number': { 'id': 'number', 'params': {} }, - '_default_': { 'id': 'string', 'params': {} } - }; - config['format:number:defaultPattern'] = '0,0.[000]'; - const getConfig = (key) => config[key]; - const fieldFormatClasses = [createNumberFormat(FieldFormat)]; - - let fieldFormats; - beforeEach(function () { - fieldFormats = new FieldFormatsService(fieldFormatClasses, getConfig); - }); - - it('FieldFormats are accessible via getType method', function () { - const Type = fieldFormats.getType('number'); - expect(Type.id).to.be('number'); - }); - - it('getDefaultInstance returns default FieldFormat instance for fieldType', function () { - const instance = fieldFormats.getDefaultInstance('number', getConfig); - expect(instance.type.id).to.be('number'); - expect(instance.convert('0.33333')).to.be('0.333'); - }); - -}); diff --git a/src/legacy/ui/field_formats/content_types.js b/src/legacy/ui/field_formats/content_types.js deleted file mode 100644 index 39a5cac7b9027..0000000000000 --- a/src/legacy/ui/field_formats/content_types.js +++ /dev/null @@ -1,84 +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 { asPrettyString } from '../../core_plugins/kibana/common/utils/as_pretty_string'; -import { getHighlightHtml } from '../../core_plugins/kibana/common/highlight/highlight_html'; - -const types = { - html: function (format, convert) { - function recurse(value, field, hit, meta) { - if (value == null) { - return asPrettyString(value); - } - - if (!value || typeof value.map !== 'function') { - return convert.call(format, value, field, hit, meta); - } - - const subVals = value.map(v => { - return recurse(v, field, hit, meta); - }); - const useMultiLine = subVals.some(sub => { - return sub.indexOf('\n') > -1; - }); - - return subVals.join(',' + (useMultiLine ? '\n' : ' ')); - } - - return function (...args) { - return `${recurse(...args)}`; - }; - }, - - text: function (format, convert) { - return function recurse(value) { - if (!value || typeof value.map !== 'function') { - return convert.call(format, value); - } - - // format a list of values. In text contexts we just use JSON encoding - return JSON.stringify(value.map(recurse)); - }; - } -}; - -function fallbackText(value) { - return asPrettyString(value); -} - -function fallbackHtml(value, field, hit) { - const formatted = _.escape(this.convert(value, 'text')); - - if (!hit || !hit.highlight || !hit.highlight[field.name]) { - return formatted; - } else { - return getHighlightHtml(formatted, hit.highlight[field.name]); - } -} - -export function contentTypesSetup(format) { - const src = format._convert || {}; - const converters = format._convert = {}; - - converters.text = types.text(format, src.text || fallbackText); - converters.html = types.html(format, src.html || fallbackHtml); - - return format._convert; -} diff --git a/src/legacy/ui/field_formats/content_types/html_content_type.ts b/src/legacy/ui/field_formats/content_types/html_content_type.ts new file mode 100644 index 0000000000000..21cdb21d16895 --- /dev/null +++ b/src/legacy/ui/field_formats/content_types/html_content_type.ts @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { escape, isFunction } from 'lodash'; +import { FieldFormatConvert, IFieldFormat, HtmlConventTypeConvert } from '../types'; + +// @ts-ignore +import { asPrettyString } from '../../../core_plugins/kibana/common/utils/as_pretty_string'; +// @ts-ignore +import { getHighlightHtml } from '../../../core_plugins/kibana/common/highlight/highlight_html'; + +const CONTEXT_TYPE = 'html'; + +const getConvertFn = ( + format: IFieldFormat, + fieldFormatConvert: FieldFormatConvert +): HtmlConventTypeConvert => { + const fallbackHtml: HtmlConventTypeConvert = (value, field, hit) => { + const formatted = escape(format.convert(value, 'text')); + + return !field || !hit || !hit.highlight || !hit.highlight[field.name] + ? formatted + : getHighlightHtml(formatted, hit.highlight[field.name]); + }; + + return (fieldFormatConvert[CONTEXT_TYPE] || fallbackHtml) as HtmlConventTypeConvert; +}; + +export const setup = ( + format: IFieldFormat, + fieldFormatConvert: FieldFormatConvert +): FieldFormatConvert => { + const convert = getConvertFn(format, fieldFormatConvert); + + const recurse: HtmlConventTypeConvert = (value, field, hit, meta) => { + if (value == null) { + return asPrettyString(value); + } + + if (!value || !isFunction(value.map)) { + return convert.call(format, value, field, hit, meta); + } + + const subValues = value.map((v: any) => { + return recurse(v, field, hit, meta); + }); + const useMultiLine = subValues.some((sub: any) => { + return sub.indexOf('\n') > -1; + }); + + return subValues.join(',' + (useMultiLine ? '\n' : ' ')); + }; + + const wrap: HtmlConventTypeConvert = (value, field, hit, meta) => { + return `${recurse(value, field, hit, meta)}`; + }; + + return { + [CONTEXT_TYPE]: wrap, + }; +}; diff --git a/src/legacy/ui/field_formats/content_types/index.ts b/src/legacy/ui/field_formats/content_types/index.ts new file mode 100644 index 0000000000000..b5d98a7bc8393 --- /dev/null +++ b/src/legacy/ui/field_formats/content_types/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { setup as textContentTypeSetup } from './text_content_type'; +export { setup as htmlContentTypeSetup } from './html_content_type'; diff --git a/src/legacy/ui/field_formats/content_types/text_content_type.ts b/src/legacy/ui/field_formats/content_types/text_content_type.ts new file mode 100644 index 0000000000000..0a6983f8f1a87 --- /dev/null +++ b/src/legacy/ui/field_formats/content_types/text_content_type.ts @@ -0,0 +1,47 @@ +/* + * 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 { isFunction } from 'lodash'; +import { IFieldFormat, FieldFormatConvert, TextContextTypeConvert } from '../types'; + +// @ts-ignore +import { asPrettyString } from '../../../core_plugins/kibana/common/utils/as_pretty_string'; + +const CONTEXT_TYPE = 'text'; + +const getConvertFn = (fieldFormatConvert: FieldFormatConvert): TextContextTypeConvert => + (fieldFormatConvert[CONTEXT_TYPE] || asPrettyString) as TextContextTypeConvert; + +export const setup = ( + format: IFieldFormat, + fieldFormatConvert: FieldFormatConvert +): FieldFormatConvert => { + const convert = getConvertFn(fieldFormatConvert); + + const recurse: TextContextTypeConvert = value => { + if (!value || !isFunction(value.map)) { + return convert.call(format, value); + } + + // format a list of values. In text contexts we just use JSON encoding + return JSON.stringify(value.map(recurse)); + }; + + return { [CONTEXT_TYPE]: recurse }; +}; diff --git a/src/legacy/ui/field_formats/converters/custom.ts b/src/legacy/ui/field_formats/converters/custom.ts new file mode 100644 index 0000000000000..bc9b421127228 --- /dev/null +++ b/src/legacy/ui/field_formats/converters/custom.ts @@ -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 { FieldFormat } from '../field_format'; +import { FieldFormatConvert } from '../types'; + +const ID = 'custom'; + +export const createCustomFieldFormat = (convert: FieldFormatConvert) => + class CustomFieldFormat extends FieldFormat { + static id = ID; + + public get _convert() { + return convert; + } + }; diff --git a/src/legacy/ui/field_formats/field_format.js b/src/legacy/ui/field_formats/field_format.js deleted file mode 100644 index 21e4946a86b48..0000000000000 --- a/src/legacy/ui/field_formats/field_format.js +++ /dev/null @@ -1,123 +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 { contentTypesSetup } from './content_types'; - -export function FieldFormat(params) { - // give the constructor a more appropriate name - this.type = this.constructor; - - // keep the params and defaults separate - this._params = params || {}; - - // one content type, so assume text - if (_.isFunction(this._convert)) { - this._convert = { text: this._convert }; - } - - contentTypesSetup(this); -} - -FieldFormat.from = function (converter) { - class FieldFormatFromConverter extends FieldFormat {} - FieldFormatFromConverter.prototype._convert = converter; - return FieldFormatFromConverter; -}; - -/** - * Convert a raw value to a formated string - * @param {any} value - * @param {string} [contentType=text] - optional content type, the only two contentTypes - * currently supported are "html" and "text", which helps - * formatters adjust to different contexts - * @return {string} - the formatted string, which is assumed to be html, safe for - * injecting into the DOM or a DOM attribute - */ -FieldFormat.prototype.convert = function (value, contentType) { - return this.getConverterFor(contentType)(value); -}; - -/** - * Get a convert function that is bound to a specific contentType - * @param {string} [contentType=text] - * @return {function} - a bound converter function - */ -FieldFormat.prototype.getConverterFor = function (contentType) { - return this._convert[contentType || 'text']; -}; - -/** - * Get parameter defaults - * @return {object} - parameter defaults - */ -FieldFormat.prototype.getParamDefaults = function () { - return {}; -}; - -/** - * Get the value of a param. This value may be a default value. - * - * @param {string} name - the param name to fetch - * @return {any} - */ -FieldFormat.prototype.param = function (name) { - const val = this._params[name]; - if (val || val === false || val === 0) { - // truthy, false, or 0 are fine - // '', NaN, null, undefined, etc are not - return val; - } - - return this.getParamDefaults()[name]; -}; - -/** - * Get all of the params in a single object - * @return {object} - */ -FieldFormat.prototype.params = function () { - return _.cloneDeep(_.defaults({}, this._params, this.getParamDefaults())); -}; - -/** - * serialize this format to a simple POJO, with only the params - * that are not default - * - * @return {object} - */ -FieldFormat.prototype.toJSON = function () { - const type = this.type; - const defaults = this.getParamDefaults(); - - let params = _.transform(this._params, function (uniqParams, val, param) { - if (val !== defaults[param]) { - uniqParams[param] = val; - } - }, {}); - - if (!_.size(params)) { - params = undefined; - } - - return { - id: type.id, - params: params - }; -}; diff --git a/src/legacy/ui/field_formats/field_format.test.ts b/src/legacy/ui/field_formats/field_format.test.ts new file mode 100644 index 0000000000000..05912d5b5f4a8 --- /dev/null +++ b/src/legacy/ui/field_formats/field_format.test.ts @@ -0,0 +1,187 @@ +/* + * 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 { constant, trimRight, trimLeft, get } from 'lodash'; +import { FieldFormat } from './field_format'; +import { FieldFormatConvert } from './types'; + +// @ts-ignore +import { asPrettyString } from '../../core_plugins/kibana/common/utils/as_pretty_string'; + +const getTestFormat = ( + _convert: FieldFormatConvert = { + text: (val: string) => asPrettyString(val), + }, + _params?: any +) => + new (class TestFormat extends FieldFormat { + static id = 'test-format'; + static title = 'Test Format'; + + public get _convert() { + return _convert; + } + })(_params); + +describe('FieldFormat class', () => { + describe('params', () => { + test('accepts its params via the constructor', () => { + const f = getTestFormat(undefined, { foo: 'bar' }); + const fooParam = f.param('foo'); + + expect(fooParam).toBe('bar'); + }); + + test('allows reading a clone of the params', () => { + const params = { foo: 'bar' }; + const f = getTestFormat(undefined, params); + const output = f.params(); + + expect(output).toEqual(params); + expect(output).not.toBe(params); + }); + }); + + describe('type', () => { + test('links the constructor class to instances as the `type`', () => { + const f = getTestFormat(); + + expect(get(f.type, 'id')).toBe('test-format'); + expect(get(f.type, 'title')).toBe('Test Format'); + }); + }); + + describe('toJSON', () => { + it('serializes to a version a basic id and param pair', () => { + const f = getTestFormat(undefined, { foo: 'bar' }); + const ser = JSON.parse(JSON.stringify(f)); + + expect(ser).toEqual({ id: 'test-format', params: { foo: 'bar' } }); + }); + + it('removes the params entirely if they are empty', () => { + const f = getTestFormat(); + const ser = JSON.parse(JSON.stringify(f)); + + expect(ser).not.toHaveProperty('params'); + }); + }); + + describe('converters', () => { + describe('#getConverterFor', () => { + it('returns a converter for a specific content type', () => { + const f = getTestFormat(); + const htmlConverter = f.getConverterFor('html'); + const textConverter = f.getConverterFor('text'); + + expect(htmlConverter && typeof htmlConverter('')).toBe('string'); + expect(textConverter && typeof textConverter('')).toBe('string'); + }); + }); + + describe('#_convert, the instance method or methods used to format values', () => { + it('can be a function, which gets converted to a text and html converter', () => { + const f = getTestFormat({ + text: () => 'formatted', + }); + const text = f.getConverterFor('text'); + const html = f.getConverterFor('html'); + + expect(text).not.toBe(html); + expect(text && text('formatted')).toBe('formatted'); + expect(html && html('formatted')).toBe('formatted'); + }); + + it('can be an object, with separate text and html converter', () => { + const f = getTestFormat({ + text: constant('formatted text'), + html: constant('formatted html'), + }); + const text = f.getConverterFor('text'); + const html = f.getConverterFor('html'); + + expect(text).not.toBe(html); + expect(text && text('formatted text')).toBe('formatted text'); + expect(html && html('formatted html')).toBe('formatted html'); + }); + + it('does not escape the output of the text converter', () => { + const f = getTestFormat({ + text: constant(''), + }); + + expect(f.convert('', 'text')).toContain('<'); + }); + + it('does escape the output of the text converter if used in an html context', () => { + const f = getTestFormat({ + text: constant(''), + }); + + const expected = trimRight( + trimLeft(f.convert('', 'html'), ''), + '' + ); + + expect(expected).not.toContain('<'); + }); + + it('does not escape the output of an html specific converter', function() { + const f = getTestFormat({ + text: constant(''), + html: constant(''), + }); + expect(f.convert('', 'text')).toBe(''); + expect(f.convert('', 'html')).toBe(''); + }); + }); + + describe('#convert', () => { + it('formats a value, defaulting to text content type', () => { + const f = getTestFormat({ + text: constant('text'), + html: constant('html'), + }); + + expect(f.convert('val')).toBe('text'); + }); + + it('formats a value as html, when specified via second param', () => { + const f = getTestFormat({ + text: constant('text'), + html: constant('html'), + }); + + expect(f.convert('val', 'html')).toBe('html'); + }); + + it('formats a value as " - " when no value is specified', () => { + const f = getTestFormat(); + + expect(f.convert(undefined)).toBe(' - '); + }); + + it('formats a list of values as text', () => { + const f = getTestFormat(); + + expect(f.convert(['one', 'two', 'three'])).toBe('["one","two","three"]'); + }); + }); + }); +}); diff --git a/src/legacy/ui/field_formats/field_format.ts b/src/legacy/ui/field_formats/field_format.ts new file mode 100644 index 0000000000000..226631660b8a0 --- /dev/null +++ b/src/legacy/ui/field_formats/field_format.ts @@ -0,0 +1,191 @@ +/* + * 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 { isFunction, transform, size, cloneDeep, get, defaults } from 'lodash'; +import { createCustomFieldFormat } from './converters/custom'; +import { ContentType, FieldFormatConvert, FieldFormatConvertFunction } from './types'; + +import { htmlContentTypeSetup, textContentTypeSetup } from './content_types'; + +const DEFAULT_CONTEXT_TYPE = 'text'; + +export abstract class FieldFormat { + /** + * @property {string} - Field Format Id + * @static + * @public + */ + static id: string; + /** + * @property {string} - Field Format Title + * @static + * @public + */ + static title: string; + + /** + * @property {string} - Field Format Type + * @private + */ + static fieldType: string; + + /** + * @property {FieldFormatConvert} + * @private + */ + _convert: FieldFormatConvert = FieldFormat.setupContentType(this, get(this, '_convert', {})); + + /** + * @property {Function} - ref to child class + * @private + */ + type: any = this.constructor; + + constructor(public _params: any = {}) {} + + /** + * Convert a raw value to a formatted string + * @param {any} value + * @param {string} [contentType=text] - optional content type, the only two contentTypes + * currently supported are "html" and "text", which helps + * formatters adjust to different contexts + * @return {string} - the formatted string, which is assumed to be html, safe for + * injecting into the DOM or a DOM attribute + * @public + */ + convert(value: any, contentType: ContentType = DEFAULT_CONTEXT_TYPE): string { + const converter = this.getConverterFor(contentType); + + if (converter) { + return converter.call(this, value); + } + + return value; + } + + /** + * Get a convert function that is bound to a specific contentType + * @param {string} [contentType=text] + * @return {function} - a bound converter function + * @public + */ + getConverterFor( + contentType: ContentType = DEFAULT_CONTEXT_TYPE + ): FieldFormatConvertFunction | null { + if (this._convert) { + return this._convert[contentType]; + } + + return null; + } + + /** + * Get parameter defaults + * @return {object} - parameter defaults + * @public + */ + getParamDefaults(): Record { + return {}; + } + + /** + * Get the value of a param. This value may be a default value. + * + * @param {string} name - the param name to fetch + * @return {any} + * @public + */ + param(name: string): any { + const val = get(this._params, name); + + if (val || val === false || val === 0) { + // truthy, false, or 0 are fine + // '', NaN, null, undefined, etc are not + return val; + } + + return get(this.getParamDefaults(), name); + } + + /** + * Get all of the params in a single object + * @return {object} + * @public + */ + params(): Record { + return cloneDeep(defaults({}, this._params, this.getParamDefaults())); + } + + /** + * Serialize this format to a simple POJO, with only the params + * that are not default + * + * @return {object} + * @public + */ + toJSON() { + const id = get(this.type, 'id'); + const defaultsParams = this.getParamDefaults() || {}; + + const params = transform( + this._params, + (uniqParams, val, param) => { + if (param && val !== get(defaultsParams, param)) { + uniqParams[param] = val; + } + }, + {} + ); + + return { + id, + params: size(params) ? params : undefined, + }; + } + + static from(convertFn: FieldFormatConvertFunction) { + return createCustomFieldFormat(FieldFormat.toConvertObject(convertFn)); + } + + private static setupContentType( + fieldFormat: IFieldFormat, + convert: FieldFormatConvert | FieldFormatConvertFunction + ): FieldFormatConvert { + const convertObject = FieldFormat.toConvertObject(convert); + + return { + ...textContentTypeSetup(fieldFormat, convertObject), + ...htmlContentTypeSetup(fieldFormat, convertObject), + }; + } + + private static toConvertObject( + convert: FieldFormatConvert | FieldFormatConvertFunction + ): FieldFormatConvert { + if (isFunction(convert)) { + return { + [DEFAULT_CONTEXT_TYPE]: convert, + }; + } + return convert; + } +} + +export type FieldFormatConvert = { [key: string]: Function } | FieldFormatConvertFunction; +export type IFieldFormat = PublicMethodsOf; diff --git a/src/legacy/ui/field_formats/index.js b/src/legacy/ui/field_formats/index.ts similarity index 92% rename from src/legacy/ui/field_formats/index.js rename to src/legacy/ui/field_formats/index.ts index 1bbad4800a363..cb15ba7a6b0ae 100644 --- a/src/legacy/ui/field_formats/index.js +++ b/src/legacy/ui/field_formats/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { fieldFormatsMixin } from './field_formats_mixin'; +export { fieldFormatsMixin } from './mixin/field_formats_mixin'; export { FieldFormat } from './field_format'; diff --git a/src/legacy/ui/field_formats/field_formats_mixin.js b/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts similarity index 72% rename from src/legacy/ui/field_formats/field_formats_mixin.js rename to src/legacy/ui/field_formats/mixin/field_formats_mixin.ts index 76c72102ebff7..6f20ab8cdf51f 100644 --- a/src/legacy/ui/field_formats/field_formats_mixin.js +++ b/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts @@ -17,27 +17,29 @@ * under the License. */ -import _ from 'lodash'; +import { has } from 'lodash'; +import { Legacy } from 'kibana'; import { FieldFormatsService } from './field_formats_service'; -import { FieldFormat } from './field_format'; +import { FieldFormat } from '../field_format'; -export function fieldFormatsMixin(kbnServer, server) { - const fieldFormatClasses = []; +export function fieldFormatsMixin(kbnServer: any, server: Legacy.Server) { + const fieldFormatClasses: FieldFormat[] = []; // for use outside of the request context, for special cases - server.decorate('server', 'fieldFormatServiceFactory', async function (uiSettings) { + server.decorate('server', 'fieldFormatServiceFactory', async function(uiSettings) { const uiConfigs = await uiSettings.getAll(); const uiSettingDefaults = await uiSettings.getDefaults(); - Object.keys(uiSettingDefaults).forEach((key) => { - if (_.has(uiConfigs, key) && uiSettingDefaults[key].type === 'json') { + Object.keys(uiSettingDefaults).forEach(key => { + if (has(uiConfigs, key) && uiSettingDefaults[key].type === 'json') { uiConfigs[key] = JSON.parse(uiConfigs[key]); } }); - const getConfig = (key) => uiConfigs[key]; + const getConfig = (key: string) => uiConfigs[key]; + return new FieldFormatsService(fieldFormatClasses, getConfig); }); - server.decorate('server', 'registerFieldFormat', (createFormat) => { + server.decorate('server', 'registerFieldFormat', createFormat => { fieldFormatClasses.push(createFormat(FieldFormat)); }); } diff --git a/src/legacy/ui/field_formats/mixin/field_formats_service.test.ts b/src/legacy/ui/field_formats/mixin/field_formats_service.test.ts new file mode 100644 index 0000000000000..32f1579510c64 --- /dev/null +++ b/src/legacy/ui/field_formats/mixin/field_formats_service.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { FieldFormat } from '../field_format'; +import { FieldFormatsService } from './field_formats_service'; + +// @ts-ignore +import { createNumberFormat } from '../../../core_plugins/kibana/common/field_formats/types/number'; + +const getConfig = (key: string) => { + switch (key) { + case 'format:defaultTypeMap': + return { + number: { id: 'number', params: {} }, + _default_: { id: 'string', params: {} }, + }; + case 'format:number:defaultPattern': + return '0,0.[000]'; + } +}; + +describe('FieldFormatsService', function() { + let fieldFormatsService: FieldFormatsService; + + beforeEach(function() { + const fieldFormatClasses = [createNumberFormat(FieldFormat)]; + + fieldFormatsService = new FieldFormatsService(fieldFormatClasses, getConfig); + }); + + test('FieldFormats are accessible via getType method', function() { + const Type = fieldFormatsService.getType('number'); + + expect(Type.id).toBe('number'); + }); + + test('getDefaultInstance returns default FieldFormat instance for fieldType', function() { + const instance = fieldFormatsService.getDefaultInstance('number'); + + expect(instance.type.id).toBe('number'); + expect(instance.convert('0.33333')).toBe('0.333'); + }); +}); diff --git a/src/legacy/ui/field_formats/field_formats_service.js b/src/legacy/ui/field_formats/mixin/field_formats_service.ts similarity index 66% rename from src/legacy/ui/field_formats/field_formats_service.js rename to src/legacy/ui/field_formats/mixin/field_formats_service.ts index 9584f78046c35..779157f5c418b 100644 --- a/src/legacy/ui/field_formats/field_formats_service.js +++ b/src/legacy/ui/field_formats/mixin/field_formats_service.ts @@ -17,11 +17,20 @@ * under the License. */ -import _ from 'lodash'; +import { indexBy, Dictionary } from 'lodash'; +import { FieldFormat } from '../field_format'; + +interface FieldFormatConfig { + id: string; + params?: Record; +} export class FieldFormatsService { - constructor(fieldFormatClasses, getConfig) { - this._fieldFormats = _.indexBy(fieldFormatClasses, 'id'); + getConfig: any; + _fieldFormats: Dictionary; + + constructor(fieldFormatClasses: FieldFormat[], getConfig: Function) { + this._fieldFormats = indexBy(fieldFormatClasses, 'id'); this.getConfig = getConfig; } @@ -30,9 +39,9 @@ export class FieldFormatsService { * using the format:defaultTypeMap config map * * @param {String} fieldType - the field type - * @return {String} + * @return {FieldFormatConfig} */ - getDefaultConfig(fieldType) { + getDefaultConfig(fieldType: string): FieldFormatConfig { const defaultMap = this.getConfig('format:defaultTypeMap'); return defaultMap[fieldType] || defaultMap._default_; } @@ -43,21 +52,19 @@ export class FieldFormatsService { * @param {String} fieldType * @return {FieldFormat} */ - getDefaultInstance(fieldType) { - const conf = this.getDefaultConfig(fieldType); - const FieldFormat = this._fieldFormats[conf.id]; - return new FieldFormat(conf.params, this.getConfig); + getDefaultInstance(fieldType: string): FieldFormat { + return this.getInstance(this.getDefaultConfig(fieldType)); } /** * Get the fieldFormat instance for a field format configuration. * - * @param {Object} conf:id, conf:params + * @param {FieldFormatConfig} field format config * @return {FieldFormat} */ - getInstance(conf) { - const FieldFormat = this._fieldFormats[conf.id]; - return new FieldFormat(conf.params, this.getConfig); + getInstance(conf: FieldFormatConfig): FieldFormat { + // @ts-ignore + return new this._fieldFormats[conf.id](conf.params, this.getConfig); } /** @@ -66,7 +73,7 @@ export class FieldFormatsService { * @param {String} fieldFormatId - the FieldFormat id * @return {FieldFormat} */ - getType(fieldFormatId) { + getType(fieldFormatId: string): any { return this._fieldFormats[fieldFormatId]; } } diff --git a/src/legacy/ui/field_formats/types.ts b/src/legacy/ui/field_formats/types.ts new file mode 100644 index 0000000000000..f5238422020c2 --- /dev/null +++ b/src/legacy/ui/field_formats/types.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 { Field } from '../../core_plugins/data/public/index_patterns'; + +/** @public **/ +export type ContentType = 'html' | 'text'; + +/** @public **/ +export { IFieldFormat } from './field_format'; + +/** @internal **/ +export type HtmlConventTypeConvert = ( + value: any, + field?: Field, + hit?: Record, + meta?: any +) => string; + +/** @internal **/ +export type TextContextTypeConvert = (value: any) => string; + +/** @internal **/ +export type FieldFormatConvertFunction = HtmlConventTypeConvert | TextContextTypeConvert; + +/** @internal **/ +export interface FieldFormatConvert { + [key: string]: FieldFormatConvertFunction; +} diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index 1c3a9c006acfd..e7d85b8cc3f8e 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -19,6 +19,7 @@ @import './exit_full_screen/index'; @import './field_editor/index'; @import './notify/index'; +@import './saved_objects/index'; @import './share/index'; @import './style_compile/index'; diff --git a/src/legacy/ui/public/chrome/chrome.js b/src/legacy/ui/public/chrome/chrome.js index 8f58da9107673..a5a0521013a6e 100644 --- a/src/legacy/ui/public/chrome/chrome.js +++ b/src/legacy/ui/public/chrome/chrome.js @@ -95,7 +95,6 @@ const waitForBootstrap = new Promise(resolve => { document.body.setAttribute('id', `${internals.app.id}-app`); chrome.setupAngular(); - // targetDomElement.setAttribute('id', 'kibana-body'); targetDomElement.setAttribute('kbn-chrome', 'true'); targetDomElement.setAttribute('ng-class', '{ \'hidden-chrome\': !chrome.getVisible() }'); targetDomElement.className = 'app-wrapper'; diff --git a/src/legacy/ui/public/chrome/directives/kbn_chrome.js b/src/legacy/ui/public/chrome/directives/kbn_chrome.js index d81a1ceb5f288..755cb8b42d363 100644 --- a/src/legacy/ui/public/chrome/directives/kbn_chrome.js +++ b/src/legacy/ui/public/chrome/directives/kbn_chrome.js @@ -77,15 +77,21 @@ export function kbnChromeProvider(chrome, internals) { // Non-scope based code (e.g., React) // Banners - ReactDOM.render( - - - , - document.getElementById('globalBannerList') - ); + const bannerListContainer = document.getElementById('globalBannerList'); + // Banners not supported in New Platform yet + // https://github.com/elastic/kibana/issues/41986 + if (bannerListContainer) { + ReactDOM.render( + + + , + bannerListContainer + ); + } + return chrome; } diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.js.snap index 1b48a8e6a7250..5561fbec68b3b 100644 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.js.snap +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.js.snap @@ -58,6 +58,10 @@ exports[`StringFormatEditor should render normally 1`] = ` "input": "SGVsbG8gd29ybGQ=", "output": "SGVSBG8GD29YBGQ=", }, + Object { + "input": "%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98", + "output": "%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98", + }, ] } /> diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.js index 8d3c1d9fca1c8..9aacc7899afee 100644 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.js +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.js @@ -44,7 +44,8 @@ export class StringFormatEditor extends DefaultFormatEditor { 'STAY CALM!', 'com.organizations.project.ClassName', 'hostname.net', - 'SGVsbG8gd29ybGQ=' + 'SGVsbG8gd29ybGQ=', + '%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98' ]; } diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 1e22003b32833..28d57e9f8e8c9 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -33,7 +33,7 @@ import * as Rx from 'rxjs'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { InternalCoreStart } from 'kibana/public'; +import { CoreStart, LegacyCoreStart } from 'kibana/public'; import { fatalError } from 'ui/notify'; import { capabilities } from 'ui/capabilities'; @@ -77,7 +77,7 @@ export const configureAppAngularModule = (angularModule: IModule) => { .run($setupUrlOverflowHandling(newPlatform)); }; -const getEsUrl = (newPlatform: InternalCoreStart) => { +const getEsUrl = (newPlatform: CoreStart) => { const a = document.createElement('a'); a.href = newPlatform.http.basePath.prepend('/elasticsearch'); const protocolPort = /https/.test(a.protocol) ? 443 : 80; @@ -90,7 +90,7 @@ const getEsUrl = (newPlatform: InternalCoreStart) => { }; }; -const setupCompileProvider = (newPlatform: InternalCoreStart) => ( +const setupCompileProvider = (newPlatform: LegacyCoreStart) => ( $compileProvider: ICompileProvider ) => { if (!newPlatform.injectedMetadata.getLegacyMetadata().devMode) { @@ -98,7 +98,7 @@ const setupCompileProvider = (newPlatform: InternalCoreStart) => ( } }; -const setupLocationProvider = (newPlatform: InternalCoreStart) => ( +const setupLocationProvider = (newPlatform: CoreStart) => ( $locationProvider: ILocationProvider ) => { $locationProvider.html5Mode({ @@ -110,7 +110,7 @@ const setupLocationProvider = (newPlatform: InternalCoreStart) => ( $locationProvider.hashPrefix(''); }; -export const $setupXsrfRequestInterceptor = (newPlatform: InternalCoreStart) => { +export const $setupXsrfRequestInterceptor = (newPlatform: LegacyCoreStart) => { const version = newPlatform.injectedMetadata.getLegacyMetadata().version; // Configure jQuery prefilter @@ -145,7 +145,7 @@ export const $setupXsrfRequestInterceptor = (newPlatform: InternalCoreStart) => * @param {HttpService} $http * @return {undefined} */ -const capture$httpLoadingCount = (newPlatform: InternalCoreStart) => ( +const capture$httpLoadingCount = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $http: IHttpService ) => { @@ -166,7 +166,7 @@ const capture$httpLoadingCount = (newPlatform: InternalCoreStart) => ( * lets us integrate with the angular router so that we can automatically clear * the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly */ -const $setupBreadcrumbsAutoClear = (newPlatform: InternalCoreStart) => ( +const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -213,7 +213,7 @@ const $setupBreadcrumbsAutoClear = (newPlatform: InternalCoreStart) => ( * lets us integrate with the angular router so that we can automatically clear * the badge if we switch to a Kibana app that does not use the badge correctly */ -const $setupBadgeAutoClear = (newPlatform: InternalCoreStart) => ( +const $setupBadgeAutoClear = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -253,7 +253,7 @@ const $setupBadgeAutoClear = (newPlatform: InternalCoreStart) => ( * the helpExtension if we switch to a Kibana app that does not set its own * helpExtension */ -const $setupHelpExtensionAutoClear = (newPlatform: InternalCoreStart) => ( +const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( $rootScope: IRootScopeService, $injector: any ) => { @@ -285,7 +285,7 @@ const $setupHelpExtensionAutoClear = (newPlatform: InternalCoreStart) => ( }); }; -const $setupUrlOverflowHandling = (newPlatform: InternalCoreStart) => ( +const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( $location: ILocationService, $rootScope: IRootScopeService, Private: any, diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 5e0eb2feeb450..4f55349e3efe2 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { InternalCoreSetup, InternalCoreStart } from '../../../../core/public'; +import { LegacyCoreSetup, LegacyCoreStart } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { Setup as InspectorSetup, @@ -34,12 +34,12 @@ export interface PluginsStart { } export const npSetup = { - core: (null as unknown) as InternalCoreSetup, + core: (null as unknown) as LegacyCoreSetup, plugins: {} as PluginsSetup, }; export const npStart = { - core: (null as unknown) as InternalCoreStart, + core: (null as unknown) as LegacyCoreStart, plugins: {} as PluginsStart, }; @@ -48,18 +48,18 @@ export const npStart = { * @internal */ export function __reset__() { - npSetup.core = (null as unknown) as InternalCoreSetup; + npSetup.core = (null as unknown) as LegacyCoreSetup; npSetup.plugins = {} as any; - npStart.core = (null as unknown) as InternalCoreStart; + npStart.core = (null as unknown) as LegacyCoreStart; npStart.plugins = {} as any; } -export function __setup__(coreSetup: InternalCoreSetup, plugins: PluginsSetup) { +export function __setup__(coreSetup: LegacyCoreSetup, plugins: PluginsSetup) { npSetup.core = coreSetup; npSetup.plugins = plugins; } -export function __start__(coreStart: InternalCoreStart, plugins: PluginsStart) { +export function __start__(coreStart: LegacyCoreStart, plugins: PluginsStart) { npStart.core = coreStart; npStart.plugins = plugins; } diff --git a/src/legacy/ui/public/saved_objects/_index.scss b/src/legacy/ui/public/saved_objects/_index.scss new file mode 100644 index 0000000000000..50a192b6a7b17 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/_index.scss @@ -0,0 +1 @@ +@import '../../../../plugins/kibana_react/public/saved_objects/index'; diff --git a/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.js b/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx similarity index 52% rename from src/legacy/ui/public/saved_objects/show_saved_object_save_modal.js rename to src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx index bd2fbba3aa145..6aea3c72e0c34 100644 --- a/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.js +++ b/src/legacy/ui/public/saved_objects/show_saved_object_save_modal.tsx @@ -21,7 +21,25 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nContext } from 'ui/i18n'; -export function showSaveModal(saveModal) { +/** + * Represents the result of trying to persist the saved object. + * Contains `error` prop if something unexpected happened (e.g. network error). + * Contains an `id` if persisting was successful. If `id` and + * `error` are undefined, persisting was not successful, but the + * modal can still recover (e.g. the name of the saved object was already taken). + */ +export type SaveResult = { id?: string } | { error: Error }; + +function isSuccess(result: SaveResult): result is { id?: string } { + return 'id' in result; +} + +interface MinimalSaveModalProps { + onSave: (...args: any[]) => Promise; + onClose: () => void; +} + +export function showSaveModal(saveModal: React.ReactElement) { const container = document.createElement('div'); const closeModal = () => { ReactDOM.unmountComponentAtNode(container); @@ -30,21 +48,19 @@ export function showSaveModal(saveModal) { const onSave = saveModal.props.onSave; - const onSaveConfirmed = (...args) => { - onSave(...args).then(({ id, error }) => { - if (id || error) { - closeModal(); - } - }); + const onSaveConfirmed: MinimalSaveModalProps['onSave'] = async (...args) => { + const response = await onSave(...args); + // close modal if we either hit an error or the saved object got an id + if (Boolean(isSuccess(response) ? response.id : response.error)) { + closeModal(); + } + return response; }; document.body.appendChild(container); - const element = React.cloneElement( - saveModal, - { - onSave: onSaveConfirmed, - onClose: closeModal - } - ); + const element = React.cloneElement(saveModal, { + onSave: onSaveConfirmed, + onClose: closeModal, + }); ReactDOM.render({element}, container); } diff --git a/src/legacy/ui/public/timefilter/index.ts b/src/legacy/ui/public/timefilter/index.ts index 5f5fcf6b19c7f..34f2a367a217c 100644 --- a/src/legacy/ui/public/timefilter/index.ts +++ b/src/legacy/ui/public/timefilter/index.ts @@ -17,8 +17,16 @@ * under the License. */ -export { TimeRange, RefreshInterval } from '../../../../plugins/data/public'; +import uiRoutes from 'ui/routes'; +import { registerTimefilterWithGlobalState, getTimefilterConfig } from './setup_router'; +import { Timefilter, TimeHistory } from '../../../core_plugins/data/public/timefilter'; -export { timefilter, Timefilter, registerTimefilterWithGlobalState } from './timefilter'; -export { timeHistory, TimeHistory } from './time_history'; -export { getTime } from './get_time'; +const config = getTimefilterConfig(); + +export { Timefilter, TimeHistory, getTime } from '../../../core_plugins/data/public/timefilter'; +export const timeHistory = new TimeHistory(); +export const timefilter = new Timefilter(config, timeHistory); + +uiRoutes.addSetupWork((globalState, $rootScope) => { + return registerTimefilterWithGlobalState(timefilter, globalState, $rootScope); +}); diff --git a/src/legacy/ui/public/timefilter/setup_router.ts b/src/legacy/ui/public/timefilter/setup_router.ts new file mode 100644 index 0000000000000..cbd03df455da3 --- /dev/null +++ b/src/legacy/ui/public/timefilter/setup_router.ts @@ -0,0 +1,89 @@ +/* + * 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 { IScope } from 'angular'; +import moment from 'moment'; +import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; +import chrome from 'ui/chrome'; +import { RefreshInterval, TimeRange } from 'src/plugins/data/public'; +import { Timefilter } from '../../../core_plugins/data/public/timefilter'; + +// TODO +// remove everything underneath once globalState is no longer an angular service +// and listener can be registered without angular. +function convertISO8601(stringTime: string): string { + const obj = moment(stringTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ', true); + return obj.isValid() ? obj.toString() : stringTime; +} + +export function getTimefilterConfig() { + const settings = chrome.getUiSettingsClient(); + return { + timeDefaults: settings.get('timepicker:timeDefaults'), + refreshIntervalDefaults: settings.get('timepicker:refreshIntervalDefaults'), + }; +} + +// Currently some parts of Kibana (index patterns, timefilter) rely on addSetupWork in the uiRouter +// and require it to be executed to properly function. +// This function is exposed for applications that do not use uiRoutes like APM +// Kibana issue https://github.com/elastic/kibana/issues/19110 tracks the removal of this dependency on uiRouter +export const registerTimefilterWithGlobalState = _.once( + (timefilter: Timefilter, globalState: any, $rootScope: IScope) => { + // settings have to be re-fetched here, to make sure that settings changed by overrideLocalDefault are taken into account. + const config = getTimefilterConfig(); + timefilter.setTime(_.defaults(globalState.time || {}, config.timeDefaults)); + timefilter.setRefreshInterval( + _.defaults(globalState.refreshInterval || {}, config.refreshIntervalDefaults) + ); + + globalState.on('fetch_with_changes', () => { + // clone and default to {} in one + const newTime: TimeRange = _.defaults({}, globalState.time, config.timeDefaults); + const newRefreshInterval: RefreshInterval = _.defaults( + {}, + globalState.refreshInterval, + config.refreshIntervalDefaults + ); + + if (newTime) { + if (newTime.to) newTime.to = convertISO8601(newTime.to); + if (newTime.from) newTime.from = convertISO8601(newTime.from); + } + + timefilter.setTime(newTime); + timefilter.setRefreshInterval(newRefreshInterval); + }); + + const updateGlobalStateWithTime = () => { + globalState.time = timefilter.getTime(); + globalState.refreshInterval = timefilter.getRefreshInterval(); + globalState.save(); + }; + + subscribeWithScope($rootScope, timefilter.getRefreshIntervalUpdate$(), { + next: updateGlobalStateWithTime, + }); + + subscribeWithScope($rootScope, timefilter.getTimeUpdate$(), { + next: updateGlobalStateWithTime, + }); + } +); diff --git a/src/legacy/ui/public/vis/agg_configs.ts b/src/legacy/ui/public/vis/agg_configs.ts index e3edbef40c654..e16784bb6e2f2 100644 --- a/src/legacy/ui/public/vis/agg_configs.ts +++ b/src/legacy/ui/public/vis/agg_configs.ts @@ -27,8 +27,8 @@ */ import _ from 'lodash'; +import { TimeRange } from 'src/plugins/data/public'; import { Schemas } from '../visualize/loader/pipeline_helpers/build_pipeline'; -import { TimeRange } from '../timefilter'; import { Schema } from '../vis/editors/default/schemas'; import { AggConfig, AggConfigOptions } from './agg_config'; import { AggGroupNames } from './editors/default/agg_groups'; diff --git a/src/legacy/ui/public/vis/request_handlers/courier.js b/src/legacy/ui/public/vis/request_handlers/courier.js index cdd5158c5701d..b6f05f6228ea3 100644 --- a/src/legacy/ui/public/vis/request_handlers/courier.js +++ b/src/legacy/ui/public/vis/request_handlers/courier.js @@ -24,7 +24,7 @@ import { calculateObjectHash } from '../lib/calculate_object_hash'; import { getRequestInspectorStats, getResponseInspectorStats } from '../../courier/utils/courier_inspector_utils'; import { tabifyAggResponse } from '../../agg_response/tabify/tabify'; import { buildTabularInspectorData } from '../../inspector/build_tabular_inspector_data'; -import { getTime } from '../../timefilter/get_time'; +import { getTime } from '../../timefilter'; const CourierRequestHandlerProvider = function () { diff --git a/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts b/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts index 213f1e6252a6d..03751e189210a 100644 --- a/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts +++ b/src/legacy/ui/public/vis/request_handlers/request_handlers.d.ts @@ -17,7 +17,7 @@ * under the License. */ -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { Query } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; import { AggConfigs } from 'ui/vis/agg_configs'; diff --git a/src/legacy/ui/public/visualize/loader/types.ts b/src/legacy/ui/public/visualize/loader/types.ts index 595a0649ec5ef..bb1113d212261 100644 --- a/src/legacy/ui/public/visualize/loader/types.ts +++ b/src/legacy/ui/public/visualize/loader/types.ts @@ -18,7 +18,7 @@ */ import { Filter } from '@kbn/es-query'; -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { Query } from 'src/legacy/core_plugins/data/public'; import { SavedObject } from 'ui/saved_objects/saved_object'; diff --git a/src/legacy/ui/ui_bundles/ui_bundles_controller.js b/src/legacy/ui/ui_bundles/ui_bundles_controller.js index a4521268ea121..5e9ae0abbb183 100644 --- a/src/legacy/ui/ui_bundles/ui_bundles_controller.js +++ b/src/legacy/ui/ui_bundles/ui_bundles_controller.js @@ -25,6 +25,7 @@ import { existsSync } from 'fs'; import del from 'del'; import { makeRe } from 'minimatch'; import mkdirp from 'mkdirp'; +import jsonStableStringify from 'json-stable-stringify'; import { IS_KIBANA_DISTRIBUTABLE } from '../../utils'; @@ -48,6 +49,21 @@ function getWebpackAliases(pluginSpecs) { }, {}); } +function sortAllArrays(input) { + if (Array.isArray(input)) { + return input + .map(i => sortAllArrays(i)) + .sort((a, b) => typeof a === 'string' && typeof b === 'string' ? a.localeCompare(b) : 0); + } + + if (typeof input === 'object') { + return Object.entries(input) + .map(([key, value]) => [key, sortAllArrays(value)]); + } + + return input; +} + export class UiBundlesController { constructor(kbnServer) { const { config, uiApps, uiExports, pluginSpecs } = kbnServer; @@ -59,9 +75,7 @@ export class UiBundlesController { sourceMaps: config.get('optimize.sourceMaps'), kbnVersion: config.get('pkg.version'), buildNum: config.get('pkg.buildNum'), - plugins: pluginSpecs - .map(spec => spec.getId()) - .sort((a, b) => a.localeCompare(b)) + appExtensions: sortAllArrays(uiExports.appExtensions), }; this._filter = makeRe(config.get('optimize.bundleFilter') || '*', { @@ -81,6 +95,13 @@ export class UiBundlesController { this._postLoaders = []; this._bundles = []; + // create a bundle for core-only with no modules + this.add({ + id: 'core', + modules: [], + template: appEntryTemplate + }); + // create a bundle for each uiApp for (const uiApp of uiApps) { this.add({ @@ -143,7 +164,9 @@ export class UiBundlesController { } getContext() { - return JSON.stringify(this._context, null, ' '); + return jsonStableStringify(this._context, { + space: ' ' + }); } resolvePath(...args) { diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 47d13184bfd0a..7e6609c4b5487 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -102,9 +102,7 @@ export function uiRenderMixin(kbnServer, server, config) { async handler(request, h) { const { id } = request.params; const app = server.getUiAppById(id) || server.getHiddenUiAppById(id); - if (!app) { - throw Boom.notFound(`Unknown app: ${id}`); - } + const isCore = !app; const uiSettings = request.getUiSettingsService(); const darkMode = !authEnabled || request.auth.isAuthenticated @@ -130,7 +128,9 @@ export function uiRenderMixin(kbnServer, server, config) { ), `${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`, `${regularBundlePath}/commons.style.css`, - `${regularBundlePath}/${app.getId()}.style.css`, + ...( + !isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : [] + ), ...kbnServer.uiExports.styleSheetPaths .filter(path => ( path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light') @@ -145,7 +145,7 @@ export function uiRenderMixin(kbnServer, server, config) { const bootstrap = new AppBootstrap({ templateData: { - appId: app.getId(), + appId: isCore ? 'core' : app.getId(), regularBundlePath, dllBundlePath, styleSheetPaths, @@ -164,12 +164,11 @@ export function uiRenderMixin(kbnServer, server, config) { }); server.route({ - path: '/app/{id}', + path: '/app/{id}/{any*}', method: 'GET', async handler(req, h) { const id = req.params.id; const app = server.getUiAppById(id); - if (!app) throw Boom.notFound('Unknown app ' + id); try { if (kbnServer.status.isGreen()) { @@ -183,9 +182,15 @@ export function uiRenderMixin(kbnServer, server, config) { } }); - async function getLegacyKibanaPayload({ app, translations, request, includeUserProvidedConfig }) { + async function getUiSettings({ request, includeUserProvidedConfig }) { const uiSettings = request.getUiSettingsService(); + return props({ + defaults: uiSettings.getDefaults(), + user: includeUserProvidedConfig && uiSettings.getUserProvided() + }); + } + async function getLegacyKibanaPayload({ app, translations, request, includeUserProvidedConfig }) { return { app, translations, @@ -198,16 +203,15 @@ export function uiRenderMixin(kbnServer, server, config) { basePath: request.getBasePath(), serverName: config.get('server.name'), devMode: config.get('env.dev'), - uiSettings: await props({ - defaults: uiSettings.getDefaults(), - user: includeUserProvidedConfig && uiSettings.getUserProvided() - }) + uiSettings: await getUiSettings({ request, includeUserProvidedConfig }), }; } async function renderApp({ app, h, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) { const request = h.request; const basePath = request.getBasePath(); + const uiSettings = await getUiSettings({ request, includeUserProvidedConfig }); + app = app || { getId: () => 'core' }; const legacyMetadata = await getLegacyKibanaPayload({ app, @@ -228,13 +232,14 @@ export function uiRenderMixin(kbnServer, server, config) { bootstrapScriptUrl: `${basePath}/bundles/app/${app.getId()}/bootstrap.js`, i18n: (id, options) => i18n.translate(id, options), locale: i18n.getLocale(), - darkMode: get(legacyMetadata.uiSettings.user, ['theme:darkMode', 'userValue'], false), + darkMode: get(uiSettings.user, ['theme:darkMode', 'userValue'], false), injectedMetadata: { version: kbnServer.version, buildNumber: config.get('pkg.buildNum'), branch: config.get('pkg.branch'), basePath, + legacyMode: app.getId() !== 'core', i18n: { translationsUrl: `${basePath}/translations/${i18n.getLocale()}.json`, }, @@ -245,7 +250,7 @@ export function uiRenderMixin(kbnServer, server, config) { request, mergeVariables( injectedVarsOverrides, - await server.getInjectedUiAppVars(app.getId()), + app ? await server.getInjectedUiAppVars(app.getId()) : {}, defaultInjectedVars, ), ), diff --git a/src/legacy/ui/ui_render/views/ui_app.pug b/src/legacy/ui/ui_render/views/ui_app.pug index 5bbcc51e7745c..95b321e09b500 100644 --- a/src/legacy/ui/ui_render/views/ui_app.pug +++ b/src/legacy/ui/ui_render/views/ui_app.pug @@ -114,7 +114,7 @@ block content } } - .kibanaWelcomeView(id="kbn_loading_message", style="display: none;") + .kibanaWelcomeView(id="kbn_loading_message", style="display: none;", data-test-subj="kbnLoadingMessage") .kibanaLoaderWrap .kibanaLoader .kibanaWelcomeLogoCircle diff --git a/src/plugins/data/common/expressions/expression_types/kibana_context.ts b/src/plugins/data/common/expressions/expression_types/kibana_context.ts index 5ee3f7abbbdb0..0a3e8a4db87f6 100644 --- a/src/plugins/data/common/expressions/expression_types/kibana_context.ts +++ b/src/plugins/data/common/expressions/expression_types/kibana_context.ts @@ -18,8 +18,8 @@ */ import { Filter } from '@kbn/es-query'; +import { TimeRange } from 'src/plugins/data/public'; import { Query } from '../../query/types'; -import { TimeRange } from '../../timefilter/types'; const name = 'kibana_context'; diff --git a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap index 47b4a5219068f..5bf5c8be05ed8 100644 --- a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -6,7 +6,7 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` onSubmit={[Function]} > - + Save diff --git a/src/plugins/kibana_react/public/saved_objects/_index.scss b/src/plugins/kibana_react/public/saved_objects/_index.scss new file mode 100644 index 0000000000000..6c773c7f777be --- /dev/null +++ b/src/plugins/kibana_react/public/saved_objects/_index.scss @@ -0,0 +1 @@ +@import './saved_object_save_modal'; diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.scss b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.scss new file mode 100644 index 0000000000000..b8758f692b7f7 --- /dev/null +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_save_modal.scss @@ -0,0 +1,3 @@ +.kbnSavedObjectSaveModal { + width: $euiSizeXXL * 10; +} \ No newline at end of file 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 27da5d90646b3..4d4e429417085 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 @@ -35,8 +35,9 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -interface OnSaveProps { +export interface OnSaveProps { newTitle: string; newCopyOnSave: boolean; isTitleDuplicateConfirmed: boolean; @@ -72,14 +73,14 @@ export class SavedObjectSaveModal extends React.Component { }; public render() { - const { isTitleDuplicateConfirmed, hasTitleDuplicate, title, isLoading } = this.state; + const { isTitleDuplicateConfirmed, hasTitleDuplicate, title } = this.state; return (
@@ -136,22 +137,7 @@ export class SavedObjectSaveModal extends React.Component { /> - - {this.props.confirmButtonLabel ? ( - this.props.confirmButtonLabel - ) : ( - - )} - + {this.renderConfirmButton()}
@@ -204,6 +190,34 @@ export class SavedObjectSaveModal extends React.Component { this.saveSavedObject(); }; + private renderConfirmButton = () => { + const { isLoading, title, hasTitleDuplicate } = this.state; + + let confirmLabel: string | React.ReactNode = hasTitleDuplicate + ? i18n.translate('kibana-react.savedObjects.saveModal.confirmSaveButtonLabel', { + defaultMessage: 'Confirm save', + }) + : i18n.translate('kibana-react.savedObjects.saveModal.saveButtonLabel', { + defaultMessage: 'Save', + }); + + if (this.props.confirmButtonLabel) { + confirmLabel = this.props.confirmButtonLabel; + } + + return ( + + {confirmLabel} + + ); + }; + private renderDuplicateTitleCallout = () => { if (!this.state.hasTitleDuplicate) { return; @@ -230,10 +244,14 @@ export class SavedObjectSaveModal extends React.Component { objectType: this.props.objectType, confirmSaveLabel: ( - + {this.props.confirmButtonLabel + ? this.props.confirmButtonLabel + : i18n.translate( + 'kibana-react.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText', + { + defaultMessage: 'Confirm save', + } + )} ), }} diff --git a/src/test_utils/public/enzyme_helpers.tsx b/src/test_utils/public/enzyme_helpers.tsx index 17027bd8dce48..43ec49c5c1404 100644 --- a/src/test_utils/public/enzyme_helpers.tsx +++ b/src/test_utils/public/enzyme_helpers.tsx @@ -128,3 +128,15 @@ export function renderWithIntl( } export const nextTick = () => new Promise(res => process.nextTick(res)); + +export function shallowWithI18nProvider(child: ReactElement) { + const wrapped = shallow({child}); + const name = typeof child.type === 'string' ? child.type : child.type.name; + return wrapped.find(name).dive(); +} + +export function mountWithI18nProvider(child: ReactElement) { + const wrapped = mount({child}); + const name = typeof child.type === 'string' ? child.type : child.type.name; + return wrapped.find(name); +} diff --git a/test/functional/apps/dashboard/dashboard_filter_bar.js b/test/functional/apps/dashboard/dashboard_filter_bar.js index 312066000c63c..a48393e11b19c 100644 --- a/test/functional/apps/dashboard/dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/dashboard_filter_bar.js @@ -133,10 +133,10 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.setTimepickerInDataRange(); }); - it('are added when pie chart legend item is clicked', async function () { - await dashboardAddPanel.addVisualization('Rendering Test: pie'); + it('are added when a cell magnifying glass is clicked', async function () { + await dashboardAddPanel.addSavedSearch('Rendering-Test:-saved-search'); await PageObjects.dashboard.waitForRenderComplete(); - await pieChart.filterByLegendItem('4,886'); + await testSubjects.click('docTableCellFilter'); const filterCount = await filterBar.getFilterCount(); expect(filterCount).to.equal(1); diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index 15b444cb74151..862ed8c87d347 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -74,7 +74,6 @@ export default function ({ getService, getPageObjects }) { it('tsvb time series shows no data message', async () => { expect(await testSubjects.exists('noTSVBDataMessage')).to.be(true); - await dashboardExpect.tsvbTimeSeriesLegendCount(0); }); it('metric value shows no data', async () => { @@ -134,11 +133,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.goalAndGuageLabelsExist(['0', '0%']); }); - it('tsvb time series shows no data message', async () => { - expect(await testSubjects.exists('noTSVBDataMessage')).to.be(true); - await dashboardExpect.tsvbTimeSeriesLegendCount(0); - }); - it('metric value shows no data', async () => { await dashboardExpect.metricValuesExist(['-']); }); @@ -195,11 +189,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.goalAndGuageLabelsExist(['39.958%', '7,544']); }); - it('tsvb time series', async () => { - expect(await testSubjects.exists('noTSVBDataMessage')).to.be(false); - await dashboardExpect.tsvbTimeSeriesLegendCount(10); - }); - it('metric value', async () => { await dashboardExpect.metricValuesExist(['101']); }); diff --git a/test/functional/apps/dashboard/dashboard_grid.js b/test/functional/apps/dashboard/dashboard_grid.js index 958d61176ded2..a0c22ca85b91b 100644 --- a/test/functional/apps/dashboard/dashboard_grid.js +++ b/test/functional/apps/dashboard/dashboard_grid.js @@ -25,7 +25,6 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['dashboard']); describe('dashboard grid', function () { - this.tags(['skipFirefox']); before(async () => { await PageObjects.dashboard.gotoDashboardLandingPage(); diff --git a/test/functional/apps/dashboard/embeddable_rendering.js b/test/functional/apps/dashboard/embeddable_rendering.js index 831622716f381..e21964495e46e 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.js +++ b/test/functional/apps/dashboard/embeddable_rendering.js @@ -50,7 +50,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.tagCloudWithValuesFound(['CN', 'IN', 'US', 'BR', 'ID']); // TODO add test for 'region map viz' // TODO add test for 'tsvb gauge' viz - await dashboardExpect.tsvbTimeSeriesLegendCount(1); // TODO add test for 'geo map' viz // This tests the presence of the two input control embeddables await dashboardExpect.inputControlItemCount(5); @@ -86,7 +85,6 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.tsvbMetricValuesExist(['0']); await dashboardExpect.tsvbMarkdownWithValuesExists(['Hi Avg last bytes: 0']); await dashboardExpect.tsvbTableCellCount(0); - await dashboardExpect.tsvbTimeSeriesLegendCount(1); await dashboardExpect.tsvbTopNValuesExist(['0']); await dashboardExpect.vegaTextsDoNotExist(['5,000']); }; diff --git a/test/functional/apps/dashboard/full_screen_mode.js b/test/functional/apps/dashboard/full_screen_mode.js index 1f935e9b9aa1b..2a534a66d43e4 100644 --- a/test/functional/apps/dashboard/full_screen_mode.js +++ b/test/functional/apps/dashboard/full_screen_mode.js @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const retry = getService('retry'); - const browser = getService('browser'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['dashboard', 'common']); @@ -69,7 +68,7 @@ export default function ({ getService, getPageObjects }) { it('exits when the text button is clicked on', async () => { const logoButton = await PageObjects.dashboard.getExitFullScreenLogoButton(); - await browser.moveMouseTo(logoButton); + await logoButton.moveMouseTo(); await PageObjects.dashboard.clickExitFullScreenTextButton(); await retry.try(async () => { diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 3cb5d0a71cce5..c29542e6a4035 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -160,7 +160,7 @@ export default function ({ getService, getPageObjects }) { const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); expect(Math.round(newDurationHours)).to.be(3); - const rowData = await PageObjects.discover.getDocTableIndex(1); + const rowData = await PageObjects.discover.getDocTableField(1); expect(rowData).to.have.string('Sep 20, 2015 @ 02:56:02.323'); }); diff --git a/test/functional/apps/management/index.js b/test/functional/apps/management/index.js index c9b444e501789..4d4031b4e489b 100644 --- a/test/functional/apps/management/index.js +++ b/test/functional/apps/management/index.js @@ -33,7 +33,7 @@ export default function ({ getService, loadTestFile }) { }); describe('', function () { - this.tags('ciGroup1'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./_create_index_pattern_wizard')); loadTestFile(require.resolve('./_index_pattern_create_delete')); @@ -45,7 +45,7 @@ export default function ({ getService, loadTestFile }) { }); describe('', function () { - this.tags('ciGroup2'); + this.tags('ciGroup8'); loadTestFile(require.resolve('./_index_pattern_filter')); loadTestFile(require.resolve('./_scripted_fields_filter')); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index 0a2400a367a76..fa79190a5bf94 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -75,7 +75,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await visualBuilder.changePanelPreview(); await visualBuilder.cloneSeries(); - const legend = await visualBuilder.getLegentItems(); + const legend = await visualBuilder.getLegendItems(); const series = await visualBuilder.getSeries(); expect(legend.length).to.be(2); expect(series.length).to.be(2); @@ -108,8 +108,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(actualCount).to.be(expectedLegendValue); }); - // FLAKY: https://github.com/elastic/kibana/issues/40458 - it('should show the correct count in the legend with "Human readable" duration formatter', async () => { + it.skip('should show the correct count in the legend with "Human readable" duration formatter', async () => { await visualBuilder.clickSeriesOption(); await visualBuilder.changeDataFormatter('Duration'); await visualBuilder.setDurationFormatterSettings({ to: 'Human readable' }); @@ -127,7 +126,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(actualCountMin).to.be('3 hours'); }); - describe('Dark mode', () => { + // --reversed class is not implemented in @elastic\chart + describe.skip('Dark mode', () => { before(async () => { await kibanaServer.uiSettings.update({ 'theme:darkMode': true, diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 68a00b29d107e..2a13b6fea9158 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -40,7 +40,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function() { - this.tags('ciGroup3'); + this.tags('ciGroup9'); loadTestFile(require.resolve('./_embedding_chart')); loadTestFile(require.resolve('./_chart_types')); @@ -50,7 +50,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function() { - this.tags('ciGroup4'); + this.tags('ciGroup10'); loadTestFile(require.resolve('./_inspector')); loadTestFile(require.resolve('./_experimental_vis')); @@ -62,7 +62,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function() { - this.tags('ciGroup5'); + this.tags('ciGroup11'); loadTestFile(require.resolve('./_line_chart')); loadTestFile(require.resolve('./_pie_chart')); @@ -76,7 +76,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function() { - this.tags('ciGroup6'); + this.tags('ciGroup12'); loadTestFile(require.resolve('./_tag_cloud')); loadTestFile(require.resolve('./_tile_map')); diff --git a/test/functional/page_objects/common_page.js b/test/functional/page_objects/common_page.js index 651d82608961a..fc47a53a092e8 100644 --- a/test/functional/page_objects/common_page.js +++ b/test/functional/page_objects/common_page.js @@ -151,11 +151,22 @@ export function CommonPageProvider({ getService, getPageObjects }) { navigateToApp(appName, { basePath = '', shouldLoginIfPrompted = true, shouldAcceptAlert = true, hash = '' } = {}) { const self = this; - const appConfig = config.get(['apps', appName]); - const appUrl = getUrl.noAuth(config.get('servers.kibana'), { - pathname: `${basePath}${appConfig.pathname}`, - hash: hash || appConfig.hash, - }); + + let appUrl; + if (config.has(['apps', appName])) { + // Legacy applications + const appConfig = config.get(['apps', appName]); + appUrl = getUrl.noAuth(config.get('servers.kibana'), { + pathname: `${basePath}${appConfig.pathname}`, + hash: hash || appConfig.hash, + }); + } else { + appUrl = getUrl.noAuth(config.get('servers.kibana'), { + pathname: `${basePath}/app/${appName}`, + hash + }); + } + log.debug('navigating to ' + appName + ' url: ' + appUrl); function navigateTo(url) { @@ -359,7 +370,7 @@ export function CommonPageProvider({ getService, getPageObjects }) { throw new Error('Toast is not visible yet'); } }); - await browser.moveMouseTo(toast); + await toast.moveMouseTo(); const title = await (await find.byCssSelector('.euiToastHeader__title')).getVisibleText(); log.debug(title); await find.clickByCssSelector('.euiToast__closeButton'); @@ -370,7 +381,7 @@ export function CommonPageProvider({ getService, getPageObjects }) { const toasts = await find.allByCssSelector('.euiToast'); for (const toastElement of toasts) { try { - await browser.moveMouseTo(toastElement); + await toastElement.moveMouseTo(); const closeBtn = await toastElement.findByCssSelector('.euiToast__closeButton'); await closeBtn.click(); } catch (err) { diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 6954bed438478..dfe8dc1071d46 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -193,6 +193,13 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { return await row.getVisibleText(); } + async getDocTableField(index) { + const field = await find.byCssSelector( + `tr.kbnDocTable__row:nth-child(${index}) > [data-test-subj='docTableField']` + ); + return await field.getVisibleText(); + } + async clickDocSortDown() { await find.clickByCssSelector('.fa-sort-down'); } diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index c64d623979313..5f34e5c4f8637 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -67,11 +67,14 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } public async checkTimeSeriesChartIsPresent() { - await testSubjects.existOrFail('timeseriesChart'); + const isPresent = await find.existsByCssSelector('.tvbVisTimeSeries'); + if (!isPresent) { + throw new Error(`TimeSeries chart is not loaded`); + } } public async checkTimeSeriesLegendIsPresent() { - const isPresent = await find.existsByCssSelector('.tvbLegend'); + const isPresent = await find.existsByCssSelector('.echLegend'); if (!isPresent) { throw new Error(`TimeSeries legend is not loaded`); } @@ -239,7 +242,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro formatter: 'Bytes' | 'Number' | 'Percent' | 'Duration' | 'Custom' ) { const formatterEl = await find.byCssSelector('[id$="row"] .euiComboBox'); - await comboBox.setElement(formatterEl, formatter); + await comboBox.setElement(formatterEl, formatter, { clickWithMouse: true }); } /** @@ -260,11 +263,11 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro }) { if (from) { const fromCombobox = await find.byCssSelector('[id$="from-row"] .euiComboBox'); - await comboBox.setElement(fromCombobox, from); + await comboBox.setElement(fromCombobox, from, { clickWithMouse: true }); } if (to) { const toCombobox = await find.byCssSelector('[id$="to-row"] .euiComboBox'); - await comboBox.setElement(toCombobox, to); + await comboBox.setElement(toCombobox, to, { clickWithMouse: true }); } if (decimalPlaces) { const decimalPlacesInput = await find.byCssSelector('[id$="decimal"]'); @@ -291,9 +294,11 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro await el.type(value); } - public async getRhythmChartLegendValue() { + public async getRhythmChartLegendValue(nth = 0) { await PageObjects.visualize.waitForVisualizationRenderingStabilized(); - const metricValue = await find.byCssSelector('.tvbLegend__itemValue'); + const metricValue = (await find.allByCssSelector( + `.echLegendItem .echLegendItem__displayValue` + ))[nth]; await metricValue.moveMouseTo(); return await metricValue.getVisibleText(); } @@ -447,7 +452,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro public async clickColorPicker(): Promise { const picker = await find.byCssSelector('.tvbColorPicker button'); - await browser.clickMouseButton(picker); + await picker.clickMouseButton(); } public async setBackgroundColor(colorHex: string): Promise { @@ -502,8 +507,8 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro await PageObjects.visualize.waitForRenderingCount(prevRenderingCount + 1); } - public async getLegentItems(): Promise { - return await testSubjects.findAll('tsvbLegendItem'); + public async getLegendItems(): Promise { + return await find.allByCssSelector('.echLegendItem'); } public async getSeries(): Promise { diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index db6656deed7cb..d02638d37aa49 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -1187,7 +1187,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli await retry.try(async () => { const table = await testSubjects.find('tableVis'); const cell = await table.findByCssSelector(`tbody tr:nth-child(${row}) td:nth-child(${column})`); - await browser.moveMouseTo(cell); + await cell.moveMouseTo(); const filterBtn = await testSubjects.findDescendant('filterForCellValue', cell); await filterBtn.click(); }); diff --git a/test/functional/services/browser.ts b/test/functional/services/browser.ts index d52be4b90c043..ccd32590e941c 100644 --- a/test/functional/services/browser.ts +++ b/test/functional/services/browser.ts @@ -155,49 +155,26 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { } /** - * Moves the remote environment’s mouse cursor to the specified element or relative - * position. + * Moves the remote environment’s mouse cursor to the specified point {x, y} which is + * offset to browser page top left corner. * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move * - * @param {WebElementWrapper} element Optional - * @param {number} xOffset Optional - * @param {number} yOffset Optional + * @param {x: number, y: number} point on browser page * @return {Promise} */ - public async moveMouseTo(element: any, xOffset: number, yOffset: number): Promise; - public async moveMouseTo(element: WebElementWrapper): Promise; - public async moveMouseTo( - element: WebElementWrapper, - xOffset?: number, - yOffset?: number - ): Promise { + public async moveMouseTo(point: { x: number; y: number }): Promise { if (this.isW3CEnabled) { - // Workaround for scrolling bug in W3C mode: move pointer to { x: 0, y: 0 } - // https://github.com/mozilla/geckodriver/issues/776 await this.getActions() .move({ x: 0, y: 0 }) .perform(); - if (element instanceof WebElementWrapper) { - await this.getActions() - .move({ x: xOffset || 10, y: yOffset || 10, origin: element._webElement }) - .perform(); - } else { - await this.getActions() - .move({ origin: { x: xOffset, y: yOffset } }) - .perform(); - } + await this.getActions() + .move({ x: point.x, y: point.y, origin: 'pointer' }) + .perform(); } else { - if (element instanceof WebElementWrapper) { - await this.getActions() - .pause(this.getActions().mouse) - .move({ origin: element._webElement }) - .perform(); - } else { - await this.getActions() - .pause(this.getActions().mouse) - .move({ origin: { x: xOffset, y: yOffset } }) - .perform(); - } + await this.getActions() + .pause(this.getActions().mouse) + .move({ x: point.x, y: point.y, origin: 'pointer' }) + .perform(); } } @@ -213,70 +190,47 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { from: { offset: { x: any; y: any }; location: any }, to: { offset: { x: any; y: any }; location: any } ) { - // tslint:disable-next-line:variable-name - let _from; - // tslint:disable-next-line:variable-name - let _to; - // tslint:disable-next-line:variable-name - const _fromOffset = from.offset - ? { x: from.offset.x || 0, y: from.offset.y || 0 } - : { x: 0, y: 0 }; - // tslint:disable-next-line:variable-name - const _toOffset = to.offset ? { x: to.offset.x || 0, y: to.offset.y || 0 } : { x: 0, y: 0 }; - // tslint:disable-next-line:variable-name - const _convertPointW3C = async (point: any, offset: { x: any; y: any }) => { - if (point.location instanceof WebElementWrapper) { - const position = await point.location.getPosition(); - return { - x: Math.round(position.x + offset.x), - y: Math.round(position.y + offset.y), - }; - } else { - return { - x: Math.round(point.location.x + offset.x), - y: Math.round(point.location.y + offset.y), - }; - } - }; - // tslint:disable-next-line:variable-name - const _convertPoint = (point: any) => { - return point.location instanceof WebElementWrapper - ? point.location._webElement - : point.location; - }; - if (this.isW3CEnabled) { - // tslint:disable-next-line:variable-name - _from = await _convertPointW3C(from, _fromOffset); - // tslint:disable-next-line:variable-name - _to = await _convertPointW3C(to, _toOffset); - // tslint:disable-next-line:variable-name - const _offset = { x: _to.x - _from.x, y: _to.y - _from.y }; + // The offset should be specified in pixels relative to the center of the element's bounding box + const getW3CPoint = (data: any) => { + if (!data.offset) { + data.offset = {}; + } + return data.location instanceof WebElementWrapper + ? { x: data.offset.x || 0, y: data.offset.y || 0, origin: data.location._webElement } + : { x: data.location.x, y: data.location.y, origin: 'pointer' }; + }; + const startPoint = getW3CPoint(from); + const endPoint = getW3CPoint(to); + await this.getActions() + .move({ x: 0, y: 0 }) + .perform(); return await this.getActions() - .move({ x: _from.x, y: _from.y, origin: 'pointer' }) + .move(startPoint) .press() - .move({ x: _offset.x, y: _offset.y, origin: 'pointer' }) + .move(endPoint) .release() .perform(); } else { - // until Chromedriver is not supporting W3C Webdriver Actions API - // tslint:disable-next-line:variable-name - _from = _convertPoint(from); - // tslint:disable-next-line:variable-name - _to = _convertPoint(to); - if (from.location instanceof WebElementWrapper && typeof to.location.x === 'number') { + // The offset should be specified in pixels relative to the top-left corner of the element's bounding box + const getOffset: any = (offset: { x: number; y: number }) => + offset ? { x: offset.x || 0, y: offset.y || 0 } : { x: 0, y: 0 }; + + if (from.location instanceof WebElementWrapper === false) { + throw new Error('Dragging point should be WebElementWrapper instance'); + } else if (typeof to.location.x === 'number') { return await this.getActions() - .move({ origin: _from }) + .move({ origin: from.location._webElement }) .press() - .move({ x: _to.x, y: _to.y, origin: 'pointer' }) + .move({ x: to.location.x, y: to.location.y, origin: 'pointer' }) .release() .perform(); } else { return await new LegacyActionSequence(driver) - .mouseMove(_from, _fromOffset) + .mouseMove(from.location._webElement, getOffset(from.offset)) .mouseDown() - .mouseMove(_to, _toOffset) + .mouseMove(to.location._webElement, getOffset(to.offset)) .mouseUp() .perform(); } @@ -320,34 +274,29 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { } /** - * Inserts an action for moving the mouse x and y pixels relative to the specified origin. - * The origin may be defined as the mouse's current position, the viewport, or the center - * of a specific WebElement. Then adds an action for left-click (down/up) with the mouse. + * Moves the remote environment’s mouse cursor to the specified point {x, y} which is + * offset to browser page top left corner. + * Then adds an action for left-click (down/up) with the mouse. * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#click * - * @param {WebElementWrapper} element Optional - * @param {number} xOffset Optional - * @param {number} yOffset Optional + * @param {x: number, y: number} point on browser page * @return {Promise} */ - public async clickMouseButton(element: any, xOffset: number, yOffset: number): Promise; - public async clickMouseButton(element: WebElementWrapper): Promise; - public async clickMouseButton(...args: unknown[]): Promise { - const arg0 = args[0]; - if (arg0 instanceof WebElementWrapper) { + public async clickMouseButton(point: { x: number; y: number }): Promise { + if (this.isW3CEnabled) { await this.getActions() - .pause(this.getActions().mouse) - .move({ origin: arg0._webElement }) + .move({ x: 0, y: 0 }) + .perform(); + await this.getActions() + .move({ x: point.x, y: point.y, origin: 'pointer' }) .click() .perform(); - } else if (isNaN(args[1] as number) || isNaN(args[2] as number) === false) { + } else { await this.getActions() .pause(this.getActions().mouse) - .move({ origin: { x: args[1], y: args[2] } }) + .move({ x: point.x, y: point.y, origin: 'pointer' }) .click() .perform(); - } else { - throw new Error('Element or coordinates should be provided'); } } @@ -378,16 +327,10 @@ export async function BrowserProvider({ getService }: FtrProviderContext) { * @param {WebElementWrapper} element * @return {Promise} */ - public async doubleClick(element?: WebElementWrapper): Promise { - if (element instanceof WebElementWrapper) { - await this.getActions() - .doubleClick(element._webElement) - .perform(); - } else { - await this.getActions() - .doubleClick() - .perform(); - } + public async doubleClick(): Promise { + await this.getActions() + .doubleClick() + .perform(); } /** diff --git a/test/functional/services/combo_box.ts b/test/functional/services/combo_box.ts index b8267f0b4cbe3..5ecd8cd883c8a 100644 --- a/test/functional/services/combo_box.ts +++ b/test/functional/services/combo_box.ts @@ -46,13 +46,21 @@ export function ComboBoxProvider({ getService, getPageObjects }: FtrProviderCont await this.setElement(comboBox, value); } + private async clickOption(isMouseClick: boolean, element: WebElementWrapper) { + return isMouseClick ? await element.clickMouseButton() : await element.click(); + } + /** * set value inside combobox element * * @param comboBoxElement * @param value */ - public async setElement(comboBoxElement: WebElementWrapper, value: string): Promise { + public async setElement( + comboBoxElement: WebElementWrapper, + value: string, + options = { clickWithMouse: false } + ): Promise { log.debug(`comboBox.setElement, value: ${value}`); const isOptionSelected = await this.isOptionSelected(comboBoxElement, value); @@ -65,21 +73,22 @@ export function ComboBoxProvider({ getService, getPageObjects }: FtrProviderCont await this.openOptionsList(comboBoxElement); if (value !== undefined) { - const options = await find.allByCssSelector( + const selectOptions = await find.allByCssSelector( `.euiFilterSelectItem[title^="${value.toString().trim()}"]`, WAIT_FOR_EXISTS_TIME ); - if (options.length > 0) { - await options[0].click(); + if (selectOptions.length > 0) { + await this.clickOption(options.clickWithMouse, selectOptions[0]); } else { // if it doesn't find the item which text starts with value, it will choose the first option - await find.clickByCssSelector('.euiFilterSelectItem'); + const firstOption = await find.byCssSelector('.euiFilterSelectItem'); + await this.clickOption(options.clickWithMouse, firstOption); } } else { - await find.clickByCssSelector('.euiFilterSelectItem'); + const firstOption = await find.byCssSelector('.euiFilterSelectItem'); + await this.clickOption(options.clickWithMouse, firstOption); } - await this.closeOptionsList(comboBoxElement); } @@ -241,11 +250,11 @@ export function ComboBoxProvider({ getService, getPageObjects }: FtrProviderCont value: string ): Promise { log.debug(`comboBox.isOptionSelected, value: ${value}`); - const selectedOptions = await comboBoxElement.findAllByClassName( - 'euiComboBoxPill', - WAIT_FOR_EXISTS_TIME - ); - return selectedOptions.length === 1 && (await selectedOptions[0].getVisibleText()) === value; + const $ = await comboBoxElement.parseDomContent(); + const selectedOptions = $('.euiComboBoxPill') + .toArray() + .map(option => $(option).text()); + return selectedOptions.length === 1 && selectedOptions[0] === value; } } diff --git a/test/functional/services/dashboard/expectations.js b/test/functional/services/dashboard/expectations.js index b6bede32b769c..abafe89c40941 100644 --- a/test/functional/services/dashboard/expectations.js +++ b/test/functional/services/dashboard/expectations.js @@ -62,14 +62,6 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async tsvbTimeSeriesLegendCount(expectedCount) { - log.debug(`DashboardExpect.tsvbTimeSeriesLegendCount(${expectedCount})`); - await retry.try(async () => { - const tsvbLegendItems = await testSubjects.findAll('tsvbLegendItem', findTimeout); - expect(tsvbLegendItems.length).to.be(expectedCount); - }); - } - async fieldSuggestions(expectedFields) { log.debug(`DashboardExpect.fieldSuggestions(${expectedFields})`); const fields = await filterBar.getFilterEditorFields(); diff --git a/test/functional/services/dashboard/panel_actions.js b/test/functional/services/dashboard/panel_actions.js index 051074eb9b3d0..b7327f4af6d17 100644 --- a/test/functional/services/dashboard/panel_actions.js +++ b/test/functional/services/dashboard/panel_actions.js @@ -26,7 +26,6 @@ const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; export function DashboardPanelActionsProvider({ getService, getPageObjects }) { const log = getService('log'); - const browser = getService('browser'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['header', 'common']); @@ -45,7 +44,7 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }) { async toggleContextMenu(parent) { log.debug('toggleContextMenu'); - await (parent ? browser.moveMouseTo(parent) : testSubjects.moveMouseTo('dashboardPanelTitle')); + await (parent ? parent.moveMouseTo() : testSubjects.moveMouseTo('dashboardPanelTitle')); const toggleMenuItem = await this.findContextMenu(parent); await toggleMenuItem.click(); } diff --git a/test/functional/services/inspector.js b/test/functional/services/inspector.js index 9c25ebea48b4f..d7c3109251aaf 100644 --- a/test/functional/services/inspector.js +++ b/test/functional/services/inspector.js @@ -22,7 +22,6 @@ import expect from '@kbn/expect'; export function InspectorProvider({ getService }) { const log = getService('log'); const retry = getService('retry'); - const browser = getService('browser'); const renderable = getService('renderable'); const flyout = getService('flyout'); const testSubjects = getService('testSubjects'); @@ -132,7 +131,7 @@ export function InspectorProvider({ getService }) { await retry.try(async () => { const table = await testSubjects.find('inspectorTable'); const cell = await table.findByCssSelector(`tbody tr:nth-child(${row}) td:nth-child(${column})`); - await browser.moveMouseTo(cell); + await cell.moveMouseTo(); const filterBtn = await testSubjects.findDescendant('filterForInspectorCellValue', cell); await filterBtn.click(); }); @@ -143,7 +142,7 @@ export function InspectorProvider({ getService }) { await retry.try(async () => { const table = await testSubjects.find('inspectorTable'); const cell = await table.findByCssSelector(`tbody tr:nth-child(${row}) td:nth-child(${column})`); - await browser.moveMouseTo(cell); + await cell.moveMouseTo(); const filterBtn = await testSubjects.findDescendant('filterOutInspectorCellValue', cell); await filterBtn.click(); }); diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index b05485618da01..65478c4e5ccd2 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -58,6 +58,7 @@ export class WebElementWrapper { private Keys: IKey = this.webDriver.Key; private driver: WebDriver = this.webDriver.driver; public LegacyAction: any = this.webDriver.LegacyActionSequence; + public isW3CEnabled: boolean = (this.webDriver.driver as any).executor_.w3c === true; public static create( webElement: WebElement | WebElementWrapper, @@ -149,6 +150,12 @@ export class WebElementWrapper { } } + private getActions(): any { + return this.isW3CEnabled + ? (this.driver as any).actions() + : (this.driver as any).actions({ bridge: true }); + } + /** * Returns whether or not the element would be visible to an actual user. This means * that the following types of elements are considered to be not displayed: @@ -402,29 +409,81 @@ export class WebElementWrapper { } /** - * Moves the remote environment’s mouse cursor to the current element + * Moves the remote environment’s mouse cursor to the current element with optional offset * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move - * + * @param { xOffset: 0, yOffset: 0 } options * @return {Promise} */ - public async moveMouseTo() { + public async moveMouseTo(options = { xOffset: 0, yOffset: 0 }) { await this.retryCall(async function moveMouseTo(wrapper) { await wrapper.scrollIntoViewIfNecessary(); - if (wrapper.browserType === Browsers.Firefox) { - const actions = (wrapper.driver as any).actions(); - await actions.move({ x: 0, y: 0 }).perform(); - await actions.move({ x: 10, y: 10, origin: wrapper._webElement }).perform(); + if (wrapper.isW3CEnabled) { + await wrapper + .getActions() + .move({ x: 0, y: 0 }) + .perform(); + await wrapper + .getActions() + .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) + .perform(); } else { - const mouse = (wrapper.driver.actions() as any).mouse(); - const actions = (wrapper.driver as any).actions({ bridge: true }); - await actions - .pause(mouse) - .move({ origin: wrapper._webElement }) + await wrapper + .getActions() + .pause(wrapper.getActions().mouse) + .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) .perform(); } }); } + /** + * Inserts an action for moving the mouse to element center, unless optional offset is provided. + * Then adds an action for left-click (down/up) with the mouse. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#click + * + * @param { xOffset: 0, yOffset: 0 } options Optional + * @return {Promise} + */ + public async clickMouseButton(options = { xOffset: 0, yOffset: 0 }): Promise { + await this.retryCall(async function clickMouseButton(wrapper) { + await wrapper.scrollIntoViewIfNecessary(); + if (wrapper.isW3CEnabled) { + await wrapper + .getActions() + .move({ x: 0, y: 0 }) + .perform(); + await wrapper + .getActions() + .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) + .click() + .perform(); + } else { + await wrapper + .getActions() + .pause(wrapper.getActions().mouse) + .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement }) + .click() + .perform(); + } + }); + } + + /** + * Inserts action for performing a double left-click with the mouse. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#doubleClick + * @param {WebElementWrapper} element + * @return {Promise} + */ + public async doubleClick(): Promise { + await this.retryCall(async function clickMouseButton(wrapper) { + await wrapper.scrollIntoViewIfNecessary(); + await wrapper + .getActions() + .doubleClick(wrapper._webElement) + .perform(); + }); + } + /** * Gets the first element inside this element matching the given CSS selector. * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebElement.html#findElement diff --git a/test/functional/services/saved_query_management_component.js b/test/functional/services/saved_query_management_component.js index 86c565f739b5a..346b470f0e5d2 100644 --- a/test/functional/services/saved_query_management_component.js +++ b/test/functional/services/saved_query_management_component.js @@ -27,14 +27,12 @@ export function SavedQueryManagementComponentProvider({ getService }) { class SavedQueryManagementComponent { async saveNewQuery(name, description, includeFilters, includeTimeFilter) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click('saved-query-management-save-button'); + await this.openSaveCurrentQueryModal(); await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); } async saveNewQueryWithNameError(name) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click('saved-query-management-save-button'); + await this.openSaveCurrentQueryModal(); if (name) { await testSubjects.setValue('saveQueryFormTitle', name); } @@ -137,6 +135,15 @@ export function SavedQueryManagementComponentProvider({ getService }) { await testSubjects.click('saved-query-management-popover-button'); } + async openSaveCurrentQueryModal() { + await this.openSavedQueryManagementComponent(); + + await retry.try(async () => { + await testSubjects.click('saved-query-management-save-button'); + await testSubjects.existOrFail('saveQueryForm'); + }); + } + async saveNewQueryMissingOrFail() { await this.openSavedQueryManagementComponent(); await testSubjects.missingOrFail('saved-query-management-save-button'); diff --git a/test/functional/services/test_subjects.ts b/test/functional/services/test_subjects.ts index 538870ca8268e..aaad4f7c238ff 100644 --- a/test/functional/services/test_subjects.ts +++ b/test/functional/services/test_subjects.ts @@ -30,7 +30,6 @@ interface ExistsOptions { export function TestSubjectsProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); - const browser = getService('browser'); const find = getService('find'); const config = getService('config'); @@ -96,7 +95,7 @@ export function TestSubjectsProvider({ getService }: FtrProviderContext) { log.debug(`TestSubjects.doubleClick(${selector})`); const element = await this.find(selector, timeout); await element.moveMouseTo(); - await browser.doubleClick(element); + await element.doubleClick(); }); } diff --git a/test/mocha_decorations.d.ts b/test/mocha_decorations.d.ts index f6fca538a2159..4645faf3d5fe8 100644 --- a/test/mocha_decorations.d.ts +++ b/test/mocha_decorations.d.ts @@ -26,12 +26,12 @@ type Tags = | 'ciGroup4' | 'ciGroup5' | 'ciGroup6' - | 'ciGroup1' - | 'ciGroup2' - | 'ciGroup3' - | 'ciGroup4' - | 'ciGroup5' - | 'ciGroup6'; + | 'ciGroup7' + | 'ciGroup8' + | 'ciGroup9' + | 'ciGroup10' + | 'ciGroup11' + | 'ciGroup12'; // We need to use the namespace here to match the Mocha definition // eslint-disable-next-line @typescript-eslint/no-namespace diff --git a/test/plugin_functional/plugins/core_plugin_a/public/application.tsx b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx new file mode 100644 index 0000000000000..5d464cf0405d0 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_a/public/application.tsx @@ -0,0 +1,137 @@ +/* + * 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 { BrowserRouter as Router, Route, withRouter, RouteComponentProps } from 'react-router-dom'; + +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiPageSideBar, + EuiTitle, + EuiSideNav, +} from '@elastic/eui'; + +import { AppMountContext, AppMountParameters } from 'kibana/public'; + +const Home = () => ( + + + + +

Welcome to Foo!

+
+
+
+ + + + +

Bar home page section title

+
+
+
+ Wow what a home page this is! +
+
+); + +const PageA = () => ( + + + + +

Page A

+
+
+
+ + + + +

Page A section title

+
+
+
+ Page A's content goes here +
+
+); + +type NavProps = RouteComponentProps & { + navigateToApp: AppMountContext['core']['application']['navigateToApp']; +}; +const Nav = withRouter(({ history, navigateToApp }: NavProps) => ( + history.push('/'), + 'data-test-subj': 'fooNavHome', + }, + { + id: 'page-a', + name: 'Page A', + onClick: () => history.push('/page-a'), + 'data-test-subj': 'fooNavPageA', + }, + { + id: 'linktobar', + name: 'Open Bar / Page B', + onClick: () => navigateToApp('bar', { path: 'page-b?query=here', state: 'foo!!' }), + 'data-test-subj': 'fooNavBarPageB', + }, + ], + }, + ]} + /> +)); + +const FooApp = ({ basename, context }: { basename: string; context: AppMountContext }) => ( + + + +
+ } + titleSize="s" +/> +`; + exports[`NoServicesMessage status: success and historicalDataFound: false 1`] = ` { @@ -78,7 +76,7 @@ export function AddSettingsFlyout({ } }, [serviceName], - { preservePreviousResponse: false } + { preservePreviousData: false } ); const isSampleRateValid = transactionSampleRateRt diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/ErrorCountBadge.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/ErrorCountBadge.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCountBadge.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx similarity index 99% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx index b74d2480b8e63..aa2eddf2a18a0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/StickyTransactionProperties.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx @@ -30,7 +30,7 @@ import { fontSize } from '../../../../style/variables'; interface Props { transaction: Transaction; totalDuration?: number; - errorCount?: number; + errorCount: number; } const ErrorTitle = styled.span` diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/TransactionTabs.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/ServiceLegends.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/ServiceLegends.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/SpanFlyout/index.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/WaterfallItem.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/index.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index 6c9e5d5a2be6c..6f61f62167638 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -3,6 +3,27 @@ exports[`waterfall_helpers getWaterfall should return full waterfall 1`] = ` Object { "duration": 49660, + "entryTransaction": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, "errorCountByTransactionId": Object { "myTransactionId1": 2, "myTransactionId2": 3, @@ -520,6 +541,30 @@ Object { exports[`waterfall_helpers getWaterfall should return partial waterfall 1`] = ` Object { "duration": 8634, + "entryTransaction": Object { + "parent": Object { + "id": "mySpanIdD", + }, + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795823304, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 8634, + }, + "id": "myTransactionId2", + "name": "Api::ProductsController#index", + }, + }, "errorCountByTransactionId": Object { "myTransactionId1": 2, "myTransactionId2": 3, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 7835d47468b7f..cc7697c1d8964 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -30,6 +30,7 @@ interface IWaterfallGroup { } export interface IWaterfall { + entryTransaction?: Transaction; traceRoot?: Transaction; traceRootDuration?: number; @@ -224,7 +225,8 @@ function createGetTransactionById(itemsById: IWaterfallIndex) { } const item = itemsById[id]; - if (idx(item, _ => _.docType) === 'transaction') { + const isTransaction = idx(item, _ => _.docType) === 'transaction'; + if (isTransaction) { return (item as IWaterfallItemTransaction).transaction; } }; @@ -277,8 +279,10 @@ export function getWaterfall( const services = getServices(orderedItems); const getTransactionById = createGetTransactionById(itemsById); const serviceColors = getServiceColors(services); + const entryTransaction = getTransactionById(entryTransactionId); return { + entryTransaction, traceRoot, traceRootDuration, duration, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.test.ts rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/get_agent_marks.ts rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/get_agent_marks.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/index.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx similarity index 83% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx rename to x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx index 02d03ecd1fdf9..698980f2faa74 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Transaction/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -11,7 +11,8 @@ import { EuiPanel, EuiSpacer, EuiTitle, - EuiToolTip + EuiToolTip, + EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; @@ -24,6 +25,7 @@ import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/Tra import { StickyTransactionProperties } from './StickyTransactionProperties'; import { TransactionTabs } from './TransactionTabs'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; function MaybeViewTraceLink({ transaction, @@ -101,20 +103,40 @@ function MaybeViewTraceLink({ } interface Props { - transaction: ITransaction; urlParams: IUrlParams; location: Location; waterfall: IWaterfall; exceedsMax: boolean; + isLoading: boolean; } -export const Transaction: React.SFC = ({ - transaction, +export const WaterfallWithSummmary: React.SFC = ({ urlParams, location, waterfall, - exceedsMax + exceedsMax, + isLoading }) => { + const { entryTransaction } = waterfall; + if (!entryTransaction) { + const content = isLoading ? ( + + ) : ( + + {i18n.translate('xpack.apm.transactionDetails.traceNotFound', { + defaultMessage: 'The selected trace cannot be found' + })} +
+ } + titleSize="s" + /> + ); + + return {content}; + } + return ( @@ -131,10 +153,10 @@ export const Transaction: React.SFC = ({ - + @@ -143,14 +165,14 @@ export const Transaction: React.SFC = ({ - {transaction && ( - - )} +
diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index d9ca32f78f4dc..8060231b32599 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -15,7 +15,7 @@ import { DiscoverErrorLink } from '../DiscoverErrorLink'; import { DiscoverSpanLink } from '../DiscoverSpanLink'; import { DiscoverTransactionLink } from '../DiscoverTransactionLink'; import * as kibanaCore from '../../../../../../../observability/public/context/kibana_core'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -32,7 +32,7 @@ beforeAll(() => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx index d5518f6a96195..4f96f529c471c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; import { InfraLink } from './InfraLink'; import * as kibanaCore from '../../../../../observability/public/context/kibana_core'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; const coreMock = ({ http: { @@ -17,7 +17,7 @@ const coreMock = ({ prepend: (path: string) => `/basepath${path}` } } -} as unknown) as InternalCoreStart; +} as unknown) as LegacyCoreStart; jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx index c01d198b65b5a..521d62205311d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; import { KibanaLink } from './KibanaLink'; import * as kibanaCore from '../../../../../observability/public/context/kibana_core'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; describe('KibanaLink', () => { beforeEach(() => { @@ -19,7 +19,7 @@ describe('KibanaLink', () => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index 524a2d225c84c..aaf27e75ce93b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLJobLink } from './MLJobLink'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; describe('MLJobLink', () => { beforeEach(() => { @@ -19,7 +19,7 @@ describe('MLJobLink', () => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; spyOn(kibanaCore, 'useKibanaCore').and.returnValue(coreMock); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index 73f8bb2c7a213..a6ae4a82cda20 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -10,7 +10,7 @@ import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLLink } from './MLLink'; import * as savedObjects from '../../../../services/rest/savedObjects'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -20,7 +20,7 @@ const coreMock = ({ prepend: (path: string) => `/basepath${path}` } } -} as unknown) as InternalCoreStart; +} as unknown) as LegacyCoreStart; jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 8442f300aa0fc..7b711c3fd09fd 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -13,7 +13,7 @@ import * as Transactions from './mockData'; import * as apmIndexPatternHooks from '../../../../hooks/useAPMIndexPattern'; import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; import { ISavedObject } from '../../../../services/rest/savedObjects'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; jest.mock('ui/kfetch'); @@ -35,7 +35,7 @@ describe('TransactionActionMenu component', () => { prepend: (path: string) => `/basepath${path}` } } - } as unknown) as InternalCoreStart; + } as unknown) as LegacyCoreStart; jest .spyOn(apmIndexPatternHooks, 'useAPMIndexPattern') diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx index 426cc3b500447..fb45d96026f1f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx @@ -17,7 +17,6 @@ import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown' import { TransactionBreakdownHeader } from './TransactionBreakdownHeader'; import { TransactionBreakdownKpiList } from './TransactionBreakdownKpiList'; import { TransactionBreakdownGraph } from './TransactionBreakdownGraph'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; const NoTransactionsTitle = styled.span` font-weight: bold; @@ -34,8 +33,6 @@ const TransactionBreakdown: React.FC<{ receivedDataDuringLifetime } = useTransactionBreakdown(); - const loading = status === FETCH_STATUS.LOADING || status === undefined; - const { kpis, timeseries } = data; const hasHits = kpis.length > 0; @@ -61,7 +58,7 @@ const TransactionBreakdown: React.FC<{ ) : ( - !loading && ( + status === 'success' && ( <> diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js index 57e83ef62c44b..5a1cdc185ac4f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js @@ -27,8 +27,6 @@ import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue // see https://github.com/uber/react-vis/issues/1214 const getNull = d => isValidCoordinateValue(d.y) && !isNaN(d.y); -const X_TICK_TOTAL = 7; - class StaticPlot extends PureComponent { getVisSeries(series, plotValues) { return series @@ -141,12 +139,23 @@ class StaticPlot extends PureComponent { } render() { - const { series, tickFormatX, tickFormatY, plotValues, noHits } = this.props; + const { + width, + series, + tickFormatX, + tickFormatY, + plotValues, + noHits + } = this.props; const { yTickValues } = plotValues; + // approximate number of x-axis ticks based on the width of the plot. There should by approx 1 tick per 100px + // d3 will determine the exact number of ticks based on the selected range + const xTickTotal = Math.floor(width / 100); + return ( - + {noHits ? (
{ onHover={onHover} onMouseLeave={onMouseLeave} onSelectionEnd={onSelectionEnd} - width={100} + width={800} tickFormatX={x => x.getTime()} // Avoid timezone issues in snapshots /> ); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap index ad3b3d25fa4c6..aeef8ed995e92 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap @@ -5836,7 +5836,7 @@ Array [ style={ Object { "height": "256px", - "width": "100px", + "width": "800px", } } > @@ -5850,7 +5850,7 @@ Array [ onMouseLeave={[Function]} onMouseMove={[Function]} onWheel={[Function]} - width={100} + width={800} > @@ -5895,7 +5895,7 @@ Array [ @@ -6213,7 +6213,7 @@ Array [ onMouseLeave={[Function]} onMouseMove={[Function]} onWheel={[Function]} - width={100} + width={800} />
diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx index a26c2797135d3..58057b2a9a201 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx @@ -22,24 +22,21 @@ import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; import { pickKeys } from '../../utils/pickKeys'; +import { useDeepObjectIdentity } from '../../hooks/useDeepObjectIdentity'; interface TimeRange { rangeFrom: string; rangeTo: string; } -function useUiFilters( - params: Pick -): UIFilters { - return useMemo(() => { - const { kuery, environment, ...localUIFilters } = params; - const mappedLocalFilters = mapValues( - pickKeys(localUIFilters, ...localUIFilterNames), - val => (val ? val.split(',') : []) - ) as Partial>; - - return { kuery, environment, ...mappedLocalFilters }; - }, [params]); +function useUiFilters(params: IUrlParams): UIFilters { + const { kuery, environment, ...urlParams } = params; + const localUiFilters = mapValues( + pickKeys(urlParams, ...localUIFilterNames), + val => (val ? val.split(',') : []) + ) as Partial>; + + return useDeepObjectIdentity({ kuery, environment, ...localUiFilters }); } const defaultRefresh = (time: TimeRange) => {}; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useDeepObjectIdentity.ts b/x-pack/legacy/plugins/apm/public/hooks/useDeepObjectIdentity.ts new file mode 100644 index 0000000000000..3bbda44f31ff6 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/hooks/useDeepObjectIdentity.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useRef } from 'react'; +import { isEqual } from 'lodash'; + +// preserve object identity if it is deeply equal to the previous instance of it +export function useDeepObjectIdentity(value: T) { + const valueRef = useRef(value); + + // update ref if object has changed. Else return the original object and discard the new one + if (!isEqual(valueRef.current, value)) { + valueRef.current = value; + } + + return valueRef.current; +} diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx index 6078a2026dca1..24d4cbc067d74 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx @@ -15,37 +15,59 @@ import { KFetchError } from '../../../../../../src/legacy/ui/public/kfetch/kfetc export enum FETCH_STATUS { LOADING = 'loading', SUCCESS = 'success', - FAILURE = 'failure' + FAILURE = 'failure', + PENDING = 'pending' } -type Fetcher = (...args: any[]) => any; -type GetReturn = Exclude< - ReturnType, - undefined -> extends Promise - ? TReturn - : ReturnType; +interface Result { + data: Data; + status: FETCH_STATUS; + error?: Error; +} + +export function useFetcher( + fn: () => Promise | TState | undefined, + fnDeps: any[], + options?: { + preservePreviousData?: boolean; + } +): Result & { refresh: () => void }; + +// To avoid infinite rescursion when infering the type of `TState` `initialState` must be given if `prevResult` is consumed +export function useFetcher( + fn: (prevResult: Result) => Promise | TState | undefined, + fnDeps: any[], + options: { + preservePreviousData?: boolean; + initialState: TState; + } +): Result & { refresh: () => void }; -export function useFetcher( - fn: TFetcher, +export function useFetcher( + fn: Function, fnDeps: any[], - options: { preservePreviousResponse?: boolean } = {} + options: { + preservePreviousData?: boolean; + initialState?: unknown; + } = {} ) { - const { preservePreviousResponse = true } = options; + const { preservePreviousData = true } = options; const id = useComponentId(); const { dispatchStatus } = useContext(LoadingIndicatorContext); - const [result, setResult] = useState<{ - data?: GetReturn; - status?: FETCH_STATUS; - error?: Error; - }>({}); + const [result, setResult] = useState>({ + data: options.initialState, + status: FETCH_STATUS.PENDING + }); const [counter, setCounter] = useState(0); useEffect(() => { let didCancel = false; async function doFetch() { - const promise = fn(); + const promise = fn(result); + // if `fn` doesn't return a promise it is a signal that data fetching was not initiated. + // This can happen if the data fetching is conditional (based on certain inputs). + // In these cases it is not desirable to invoke the global loading spinner, or change the status to success if (!promise) { return; } @@ -53,7 +75,7 @@ export function useFetcher( dispatchStatus({ id, isLoading: true }); setResult(prevResult => ({ - data: preservePreviousResponse ? prevResult.data : undefined, // preserve data from previous state while loading next state + data: preservePreviousData ? prevResult.data : undefined, // preserve data from previous state while loading next state status: FETCH_STATUS.LOADING, error: undefined })); @@ -113,7 +135,7 @@ export function useFetcher( }, [ counter, id, - preservePreviousResponse, + preservePreviousData, dispatchStatus, ...fnDeps /* eslint-enable react-hooks/exhaustive-deps */ diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts index 3964af19bcf6b..d91238109c233 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -8,9 +8,10 @@ import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; import { useUiFilters } from '../context/UrlParamsContext'; import { callApmApi } from '../services/rest/callApmApi'; +import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; const INITIAL_DATA = { - buckets: [], + buckets: [] as TransactionDistributionAPIResponse['buckets'], totalHits: 0, bucketSize: 0 }; @@ -48,16 +49,9 @@ export function useTransactionDistribution(urlParams: IUrlParams) { } }); } - }, [ - serviceName, - start, - end, - transactionType, - transactionName, - transactionId, - traceId, - uiFilters - ]); + // the histogram should not be refetched if the transactionId or traceId changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [serviceName, start, end, transactionType, transactionName, uiFilters]); return { data, status, error }; } diff --git a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts index d3b636e3a4add..5078597d6935e 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts @@ -5,10 +5,10 @@ */ import { useMemo } from 'react'; -import { getWaterfall } from '../components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; import { callApmApi } from '../services/rest/callApmApi'; +import { getWaterfall } from '../components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; const INITIAL_DATA = { root: undefined, @@ -38,5 +38,5 @@ export function useWaterfall(urlParams: IUrlParams) { transactionId ]); - return { data: waterfall, status, error, exceedsMax: data.trace.exceedsMax }; + return { waterfall, status, error, exceedsMax: data.trace.exceedsMax }; } diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index c9a1e583a3cb0..39912ec2ca8b4 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -8,7 +8,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; -import { InternalCoreStart } from 'src/core/public'; +import { LegacyCoreStart } from 'src/core/public'; import { KibanaCoreContextProvider } from '../../../observability/public'; import { history } from '../utils/history'; import { LocationProvider } from '../context/LocationContext'; @@ -54,7 +54,7 @@ const App = () => { }; export class Plugin { - public start(core: InternalCoreStart) { + public start(core: LegacyCoreStart) { const { i18n } = core; ReactDOM.render( diff --git a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts index 0251095c9e75e..d7ebbd87c97e6 100644 --- a/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts +++ b/x-pack/legacy/plugins/canvas/__tests__/fixtures/workpads.ts @@ -6,6 +6,16 @@ import { CanvasWorkpad, CanvasElement, CanvasPage } from '../../types'; const BaseWorkpad: CanvasWorkpad = { + '@created': '2019-02-08T18:35:23.029Z', + '@timestamp': '2019-02-08T18:35:23.029Z', + assets: { + 'asset-ada763f1-295e-4188-8e08-b5bed9e006a1': { + id: 'asset-ada763f1-295e-4188-8e08-b5bed9e006a1', + '@created': '2018-01-17T19:13:09.185Z', + type: 'dataurl', + value: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=', + }, + }, name: 'base workpad', id: 'base-workpad', width: 0, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts index a461e4c305bed..c36a05e405704 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -5,7 +5,7 @@ */ import { Filter as ESFilterType } from '@kbn/es-query'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/public'; -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { buildEmbeddableFilters } from '../../../server/lib/build_embeddable_filters'; import { Filter } from '../../../types'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/all.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/all.ts index ab212419f4a9e..43bdc38340470 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/all.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/all.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { all } from '../../functions/common/all'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { BOOLEAN_TRUE } from '../constants'; +import { BOOLEAN_TRUE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.allHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/any.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/any.ts index cdf24f7c5ea77..d071bc58e1d04 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/any.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/any.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { any } from '../../functions/common/any'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { BOOLEAN_TRUE } from '../constants'; +import { BOOLEAN_TRUE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.anyHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/as.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/as.ts index a8c9a8da1e985..f40107dc4b388 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/as.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/as.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { asFn } from '../../functions/common/as'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE } from '../constants'; +import { DATATABLE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.asHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/axisConfig.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/axisConfig.ts index 5e24a499475b0..30ecae13fdc64 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/axisConfig.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/axisConfig.ts @@ -9,7 +9,7 @@ import { axisConfig } from '../../functions/common/axisConfig'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; import { Position } from '../../../types'; -import { ISO8601 } from '../constants'; +import { ISO8601 } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.axisConfigHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/case.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/case.ts index 9eb4c84b1b301..1a2824fb6af66 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/case.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/case.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { caseFn } from '../../functions/common/case'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; const IF_ARG = '`if`'; const WHEN_ARG = '`when`'; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/clear.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/clear.ts index 75816f34f9e19..6448d0bd4427b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/clear.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/clear.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { clear } from '../../functions/common/clear'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT, TYPE_NULL } from '../constants'; +import { CONTEXT, TYPE_NULL } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.clearHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/columns.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/columns.ts index 290c932789822..807acb5b187d9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/columns.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/columns.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { columns } from '../../functions/common/columns'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE } from '../constants'; +import { DATATABLE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.columnsHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/compare.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/compare.ts index 1df7a3b27271d..d974526804955 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/compare.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/compare.ts @@ -16,7 +16,7 @@ import { BOOLEAN_TRUE, BOOLEAN_FALSE, TYPE_NULL, -} from '../constants'; +} from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.compareHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/containerStyle.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/containerStyle.ts index 2feb6c703b072..39eb38a66279c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/containerStyle.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/containerStyle.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { containerStyle } from '../../functions/common/containerStyle'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CSS } from '../constants'; +import { CSS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.containerStyleHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/context.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/context.ts index 431228d760ad3..6dc01030c5755 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/context.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/context.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { context } from '../../functions/common/context'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.contextHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/csv.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/csv.ts index b1085bb10b416..0dbfbc8324842 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/csv.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/csv.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { csv } from '../../functions/common/csv'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE, CSV } from '../constants'; +import { DATATABLE, CSV } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.csvHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/date.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/date.ts index 962c6b95b2b80..3df56c51258f1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/date.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/date.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { date } from '../../functions/common/date'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { ISO8601, MOMENTJS, JS } from '../constants'; +import { ISO8601, MOMENTJS, JS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.dateHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/do.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/do.ts index 55ea0f8409bd8..fc424192ca01b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/do.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/do.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { doFn } from '../../functions/common/do'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.doHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/eq.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/eq.ts index 7fce30518cfd7..a67d223dc9bec 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/eq.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/eq.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { eq } from '../../functions/common/eq'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.eqHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/escount.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/escount.ts index 754465546f9d8..68f80f6b1e55f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/escount.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/escount.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { escount } from '../../functions/server/escount'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { ELASTICSEARCH, LUCENE } from '../constants'; +import { ELASTICSEARCH, LUCENE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.escountHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/esdocs.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/esdocs.ts index 239184c3cb129..a61ad06d9426f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/esdocs.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/esdocs.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { esdocs } from '../../functions/server/esdocs'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { ELASTICSEARCH, LUCENE } from '../constants'; +import { ELASTICSEARCH, LUCENE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.esdocsHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/essql.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/essql.ts index 8e4be350c61ab..d12c9f3632313 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/essql.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/essql.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { essql } from '../../functions/server/essql'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { ELASTICSEARCH, SQL, ISO8601, UTC } from '../constants'; +import { ELASTICSEARCH, SQL, ISO8601, UTC } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.essqlHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/filterrows.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/filterrows.ts index 808d495f16444..dc5e6a48c8f97 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/filterrows.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/filterrows.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { filterrows } from '../../functions/common/filterrows'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE, TYPE_BOOLEAN, BOOLEAN_TRUE, BOOLEAN_FALSE } from '../constants'; +import { DATATABLE, TYPE_BOOLEAN, BOOLEAN_TRUE, BOOLEAN_FALSE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.filterrowsHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/formatdate.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/formatdate.ts index a141d2bffa8b2..3aa0fb1d421e7 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/formatdate.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/formatdate.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { formatdate } from '../../functions/common/formatdate'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { ISO8601, MOMENTJS } from '../constants'; +import { ISO8601, MOMENTJS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.formatdateHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/formatnumber.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/formatnumber.ts index f1734ec67622b..9944d6cff89dc 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/formatnumber.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/formatnumber.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { formatnumber } from '../../functions/common/formatnumber'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { NUMERALJS } from '../constants'; +import { NUMERALJS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.formatnumberHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/getCell.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/getCell.ts index ae219b294e5ff..a2e50160218c8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/getCell.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/getCell.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { getCell } from '../../functions/common/getCell'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE } from '../constants'; +import { DATATABLE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.getCellHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/gt.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/gt.ts index aec5012f2d077..c03672955b975 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/gt.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/gt.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { gt } from '../../functions/common/gt'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.gtHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/gte.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/gte.ts index f216bd94fa51b..41098683981d3 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/gte.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/gte.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { gte } from '../../functions/common/gte'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.gteHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/head.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/head.ts index 3a6ca22c4d63b..8fe1248f9fc8c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/head.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/head.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { head } from '../../functions/common/head'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE } from '../constants'; +import { DATATABLE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.headHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/if.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/if.ts index 5cbd9b9366791..e58f11ad1bccd 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/if.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/if.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ifFn } from '../../functions/common/if'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { BOOLEAN_TRUE, BOOLEAN_FALSE, CONTEXT } from '../constants'; +import { BOOLEAN_TRUE, BOOLEAN_FALSE, CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.ifHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/image.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/image.ts index 719e9f8f33414..e34fba940ae08 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/image.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/image.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { image, ImageMode } from '../../functions/common/image'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { URL, BASE64 } from '../constants'; +import { URL, BASE64 } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.imageHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/lt.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/lt.ts index aa6c0110f5f0f..e05b8a260d4f9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/lt.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/lt.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { lt } from '../../functions/common/lt'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.ltHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/lte.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/lte.ts index bf6c0bf56e828..659e91aa99c16 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/lte.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/lte.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { lte } from '../../functions/common/lte'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.lteHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/mapColumn.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/mapColumn.ts index 9aa41a0b634c0..c3330132bb4b4 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/mapColumn.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/mapColumn.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { mapColumn } from '../../functions/common/mapColumn'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CANVAS, DATATABLE } from '../constants'; +import { CANVAS, DATATABLE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.mapColumnHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/markdown.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/markdown.ts index 8b5805d0246b1..043c2b1dc135e 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/markdown.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/markdown.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { markdown } from '../../functions/browser/markdown'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { MARKDOWN, CSS } from '../constants'; +import { MARKDOWN, CSS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.markdownHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/math.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/math.ts index a10e0b6ded5aa..1da6ec0c73dce 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/math.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/math.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { math } from '../../functions/common/math'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE, CONTEXT, TINYMATH, TINYMATH_URL } from '../constants'; +import { DATATABLE, CONTEXT, TINYMATH, TINYMATH_URL } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.mathHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts index 77bf71800d38a..b4f1e2448d31a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/metric.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { metric } from '../../functions/common/metric'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { FONT_FAMILY, FONT_WEIGHT, CSS, NUMERALJS } from '../constants'; +import { FONT_FAMILY, FONT_WEIGHT, CSS, NUMERALJS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.metricHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/neq.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/neq.ts index 88c2792393b47..de5ab3a2c301d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/neq.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/neq.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { neq } from '../../functions/common/neq'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.neqHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/pie.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/pie.ts index 3c90ac3ba9d6f..f8b0250a9687c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/pie.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/pie.ts @@ -9,7 +9,7 @@ import { pie } from '../../functions/common/pie'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; import { Position } from '../../../types'; -import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../constants'; +import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.pieHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/plot.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/plot.ts index ad9cbd7cfec8a..45a764ce32ef9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/plot.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/plot.ts @@ -9,7 +9,7 @@ import { plot } from '../../functions/common/plot'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; import { Position } from '../../../types'; -import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../constants'; +import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.plotHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/ply.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/ply.ts index 1a4b4ab53a123..f0a208f256fa8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/ply.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/ply.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { ply } from '../../functions/common/ply'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE } from '../constants'; +import { DATATABLE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.plyHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/pointseries.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/pointseries.ts index ceb0f0e8ac5fd..044c4c59eab01 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/pointseries.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/pointseries.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { pointseries } from '../../functions/server/pointseries'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE, TINYMATH, TINYMATH_URL } from '../constants'; +import { DATATABLE, TINYMATH, TINYMATH_URL } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.pointseriesHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/progress.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/progress.ts index cde3ae71a39ca..e300314629594 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/progress.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/progress.ts @@ -10,7 +10,7 @@ import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; import { Shape } from '../../functions/common/progress'; -import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_TRUE, BOOLEAN_FALSE } from '../constants'; +import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_TRUE, BOOLEAN_FALSE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.progressHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/render.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/render.ts index 54185771417b4..1fea040292626 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/render.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/render.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { render } from '../../functions/common/render'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT, CSS } from '../constants'; +import { CONTEXT, CSS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.renderHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/repeatImage.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/repeatImage.ts index 92df1667e2bf5..3ba2f369225b2 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/repeatImage.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/repeatImage.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { repeatImage } from '../../functions/common/repeatImage'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT, BASE64, URL } from '../constants'; +import { CONTEXT, BASE64, URL } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.repeatImageHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/replace.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/replace.ts index f2d27280cdfd6..680379c233078 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/replace.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/replace.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { replace } from '../../functions/common/replace'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { JS } from '../constants'; +import { JS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.replaceImageHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/revealImage.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/revealImage.ts index 0ef8bed9d9c78..85464abd8c349 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/revealImage.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/revealImage.ts @@ -9,7 +9,7 @@ import { revealImage } from '../../functions/common/revealImage'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; import { Position } from '../../../types'; -import { BASE64, URL } from '../constants'; +import { BASE64, URL } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.revealImageHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/rounddate.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/rounddate.ts index 26901b0f7a776..6fe1901ed9c8d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/rounddate.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/rounddate.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { rounddate } from '../../functions/common/rounddate'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { MOMENTJS } from '../constants'; +import { MOMENTJS } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.rounddateHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/shape.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/shape.ts index 09edb9f2a3dea..0e2081ffd6aaa 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/shape.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/shape.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { shape } from '../../functions/common/shape'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { SVG } from '../constants'; +import { SVG } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.shapeHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/sort.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/sort.ts index a7aed9d955aaa..c377d4900279a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/sort.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/sort.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { sort } from '../../functions/common/sort'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE } from '../constants'; +import { DATATABLE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.sortHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/switch.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/switch.ts index 8d5ec9a2b354e..45a7fb3f372da 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/switch.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/switch.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { switchFn } from '../../functions/common/switch'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CONTEXT } from '../constants'; +import { CONTEXT } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.switchHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/table.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/table.ts index 3a6ff57a9896d..7472f2d0aa4a8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/table.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/table.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { table } from '../../functions/common/table'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../constants'; +import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.tableHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/tail.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/tail.ts index b97d90923f59c..4e84b13dbf086 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/tail.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/tail.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { tail } from '../../functions/common/tail'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { DATATABLE } from '../constants'; +import { DATATABLE } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.tailHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/timefilter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/timefilter.ts index 18c655ac683b3..c202873457022 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/timefilter.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/timefilter.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { timefilter } from '../../functions/common/timefilter'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { ISO8601, ELASTICSEARCH, DATEMATH } from '../constants'; +import { ISO8601, ELASTICSEARCH, DATEMATH } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.timefilterHelpText', { diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/urlparam.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/urlparam.ts index f623409d46e73..918527320868a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/urlparam.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/functions/urlparam.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { urlparam } from '../../functions/browser/urlparam'; import { FunctionHelp } from '.'; import { FunctionFactory } from '../../../types'; -import { TYPE_STRING, URL } from '../constants'; +import { TYPE_STRING, URL } from '../../../i18n'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.urlparamHelpText', { diff --git a/x-pack/legacy/plugins/canvas/i18n/README.md b/x-pack/legacy/plugins/canvas/i18n/README.md new file mode 100644 index 0000000000000..45459b4191fae --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/README.md @@ -0,0 +1,70 @@ +# Canvas and Internationalization (i18n) + +Creating i18n strings in Kibana requires use of the [`@kbn/i18n`](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/GUIDELINE.md) library. The following outlines the strategy for localizing strings in Canvas + +## Why i18n Dictionaries + +In Canvas, we prefer to use "dictionaries" of i18n strings over including translation inline. There are a number of reasons for this. + +### API Signature is Lengthy + +A call to localize a string can look something like this: + +```ts +i18n.translate('xpack.canvas.functions.alterColumn.args.columnHelpText', { + defaultMessage: 'The name of the column to alter.', +}), +``` + +But it can also look something like this: + +```ts +i18n.translate('xpack.canvas.functions.alterColumnHelpText', { + defaultMessage: + 'Converts between core types, including {list}, and {end}, and rename columns. ' + + 'See also {mapColumnFn} and {staticColumnFn}.', + values: { + list: Object.values(DATATABLE_COLUMN_TYPES) + .slice(0, -1) + .map(type => `\`${type}\``) + .join(', '), + end: Object.values(DATATABLE_COLUMN_TYPES).slice(-1)[0], + mapColumnFn: '`mapColumn`', + staticColumnFn: '`staticColumn`', + }, +}); +``` + +In either case, including all of this code inline, where the string is ultimately utilized, makes the code look very uneven, or even complicated. By externalizing the construction of localized strings, we can reduce both of these examples: + +```ts +import { FunctionStrings } from './some/i18n/dictionary'; +const { AlterColumn: strings } = FunctionStrings; + +const help = strings.getColumnHelpText(); +const moreHelp = strings.getAlterColumnHelpText(); +``` + +### Reducing Duplication, Auditing + +By externalizing our strings into these functional dictionaries, we also make identifying duplicate strings easier... thus removing workload from translation teams. We can also deprecate functions. And Since they're written in Typescript, finding usage is easier, so we can easily remove them if a string is no longer used. + +It will also make writing more advanced auditing tools easier. + +## Creating i18n Dictionaries + +There are some Best Practices™️ to follow when localizing Canvas strings: + +- Create dictionaries in `/canvas/i18n`. + - Organize first by the top-level subject or directory, (e.g. `functions`, `renderers`, `components`, etc). +- Don't create too many files. Prefer to eventually split up a dictionary rather than start with many small ones. + - Let's avoid ten files with two strings apiece, for example. +- Create functions that produce a `string`, rather than properties of `string`s. + - Prefer `getSomeString({...values}) => i18n.translate(...);`. + - Avoid `someString: i18n.translate(...);`. + - Standardizes the practice, also allows for passing dynamic values to the localized string in the future. + - Exception to this is the dictionary for Canvas `Function`s, which use a more complex dictionary, influenced by `data/interpreter`. + +## Constants + +In some cases, there are proper nouns or other words that should not be translated, (e.g. 'Canvas', 'JavaScript', 'momentJS', etc). We've created `/canvas/i18n/constants.ts` to collect these words. Use and add to these constants as necessary. diff --git a/x-pack/legacy/plugins/canvas/i18n/angular.ts b/x-pack/legacy/plugins/canvas/i18n/angular.ts new file mode 100644 index 0000000000000..74e5ceb7d6bb7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/angular.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CANVAS as canvas } from './constants'; + +export const AngularStrings = { + CanvasRootController: { + getReadOnlyBadgeText: () => + i18n.translate('xpack.canvas.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + getReadOnlyBadgeTooltip: () => + i18n.translate('xpack.canvas.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save {canvas} workpads', + values: { + canvas, + }, + }), + }, +}; diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts new file mode 100644 index 0000000000000..a076c878d6cc5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ComponentStrings = { + AddEmbeddableFlyout: { + getNoItemsText: () => + i18n.translate('xpack.canvas.embedObject.noMatchingObjectsMessage', { + defaultMessage: 'No matching objects found.', + }), + }, +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/constants.ts b/x-pack/legacy/plugins/canvas/i18n/constants.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/strings/constants.ts rename to x-pack/legacy/plugins/canvas/i18n/constants.ts diff --git a/x-pack/legacy/plugins/canvas/i18n/index.ts b/x-pack/legacy/plugins/canvas/i18n/index.ts new file mode 100644 index 0000000000000..b67ae13a7c160 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from './angular'; +export * from './components'; +export * from './constants'; + +export const getAppDescription = () => + i18n.translate('xpack.canvas.appDescription', { + defaultMessage: 'Showcase your data in a pixel-perfect way.', + }); diff --git a/x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.js b/x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.tsx similarity index 65% rename from x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.js rename to x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.tsx index ad51d7b57d1ee..4738d35836358 100644 --- a/x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.js +++ b/x-pack/legacy/plugins/canvas/public/angular/controllers/canvas.tsx @@ -7,11 +7,22 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { Provider } from 'react-redux'; -import { i18n } from '@kbn/i18n'; +import { Store } from 'redux'; import chrome from 'ui/chrome'; +import { UICapabilities } from 'ui/capabilities'; + +// @ts-ignore Untyped local import { App } from '../../components/app'; +import { AngularStrings } from '../../../i18n'; + +const { CanvasRootController: strings } = AngularStrings; -export function CanvasRootController(canvasStore, $scope, $element, uiCapabilities) { +export function CanvasRootController( + canvasStore: Store, + $scope: any, // Untyped in Kibana + $element: any, // Untyped in Kibana + uiCapabilities: UICapabilities +) { const domNode = $element[0]; // set the read-only badge when appropriate @@ -19,12 +30,8 @@ export function CanvasRootController(canvasStore, $scope, $element, uiCapabiliti uiCapabilities.canvas.save ? undefined : { - text: i18n.translate('xpack.canvas.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('xpack.canvas.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save Canvas workpads', - }), + text: strings.getReadOnlyBadgeText(), + tooltip: strings.getReadOnlyBadgeTooltip(), iconType: 'glasses', } ); diff --git a/x-pack/legacy/plugins/canvas/public/angular/controllers/index.js b/x-pack/legacy/plugins/canvas/public/angular/controllers/index.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/public/angular/controllers/index.js rename to x-pack/legacy/plugins/canvas/public/angular/controllers/index.ts diff --git a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 373f509bf7d9a..52a91a3a2fae6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -6,7 +6,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { SavedObjectFinder, @@ -14,6 +13,9 @@ import { } from 'ui/saved_objects/components/saved_object_finder'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; +import { ComponentStrings } from '../../../i18n'; + +const { AddEmbeddableFlyout: strings } = ComponentStrings; export interface Props { onClose: () => void; @@ -66,9 +68,7 @@ export class AddEmbeddableFlyout extends React.Component { onChoose={this.onAddPanel} savedObjectMetaData={availableSavedObjects} showFilter={true} - noItemsMessage={i18n.translate('xpack.canvas.embedObject.noMatchingObjectsMessage', { - defaultMessage: 'No matching objects found.', - })} + noItemsMessage={strings.getNoItemsText()} /> diff --git a/x-pack/legacy/plugins/canvas/public/components/remove_icon/remove_icon.js b/x-pack/legacy/plugins/canvas/public/components/remove_icon/remove_icon.js deleted file mode 100644 index 726936b8b4e9b..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/remove_icon/remove_icon.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { EuiIcon } from '@elastic/eui'; - -export const RemoveIcon = ({ onClick, className }) => ( -
- -
-); - -RemoveIcon.propTypes = { - onClick: PropTypes.func, - style: PropTypes.object, - className: PropTypes.string, -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/remove_icon/remove_icon.scss b/x-pack/legacy/plugins/canvas/public/components/remove_icon/remove_icon.scss deleted file mode 100644 index efd525f3e8417..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/remove_icon/remove_icon.scss +++ /dev/null @@ -1,29 +0,0 @@ -.canvasRemove { - $clearSize: $euiSize; - @include size($clearSize); // sass-lint:disable-line mixins-before-declarations - position: absolute; - pointer-events: all; - background-color: $euiColorLightShade; - border-radius: $clearSize; - line-height: $clearSize; - top: -$euiSizeL; - right: -$euiSizeL; - - .canvasRemove__icon { - @include size($euiSizeS); - fill: $euiColorEmptyShade; - stroke: $euiColorEmptyShade; - stroke-width: 3px; // increase thickness of icon at such a small size - // better vertical position fix that works with IE - position: relative; - top: -1px; - left: $euiSizeXS; - - } - - &:hover { - background-color: $euiColorDanger; - cursor: pointer; - } - -} 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 5866faa08bdfe..51a897d3c6d9a 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 @@ -8,61 +8,12 @@ import { shallowEqual } from 'recompose'; import { getNodes, getSelectedPage } from '../../state/selectors/workpad'; import { addElement, removeElements, setMultiplePositions } from '../../state/actions/elements'; import { selectToplevelNodes } from '../../state/actions/transient'; -import { matrixToAngle, multiply, rotateZ, translate } from '../../lib/aeroelastic/matrix'; +import { matrixToAngle } from '../../lib/aeroelastic/matrix'; import { arrayToMap, flatten, identity } from '../../lib/aeroelastic/functional'; import { getLocalTransformMatrix } from '../../lib/aeroelastic/layout_functions'; +import { isGroupId, elementToShape } from './positioning_utils'; -export const isGroupId = id => id.startsWith('group'); - -const headerData = id => - isGroupId(id) - ? { id, type: 'group', subtype: 'persistentGroup' } - : { id, type: 'rectangleElement', subtype: '' }; - -const transformData = ({ top, left, width, height, angle }, z) => - multiply( - translate(left + width / 2, top + height / 2, z), // painter's algo: latest item (highest z) goes to top - rotateZ((-angle / 180) * Math.PI) // minus angle as transform:matrix3d uses a left-handed coordinate system - ); - -const simplePosition = ({ id, position, filter }, z) => ({ - ...headerData(id), - width: position.width, - height: position.height, - transformMatrix: transformData(position, z), - filter, -}); - -export const simplePositioning = ({ elements }) => ({ elements: elements.map(simplePosition) }); - -/** - * elementToShape - * - * converts a `kibana-canvas` element to an `aeroelastic` shape. - * - * Shape: the layout algorithms need to deal with objects through their geometric properties, excluding other aspects, - * such as what's inside the element, eg. image or scatter plot. This representation is, at its core, a transform matrix - * that establishes a new local coordinate system https://drafts.csswg.org/css-transforms/#local-coordinate-system plus a - * size descriptor. There are two versions of the transform matrix: - * - `transformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#current-transformation-matrix - * - `localTransformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#transformation-matrix - * - * Element: it also needs to represent the geometry, primarily because of the need to persist it in `redux` and on the - * server, and to accept such data from the server. The redux and server representations will need to change as more general - * projections such as 3D are added. The element also needs to maintain its content, such as an image or a plot. - * - * While all elements on the current page also exist as shapes, there are shapes that are not elements: annotations. - * For example, `rotation_handle`, `border_resize_handle` and `border_connection` are modeled as shapes by the layout - * library, simply for generality. - */ - -export const elementToShape = ({ id, position }, z) => ({ - ...headerData(id), - parent: (position && position.parent) || null, - transformMatrix: transformData(position, z), - a: position.width / 2, // we currently specify half-width, half-height as it leads to - b: position.height / 2, // more regular math (like ellipsis radii rather than diameters) -}); +export * from './positioning_utils'; const shapeToElement = shape => ({ left: shape.transformMatrix[12] - shape.a, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/positioning_utils.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_page/positioning_utils.ts new file mode 100644 index 0000000000000..f4d399065df83 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/positioning_utils.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PositionedElement, ElementPosition } from '../../../types'; +import { multiply, rotateZ, translate } from '../../lib/aeroelastic/matrix'; + +export const isGroupId = (id: string) => id.startsWith('group'); + +const headerData = (id: string) => + isGroupId(id) + ? { id, type: 'group', subtype: 'persistentGroup' } + : { id, type: 'rectangleElement', subtype: '' }; + +const transformData = ({ top, left, width, height, angle }: ElementPosition, z: number) => + multiply( + translate(left + width / 2, top + height / 2, z), // painter's algo: latest item (highest z) goes to top + rotateZ((-angle / 180) * Math.PI) // minus angle as transform:matrix3d uses a left-handed coordinate system + ); + +/** + * elementToShape + * + * converts a `kibana-canvas` element to an `aeroelastic` shape. + * + * Shape: the layout algorithms need to deal with objects through their geometric properties, excluding other aspects, + * such as what's inside the element, eg. image or scatter plot. This representation is, at its core, a transform matrix + * that establishes a new local coordinate system https://drafts.csswg.org/css-transforms/#local-coordinate-system plus a + * size descriptor. There are two versions of the transform matrix: + * - `transformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#current-transformation-matrix + * - `localTransformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#transformation-matrix + * + * Element: it also needs to represent the geometry, primarily because of the need to persist it in `redux` and on the + * server, and to accept such data from the server. The redux and server representations will need to change as more general + * projections such as 3D are added. The element also needs to maintain its content, such as an image or a plot. + * + * While all elements on the current page also exist as shapes, there are shapes that are not elements: annotations. + * For example, `rotation_handle`, `border_resize_handle` and `border_connection` are modeled as shapes by the layout + * library, simply for generality. + */ + +export const elementToShape = ( + { id, position }: { id: string; position: ElementPosition }, + z: number +) => ({ + ...headerData(id), + parent: (position && position.parent) || null, + transformMatrix: transformData(position, z), + a: position.width / 2, // we currently specify half-width, half-height as it leads to + b: position.height / 2, // more regular math (like ellipsis radii rather than diameters) +}); + +const simplePosition = ( + { id, position, filter }: { id: string; position: ElementPosition; filter: string }, + z: number +) => ({ + ...headerData(id), + width: position.width, + height: position.height, + transformMatrix: transformData(position, z), + filter, +}); + +export const simplePositioning = ({ elements }: { elements: PositionedElement[] }) => ({ + elements: elements.map(simplePosition), +}); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_static_page/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_static_page/index.js index f539436e97e89..6bd7af7cc89ef 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_static_page/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_static_page/index.js @@ -5,7 +5,7 @@ */ import { withProps } from 'recompose'; -import { simplePositioning } from '../integration_utils'; +import { simplePositioning } from '../positioning_utils'; import { StaticWorkpadPage } from './static_workpad_page'; export const StaticPage = () => withProps(simplePositioning)(StaticWorkpadPage); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_static_page/static_workpad_page.js b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_static_page/static_workpad_page.js index 94e16fd97bb8b..9e8962755e00b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_static_page/static_workpad_page.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_page/workpad_static_page/static_workpad_page.js @@ -7,7 +7,7 @@ import React, { PureComponent } from 'react'; import { ElementWrapper } from '../../element_wrapper'; import { staticWorkpadPagePropTypes } from '../prop_types'; -import { isGroupId } from '../integration_utils'; +import { isGroupId } from '../positioning_utils'; export class StaticWorkpadPage extends PureComponent { static propTypes = staticWorkpadPagePropTypes; diff --git a/x-pack/legacy/plugins/canvas/public/register_feature.js b/x-pack/legacy/plugins/canvas/public/register_feature.js index 2c9837ffeb285..8d78498de34b2 100644 --- a/x-pack/legacy/plugins/canvas/public/register_feature.js +++ b/x-pack/legacy/plugins/canvas/public/register_feature.js @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory, } from 'ui/registry/feature_catalogue'; +import { getAppDescription } from '../i18n'; + FeatureCatalogueRegistryProvider.register(() => { return { id: 'canvas', title: 'Canvas', - description: i18n.translate('xpack.canvas.appDescription', { - defaultMessage: 'Showcase your data in a pixel-perfect way.', - }), + description: getAppDescription(), icon: 'canvasApp', path: '/app/canvas', showOnHomePage: true, diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/pages.js b/x-pack/legacy/plugins/canvas/public/state/reducers/pages.js index 224d0a3c03795..50a28475ef5bc 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/pages.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/pages.js @@ -12,7 +12,7 @@ import { routerProvider } from '../../lib/router_provider'; import { getDefaultPage } from '../defaults'; import * as actions from '../actions/pages'; import { getSelectedPageIndex } from '../selectors/workpad'; -import { isGroupId } from '../../components/workpad_page/integration_utils'; +import { isGroupId } from '../../components/workpad_page/positioning_utils'; const { set, del, insert } = immutable; diff --git a/x-pack/legacy/plugins/canvas/public/style/index.scss b/x-pack/legacy/plugins/canvas/public/style/index.scss index 0f59142b10a55..7c3f350aa1f11 100644 --- a/x-pack/legacy/plugins/canvas/public/style/index.scss +++ b/x-pack/legacy/plugins/canvas/public/style/index.scss @@ -43,7 +43,6 @@ @import '../components/palette_picker/palette_picker'; @import '../components/palette_swatch/palette_swatch'; @import '../components/positionable/positionable'; -@import '../components/remove_icon/remove_icon'; @import '../components/rotation_handle/rotation_handle'; @import '../components/shape_preview/shape_preview'; @import '../components/shape_picker/shape_picker'; diff --git a/x-pack/legacy/plugins/canvas/scripts/jest.js b/x-pack/legacy/plugins/canvas/scripts/jest.js index 0aea9fdda9c77..12a2b9921455b 100644 --- a/x-pack/legacy/plugins/canvas/scripts/jest.js +++ b/x-pack/legacy/plugins/canvas/scripts/jest.js @@ -12,19 +12,43 @@ const { runXPackScript } = require('./_helpers'); // we're making this script allow run( ({ log, flags }) => { - const { all, storybook, update } = flags; + const { all, storybook, update, coverage } = flags; let { path } = flags; let options = []; + process.argv.splice(2, process.argv.length - 2); + + if (path) { + log.info(`Limiting tests to ${path}...`); + path = 'legacy/plugins/canvas/' + path; + } else { + path = 'legacy/plugins/canvas'; + } - if (!path) { + if (coverage) { + log.info(`Collecting test coverage and writing to canvas/coverage...`); + options = [ + '--coverage', + '--collectCoverageFrom', // Ignore TS definition files + `!${path}/**/*.d.ts`, + '--collectCoverageFrom', // Ignore build directories + `!${path}/**/build/**`, + '--collectCoverageFrom', // Ignore coverage on test files + `!${path}/**/__tests__/**/*`, + '--collectCoverageFrom', // Include JS files + `${path}/**/*.js`, + '--collectCoverageFrom', // Include TS/X files + `${path}/**/*.ts*`, + '--coverageDirectory', // Output to canvas/coverage + 'legacy/plugins/canvas/coverage', + ]; + } else { // Mitigation for https://github.com/facebook/jest/issues/7267 if (all || storybook || update) { - options = ['--no-cache', '--no-watchman']; + options = options.concat(['--no-cache', '--no-watchman']); } if (all) { log.info('Running all available tests. This will take a while...'); - path = 'legacy/plugins/canvas'; } else if (storybook || update) { path = 'legacy/plugins/canvas/.storybook'; @@ -36,13 +60,9 @@ run( } } else { log.info('Running tests. This does not include Storybook Snapshots...'); - path = 'legacy/plugins/canvas'; } - } else { - log.info(`Running tests found at ${path}...`); } - process.argv.splice(2, process.argv.length - 2); runXPackScript('jest', [path].concat(options)); }, { @@ -50,13 +70,14 @@ run( Jest test runner for Canvas. By default, will not include Storybook Snapshots. `, flags: { - boolean: ['all', 'storybook', 'update'], + boolean: ['all', 'storybook', 'update', 'coverage'], string: ['path'], help: ` --all Runs all tests and snapshots. Slower. --storybook Runs Storybook Snapshot tests only. --update Updates Storybook Snapshot tests. --path Runs any tests at a given path. + --coverage Collect coverage statistics. `, }, } diff --git a/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts b/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts index 9e49d07969dde..8de813255a230 100644 --- a/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts +++ b/x-pack/legacy/plugins/canvas/server/lib/build_embeddable_filters.ts @@ -5,7 +5,7 @@ */ import { buildQueryFilter, Filter as ESFilterType } from '@kbn/es-query'; -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { Filter } from '../../types'; // @ts-ignore Untyped Local import { buildBoolArray } from './build_bool_array'; diff --git a/x-pack/legacy/plugins/canvas/server/lib/normalize_type.js b/x-pack/legacy/plugins/canvas/server/lib/normalize_type.js index b1dd880334308..07f1cac0f7819 100644 --- a/x-pack/legacy/plugins/canvas/server/lib/normalize_type.js +++ b/x-pack/legacy/plugins/canvas/server/lib/normalize_type.js @@ -6,7 +6,7 @@ export function normalizeType(type) { const normalTypes = { - string: ['string', 'text', 'keyword', '_type', '_id', '_index'], + string: ['string', 'text', 'keyword', '_type', '_id', '_index', 'geo_point'], number: [ 'float', 'half_float', diff --git a/x-pack/legacy/plugins/canvas/server/sample_data/load_sample_data.js b/x-pack/legacy/plugins/canvas/server/sample_data/load_sample_data.ts similarity index 86% rename from x-pack/legacy/plugins/canvas/server/sample_data/load_sample_data.js rename to x-pack/legacy/plugins/canvas/server/sample_data/load_sample_data.ts index e189db6146ba8..22dfc65bf924f 100644 --- a/x-pack/legacy/plugins/canvas/server/sample_data/load_sample_data.js +++ b/x-pack/legacy/plugins/canvas/server/sample_data/load_sample_data.ts @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; +import { CANVAS as label } from '../../i18n'; +// @ts-ignore Untyped local import { ecommerceSavedObjects, flightsSavedObjects, webLogsSavedObjects } from './index'; +// @ts-ignore: Untyped in Kibana export function loadSampleData(server) { const now = new Date(); const nowTimestamp = now.toISOString(); + + // @ts-ignore: Untyped local function updateCanvasWorkpadTimestamps(savedObjects) { + // @ts-ignore: Untyped local return savedObjects.map(savedObject => { if (savedObject.type === 'canvas-workpad') { savedObject.attributes['@timestamp'] = nowTimestamp; @@ -21,17 +26,14 @@ export function loadSampleData(server) { }); } - const sampleDataLinkLabel = i18n.translate('xpack.canvas.sampleDataLinkLabel', { - defaultMessage: 'Canvas', - }); server.addSavedObjectsToSampleDataset( 'ecommerce', updateCanvasWorkpadTimestamps(ecommerceSavedObjects) ); server.addAppLinksToSampleDataset('ecommerce', { path: '/app/canvas#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e', - label: sampleDataLinkLabel, icon: 'canvasApp', + label, }); server.addSavedObjectsToSampleDataset( @@ -40,14 +42,14 @@ export function loadSampleData(server) { ); server.addAppLinksToSampleDataset('flights', { path: '/app/canvas#/workpad/workpad-a474e74b-aedc-47c3-894a-db77e62c41e0', - label: sampleDataLinkLabel, icon: 'canvasApp', + label, }); server.addSavedObjectsToSampleDataset('logs', updateCanvasWorkpadTimestamps(webLogsSavedObjects)); server.addAppLinksToSampleDataset('logs', { path: '/app/canvas#/workpad/workpad-5563cc40-5760-4afe-bf33-9da72fac53b7', - label: sampleDataLinkLabel, icon: 'canvasApp', + label, }); } diff --git a/x-pack/legacy/plugins/canvas/types/canvas.ts b/x-pack/legacy/plugins/canvas/types/canvas.ts index 97f8917d50725..0843e5e99dc4e 100644 --- a/x-pack/legacy/plugins/canvas/types/canvas.ts +++ b/x-pack/legacy/plugins/canvas/types/canvas.ts @@ -6,6 +6,13 @@ import { ElementPosition } from './elements'; +export interface CanvasAsset { + '@created': string; + id: string; + type: 'dataurl'; + value: string; +} + export interface CanvasElement { id: string; position: ElementPosition; @@ -25,13 +32,16 @@ export interface CanvasPage { } export interface CanvasWorkpad { - name: string; - id: string; - width: number; - height: number; + '@created': string; + '@timestamp': string; + assets: { [id: string]: CanvasAsset }; + colors: string[]; css: string; + height: number; + id: string; + isWriteable: boolean; + name: string; page: number; pages: CanvasPage[]; - colors: string[]; - isWriteable: boolean; + width: number; } diff --git a/x-pack/legacy/plugins/canvas/types/elements.ts b/x-pack/legacy/plugins/canvas/types/elements.ts index 48047c327a6e9..ce9db8202072f 100644 --- a/x-pack/legacy/plugins/canvas/types/elements.ts +++ b/x-pack/legacy/plugins/canvas/types/elements.ts @@ -95,4 +95,8 @@ export interface PositionedElement { * AST of the Canvas expression for the element */ ast: ExpressionAST; + /** + * Applied filter + */ + filter: string; } diff --git a/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_node_adapter.ts b/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_node_adapter.ts index d1b0e53ec5a12..6d70c8386c31d 100644 --- a/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_node_adapter.ts +++ b/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_node_adapter.ts @@ -40,8 +40,8 @@ import { ClusterNodeEndpoint } from './cluster_node_endpoint'; * - serve request locally if the requested resource is on the local node, otherwise reject it */ export class ClusterNodeAdapter implements ServiceHandlerAdapter { - private readonly clusterService: ClusterService; - private readonly clusterMembershipService: ClusterMembershipService; + readonly clusterService: ClusterService; + readonly clusterMembershipService: ClusterMembershipService; private readonly schedulerService: ResourceSchedulerService; private readonly handlers: Map = new Map(); // used to forward requests diff --git a/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_resource_locator.ts b/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_resource_locator.ts index 75dc7898e7207..27f5c57214112 100644 --- a/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_resource_locator.ts +++ b/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_resource_locator.ts @@ -18,6 +18,7 @@ export class ClusterResourceLocator implements ResourceLocator { constructor( private readonly clusterService: ClusterService, private readonly clusterMembershipService: ClusterMembershipService, + // @ts-ignore private readonly schedulerService: ResourceSchedulerService ) {} @@ -54,12 +55,12 @@ export class ClusterResourceLocator implements ResourceLocator { ); } + /** + * Return undefined to let NodeRepositoriesService enqueue the clone job in cluster mode. + */ async allocate(req: Request, resource: string): Promise { // make the cluster service synchronize the meta data and allocate new resources to nodes await this.clusterService.pollClusterState(); - // allocate the repository to nodes - await this.schedulerService.allocateUnassigned(); - // the resource should be assigned to a node for now, if possible - return this.locate(req, resource); + return undefined; } } diff --git a/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_service.ts b/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_service.ts index 578e503021472..a5ae0a27e128b 100644 --- a/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_service.ts +++ b/x-pack/legacy/plugins/code/server/distributed/cluster/cluster_service.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import util from 'util'; import { ClusterMetadata } from './cluster_meta'; import { EsClient } from '../../lib/esqueue'; import { RepositoryObjectClient } from '../../search'; @@ -79,7 +80,11 @@ export class ClusterService { private async callClusterStateListeners(event: ClusterStateEvent) { for (const applier of this.clusterStateListeners) { - await applier.onClusterStateChanged(event); + try { + await applier.onClusterStateChanged(event); + } catch (e) { + this.logger.error(`Failed to apply cluster state ${util.inspect(event)}`); + } } } diff --git a/x-pack/legacy/plugins/code/server/distributed/cluster/node_repositories_service.test.ts b/x-pack/legacy/plugins/code/server/distributed/cluster/node_repositories_service.test.ts new file mode 100644 index 0000000000000..17893e0f6b1bc --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/cluster/node_repositories_service.test.ts @@ -0,0 +1,122 @@ +/* + * 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 sinon from 'sinon'; +import { Logger } from '../../log'; +import { ConsoleLoggerFactory } from '../../utils/console_logger_factory'; +import { NodeRepositoriesService } from './node_repositories_service'; +import { ClusterService } from './cluster_service'; +import { ClusterMembershipService } from './cluster_membership_service'; +import { CodeNode, CodeNodes } from './code_nodes'; +import { emptyAsyncFunc } from '../../test_utils'; +import { CloneWorker } from '../../queue'; +import { ClusterStateEvent } from './cluster_state_event'; +import { ClusterState } from './cluster_state'; +import { ClusterMetadata } from './cluster_meta'; +import { Repository } from '../../../model'; +import { ResourceAssignment, RoutingTable } from './routing_table'; + +const log: Logger = new ConsoleLoggerFactory().getLogger(['test']); + +afterEach(() => { + sinon.restore(); +}); + +const cloneWorker = ({ + enqueueJob: emptyAsyncFunc, +} as any) as CloneWorker; + +const clusterService = {} as ClusterService; + +const testNodes = [ + { id: 'node1', address: 'http://node1' } as CodeNode, + { id: 'node2', address: 'http://node2' } as CodeNode, +]; + +const testRepos = [ + { uri: 'test1', url: 'http://test1' } as Repository, + { uri: 'test2', url: 'http://test2' } as Repository, +]; + +test('Enqueue clone job after new repository is added to the local node', async () => { + const enqueueJobSpy = sinon.spy(cloneWorker, 'enqueueJob'); + + const clusterMembershipService = { + localNode: testNodes[0], + } as ClusterMembershipService; + + const nodeService = new NodeRepositoriesService( + log, + clusterService, + clusterMembershipService, + cloneWorker + ); + + // event with no new repositories + let event = new ClusterStateEvent(ClusterState.empty(), ClusterState.empty()); + await nodeService.onClusterStateChanged(event); + expect(enqueueJobSpy.called).toBeFalsy(); + expect(nodeService.localRepos.size).toBe(0); + + // event with a new repository + event = new ClusterStateEvent( + new ClusterState( + new ClusterMetadata([testRepos[0]]), + new RoutingTable([ + { nodeId: testNodes[0].id, resource: testRepos[0].uri } as ResourceAssignment, + ]), + new CodeNodes([testNodes[0]]) + ), + event.current + ); + await nodeService.onClusterStateChanged(event); + expect(enqueueJobSpy.calledOnce).toBeTruthy(); + expect(nodeService.localRepos.size).toBe(1); + + // event with removed repository + event = new ClusterStateEvent(ClusterState.empty(), event.current); + await nodeService.onClusterStateChanged(event); + expect(enqueueJobSpy.calledOnce).toBeTruthy(); + expect(nodeService.localRepos.size).toBe(0); + + // event with two added repositories + event = new ClusterStateEvent( + new ClusterState( + new ClusterMetadata([testRepos[0], testRepos[1]]), + new RoutingTable([ + { nodeId: testNodes[0].id, resource: testRepos[0].uri } as ResourceAssignment, + { nodeId: testNodes[0].id, resource: testRepos[1].uri } as ResourceAssignment, + ]), + new CodeNodes([testNodes[0]]) + ), + event.current + ); + await nodeService.onClusterStateChanged(event); + expect(enqueueJobSpy.callCount).toBe(3); + expect(nodeService.localRepos.size).toBe(2); + + // event with removed repository + event = new ClusterStateEvent(ClusterState.empty(), event.current); + await nodeService.onClusterStateChanged(event); + expect(enqueueJobSpy.callCount).toBe(3); + expect(nodeService.localRepos.size).toBe(0); + + // event with two added repositories, one for the other node + event = new ClusterStateEvent( + new ClusterState( + new ClusterMetadata([testRepos[0], testRepos[1]]), + new RoutingTable([ + { nodeId: testNodes[0].id, resource: testRepos[0].uri } as ResourceAssignment, + { nodeId: testNodes[1].id, resource: testRepos[1].uri } as ResourceAssignment, + ]), + new CodeNodes([testNodes[0]]) + ), + event.current + ); + await nodeService.onClusterStateChanged(event); + expect(enqueueJobSpy.callCount).toBe(4); + expect(nodeService.localRepos.size).toBe(1); +}); diff --git a/x-pack/legacy/plugins/code/server/distributed/cluster/node_repositories_service.ts b/x-pack/legacy/plugins/code/server/distributed/cluster/node_repositories_service.ts new file mode 100644 index 0000000000000..42d68b6f692f9 --- /dev/null +++ b/x-pack/legacy/plugins/code/server/distributed/cluster/node_repositories_service.ts @@ -0,0 +1,71 @@ +/* + * 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 util from 'util'; +import { ClusterService, ClusterStateListener } from './cluster_service'; +import { ClusterStateEvent } from './cluster_state_event'; +import { ClusterMembershipService } from './cluster_membership_service'; +import { CloneWorker } from '../../queue'; +import { Repository, RepositoryUri } from '../../../model'; +import { Logger } from '../../log'; +import { RepoState } from '../../../public/actions'; + +export class NodeRepositoriesService implements ClusterStateListener { + // visible for test + readonly localRepos = new Map(); + private readonly localNodeId = this.clusterMembershipService.localNode.id; + + constructor( + private readonly log: Logger, + private readonly clusterService: ClusterService, + private readonly clusterMembershipService: ClusterMembershipService, + private readonly cloneWorker: CloneWorker + ) {} + + public async start() { + /** + * we can add locally exists repositories to localRepos when the service is started to avoid unnecessarily add clone + * tasks for them, but for now it's OK because clone job is idempotent. + */ + this.clusterService.addClusterStateListener(this); + } + + public async stop() {} + + async onClusterStateChanged(event: ClusterStateEvent): Promise { + // compare repositories in the cluster state with repositories in the local node, and remove + const repos = event.current.getNodeRepositories(this.clusterMembershipService.localNode.id); + const localNewRepos = repos.filter(repo => !this.localRepos.has(repo.uri)); + const localRemovedRepos = Array.from(this.localRepos.values()).filter( + repo => + event.current.routingTable.getNodeIdByRepositoryURI(repo.metadata.uri) !== this.localNodeId + ); + for (const localNewRepo of localNewRepos) { + this.log.info( + `Repository added to node [${this.localNodeId}]: ${util.inspect(localNewRepo)}` + ); + await this.cloneWorker.enqueueJob({ url: localNewRepo.url }, {}); + this.localRepos.set(localNewRepo.uri, { + metadata: localNewRepo, + currentState: RepoState.CLONING, + }); + } + // TODO remove the stale local repo after the Kibana HA is ready + for (const localRemovedRepo of localRemovedRepos) { + this.log.info( + `Repository removed from node [${this.localNodeId}]: ${util.inspect( + localRemovedRepo.metadata + )}` + ); + this.localRepos.delete(localRemovedRepo.metadata.uri); + } + } +} + +interface LocalRepository { + metadata: Repository; + currentState: RepoState; +} diff --git a/x-pack/legacy/plugins/code/server/init_workers.ts b/x-pack/legacy/plugins/code/server/init_workers.ts index b22fce8157e37..c4385cd711c5c 100644 --- a/x-pack/legacy/plugins/code/server/init_workers.ts +++ b/x-pack/legacy/plugins/code/server/init_workers.ts @@ -92,13 +92,16 @@ export function initWorkers( ); // Initialize schedulers. - const cloneScheduler = new CloneScheduler(cloneWorker, serverOptions, esClient, log); const updateScheduler = new UpdateScheduler(updateWorker, serverOptions, esClient, log); const indexScheduler = new IndexScheduler(indexWorker, serverOptions, esClient, log); updateScheduler.start(); indexScheduler.start(); // Check if the repository is local on the file system. // This should be executed once at the startup time of Kibana. - cloneScheduler.schedule(); - return { indexScheduler, updateScheduler }; + // Ignored in cluster mode, leave it to the node level control loop + if (!serverOptions.clusterEnabled) { + const cloneScheduler = new CloneScheduler(cloneWorker, serverOptions, esClient, log); + cloneScheduler.schedule(); + } + return { indexScheduler, updateScheduler, cloneWorker, deleteWorker, indexWorker, updateWorker }; } diff --git a/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts b/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts index 016afdc18eb49..c8b707f9b5fb9 100644 --- a/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts +++ b/x-pack/legacy/plugins/code/server/lsp/go_launcher.ts @@ -77,9 +77,12 @@ export class GoServerLauncher extends AbstractLauncher { } async spawnProcess(installationPath: string, port: number, log: Logger) { - const launchersFound = glob.sync('go-langserver', { - cwd: installationPath, - }); + const launchersFound = glob.sync( + process.platform === 'win32' ? 'go-langserver.exe' : 'go-langserver', + { + cwd: installationPath, + } + ); if (!launchersFound.length) { throw new Error('Cannot find executable go language server'); } @@ -92,12 +95,13 @@ export class GoServerLauncher extends AbstractLauncher { // Construct $GOROOT from the bundled go toolchain. const goRoot = goToolchain; const goHome = path.resolve(goToolchain, 'bin'); - envPath = envPath + ':' + goHome; + envPath = process.platform === 'win32' ? envPath + ';' + goHome : envPath + ':' + goHome; // Construct $GOPATH under 'kibana/data/code'. const goPath = this.options.goPath; if (!fs.existsSync(goPath)) { fs.mkdirSync(goPath); } + const goCache = path.resolve(goPath, '.cache'); const params: string[] = ['-port=' + port.toString()]; const golsp = path.resolve(installationPath, launchersFound[0]); const p = spawn(golsp, params, { @@ -109,6 +113,7 @@ export class GoServerLauncher extends AbstractLauncher { CLIENT_PORT: port.toString(), GOROOT: goRoot, GOPATH: goPath, + GOCACHE: goCache, PATH: envPath, CGO_ENABLED: '0', }, diff --git a/x-pack/legacy/plugins/code/server/plugin.ts b/x-pack/legacy/plugins/code/server/plugin.ts index 89b46c23b4320..87680837cb864 100644 --- a/x-pack/legacy/plugins/code/server/plugin.ts +++ b/x-pack/legacy/plugins/code/server/plugin.ts @@ -54,6 +54,7 @@ import { initLocalService } from './init_local'; import { initQueue } from './init_queue'; import { initWorkers } from './init_workers'; import { ClusterNodeAdapter } from './distributed/cluster/cluster_node_adapter'; +import { NodeRepositoriesService } from './distributed/cluster/node_repositories_service'; export class CodePlugin { private isCodeNode = false; @@ -66,6 +67,7 @@ export class CodePlugin { private updateScheduler: UpdateScheduler | null = null; private lspService: LspService | null = null; private codeServices: CodeServices | null = null; + private nodeService: NodeRepositoriesService | null = null; constructor(initializerContext: PluginInitializerContext) { this.log = {} as Logger; @@ -153,10 +155,15 @@ export class CodePlugin { server, this.log ); - const codeServices = new CodeServices( - new ClusterNodeAdapter(codeServerRouter, this.log, this.serverOptions, esClient) + const clusterNodeAdapter = new ClusterNodeAdapter( + codeServerRouter, + this.log, + this.serverOptions, + esClient ); + const codeServices = new CodeServices(clusterNodeAdapter); + this.queue = initQueue(server, this.log, esClient); const { gitOps, lspService } = initLocalService( @@ -169,7 +176,7 @@ export class CodePlugin { ); this.lspService = lspService; this.gitOps = gitOps; - const { indexScheduler, updateScheduler } = initWorkers( + const { indexScheduler, updateScheduler, cloneWorker } = initWorkers( server, this.log, esClient, @@ -182,6 +189,14 @@ export class CodePlugin { this.indexScheduler = indexScheduler; this.updateScheduler = updateScheduler; + this.nodeService = new NodeRepositoriesService( + this.log, + clusterNodeAdapter.clusterService, + clusterNodeAdapter.clusterMembershipService, + cloneWorker + ); + await this.nodeService.start(); + // Execute index version checking and try to migrate index data if necessary. await tryMigrateIndices(esClient, this.log); @@ -240,6 +255,9 @@ export class CodePlugin { if (this.codeServices) { await this.codeServices.stop(); } + if (this.nodeService) { + await this.nodeService.stop(); + } } private async initNonCodeNode(url: string, core: CoreSetup) { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js index 47b12ae5552e4..f0ae17c6bb8d4 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js @@ -7,7 +7,6 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { Route, Switch, Redirect } from 'react-router-dom'; -import chrome from 'ui/chrome'; import { fatalError } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -23,9 +22,8 @@ import { } from '@elastic/eui'; import { BASE_PATH } from '../../common/constants'; -import { SectionUnauthorized, SectionError } from './components'; +import { SectionError } from './components'; import routing from './services/routing'; -import { isAvailable, isActive, getReason } from './services/license'; import { loadPermissions } from './services/api'; import { @@ -113,31 +111,6 @@ export class App extends Component { missingClusterPrivileges, } = this.state; - if (!isAvailable() || !isActive()) { - return ( - - )} - > - {getReason()} - {' '} - - - - - ); - } - if (isFetchingPermissions) { return ( diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/license.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/license.js deleted file mode 100644 index c61a363472149..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/license.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export let isAvailable; -export let isActive; -export let getReason; - -export function setLicense(isAvailableCallback, isActiveCallback, getReasonCallback) { - isAvailable = isAvailableCallback; - isActive = isActiveCallback; - getReason = getReasonCallback; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js b/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js index 54c26308c8e4b..15017f7922a89 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js @@ -15,9 +15,13 @@ import template from './main.html'; import { BASE_PATH } from '../common/constants'; import { renderReact } from './app'; import { setHttpClient } from './app/services/api'; -import { setLicense } from './app/services/license'; -if (chrome.getInjected('ccrUiEnabled')) { +const isAvailable = xpackInfo.get('features.crossClusterReplication.isAvailable'); +const isActive = xpackInfo.get('features.crossClusterReplication.isActive'); +const isLicenseOK = isAvailable && isActive; +const isCcrUiEnabled = chrome.getInjected('ccrUiEnabled'); + +if (isLicenseOK && isCcrUiEnabled) { const esSection = management.getSection('elasticsearch'); esSection.register('ccr', { @@ -35,21 +39,9 @@ if (chrome.getInjected('ccrUiEnabled')) { routes.when(`${BASE_PATH}/:section?/:subsection?/:view?/:id?`, { template, - resolve: { - license() { - return { - isAvailable: () => xpackInfo.get('features.crossClusterReplication.isAvailable'), - isActive: () => xpackInfo.get('features.crossClusterReplication.isActive'), - getReason: () => xpackInfo.get('features.crossClusterReplication.message'), - }; - } - }, controllerAs: 'ccr', controller: class CrossClusterReplicationController { constructor($scope, $route, $http, $q) { - const { license: { isAvailable, isActive, getReason } } = $route.current.locals; - setLicense(isAvailable, isActive, getReason); - // React-router's does not play well with the angular router. It will cause this controller // to re-execute without the $destroy handler being called. This means that the app will be mounted twice // creating a memory leak when leaving (only 1 app will be unmounted). diff --git a/x-pack/legacy/plugins/graph/public/angular/directives/graph_save.js b/x-pack/legacy/plugins/graph/public/angular/directives/graph_save.js deleted file mode 100644 index e8a2889feda24..0000000000000 --- a/x-pack/legacy/plugins/graph/public/angular/directives/graph_save.js +++ /dev/null @@ -1,17 +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 { uiModules } from 'ui/modules'; -import template from '../templates/save_workspace.html'; -const app = uiModules.get('app/graph'); - -app.directive('graphSave', function () { - return { - replace: true, - restrict: 'E', - template, - }; -}); diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/index.html b/x-pack/legacy/plugins/graph/public/angular/templates/index.html index 4dfc1846bb259..5aaa944d45b27 100644 --- a/x-pack/legacy/plugins/graph/public/angular/templates/index.html +++ b/x-pack/legacy/plugins/graph/public/angular/templates/index.html @@ -3,8 +3,7 @@ -
- +
diff --git a/x-pack/legacy/plugins/graph/public/angular/templates/save_workspace.html b/x-pack/legacy/plugins/graph/public/angular/templates/save_workspace.html deleted file mode 100644 index 76c492397b42c..0000000000000 --- a/x-pack/legacy/plugins/graph/public/angular/templates/save_workspace.html +++ /dev/null @@ -1,62 +0,0 @@ -
-
-
- - -
- -
- - -
-
- - {{ ::'xpack.graph.topNavMenu.save.saveConfigurationOnlyWarning' | i18n: { - defaultMessage: 'The data in this workspace will be cleared and only the configuration will be saved', - } }} -
-
- -
- - - -
- - - {{ ::'xpack.graph.topNavMenu.save.saveConfigurationOnlyText' | i18n: { - defaultMessage: 'The data in this workspace will be cleared and only the configuration will be saved', - } }} - -
- -
diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index 910bdd13c03c4..4dc7faaff4771 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -56,12 +56,11 @@ import { import { getOutlinkEncoders, } from './angular/services/outlink_encoders'; -import { getEditUrl, getNewPath, getEditPath, setBreadcrumbs } from './services/url'; +import { getEditUrl, getNewPath, getEditPath, setBreadcrumbs, getHomePath } from './services/url'; +import { save } from './services/save'; -import saveTemplate from './angular/templates/save_workspace.html'; import settingsTemplate from './angular/templates/settings.html'; -import './angular/directives/graph_save'; import './angular/directives/graph_settings'; const app = uiModules.get('app/graph'); @@ -207,6 +206,21 @@ app.controller('graphuiPlugin', function ( }); } + function updateBreadcrumbs() { + setBreadcrumbs({ + chrome, + savedWorkspace: $route.current.locals.savedWorkspace, + navigateTo: () => { + // TODO this should be wrapped into canWipeWorkspace, + // but the check is too simple right now. Change this + // once actual state-diffing is in place. + $scope.$evalAsync(() => { + kbnUrl.changePath(getHomePath()); + }); + } + }); + } + $scope.title = 'Graph'; $scope.spymode = 'request'; @@ -655,7 +669,6 @@ app.controller('graphuiPlugin', function ( $scope.resetWorkspace = function () { $scope.clearWorkspace(); - $scope.userHasConfirmedSaveWorkspaceData = false; $scope.selectedIndex = null; $scope.proposedIndex = null; $scope.detail = null; @@ -862,7 +875,7 @@ app.controller('graphuiPlugin', function ( defaultMessage: 'Save', }), description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save Workspace', + defaultMessage: 'Save workspace', }), tooltip: () => { if ($scope.allSavingDisabled) { @@ -879,10 +892,11 @@ app.controller('graphuiPlugin', function ( return $scope.allSavingDisabled || $scope.selectedFields.length === 0; }, run: () => { - $scope.$evalAsync(() => { - const curState = $scope.menus.showSave; - $scope.closeMenus(); - $scope.menus.showSave = !curState; + save({ + savePolicy: $scope.graphSavePolicy, + hasData: $scope.workspace && ($scope.workspace.nodes.length > 0 || $scope.workspace.blacklistedNodes.length > 0), + workspace: $scope.savedWorkspace, + saveWorkspace: $scope.saveWorkspace }); }, testId: 'graphSaveButton', @@ -906,21 +920,9 @@ app.controller('graphuiPlugin', function ( }, }); - setBreadcrumbs({ - chrome, - savedWorkspace: $route.current.locals.savedWorkspace, - navigateTo: () => { - // TODO this should be wrapped into canWipeWorkspace, - // but the check is too simple right now. Change this - // once actual state-diffing is in place. - $scope.$evalAsync(() => { - kbnUrl.changePath('/home/'); - }); - } - }); + updateBreadcrumbs(); $scope.menus = { - showSave: false, showSettings: false, }; @@ -1082,7 +1084,7 @@ app.controller('graphuiPlugin', function ( }); } - $scope.saveWorkspace = function () { + $scope.saveWorkspace = function (saveOptions, userHasConfirmedSaveWorkspaceData) { if ($scope.allSavingDisabled) { // It should not be possible to navigate to this function if allSavingDisabled is set // but adding check here as a safeguard. @@ -1093,7 +1095,7 @@ app.controller('graphuiPlugin', function ( } initWorkspaceIfRequired(); const canSaveData = $scope.graphSavePolicy === 'configAndData' || - ($scope.graphSavePolicy === 'configAndDataWithConsent' && $scope.userHasConfirmedSaveWorkspaceData); + ($scope.graphSavePolicy === 'configAndDataWithConsent' && userHasConfirmedSaveWorkspaceData); let blacklist = []; @@ -1169,12 +1171,8 @@ app.controller('graphuiPlugin', function ( }); $scope.savedWorkspace.numVertices = vertices.length; $scope.savedWorkspace.numLinks = links.length; - $scope.savedWorkspace.description = $scope.description; - - $scope.savedWorkspace.save().then(function (id) { - $scope.closeMenus(); - $scope.userHasConfirmedSaveWorkspaceData = false; //reset flag + return $scope.savedWorkspace.save(saveOptions).then(function (id) { if (id) { const title = i18n.translate('xpack.graph.saveWorkspace.successNotificationTitle', { defaultMessage: 'Saved "{workspaceTitle}"', @@ -1192,9 +1190,11 @@ app.controller('graphuiPlugin', function ( text, 'data-test-subj': 'saveGraphSuccess', }); - if ($scope.savedWorkspace.id === $route.current.params.id) return; - kbnUrl.change(getEditPath($scope.savedWorkspace)); + if ($scope.savedWorkspace.id !== $route.current.params.id) { + kbnUrl.change(getEditPath($scope.savedWorkspace)); + } } + return { id }; }, fatalError); }; diff --git a/x-pack/legacy/plugins/graph/public/components/graph_save_modal.tsx b/x-pack/legacy/plugins/graph/public/components/graph_save_modal.tsx new file mode 100644 index 0000000000000..654cf473fb03e --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/components/graph_save_modal.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { EuiFormRow, EuiTextArea, EuiCallOut, EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + SavedObjectSaveModal, + OnSaveProps, +} from '../../../../../../src/plugins/kibana_react/public'; + +import { GraphSavePolicy } from '../types/config'; + +export interface OnSaveGraphProps extends OnSaveProps { + newDescription: string; + dataConsent: boolean; +} + +export function GraphSaveModal({ + onSave, + onClose, + title, + description, + showCopyOnSave, + savePolicy, + hasData, +}: { + onSave: (props: OnSaveGraphProps) => void; + onClose: () => void; + title: string; + description: string; + showCopyOnSave: boolean; + savePolicy: GraphSavePolicy; + hasData: boolean; +}) { + const [newDescription, setDescription] = useState(description); + const [dataConsent, setDataConsent] = useState(false); + return ( + { + onSave({ ...props, newDescription, dataConsent }); + }} + onClose={onClose} + title={title} + showCopyOnSave={showCopyOnSave} + objectType={i18n.translate('xpack.graph.topNavMenu.save.objectType', { + defaultMessage: 'workspace', + })} + options={ + <> + + { + setDescription(e.target.value); + }} + fullWidth + rows={5} + /> + + {savePolicy === 'configAndDataWithConsent' && hasData && ( + + { + setDataConsent(e.target.checked); + }} + /> + + )} + {savePolicy === 'config' && hasData && ( + <> + +

+ {i18n.translate('xpack.graph.topNavMenu.save.saveConfigurationOnlyText', { + defaultMessage: + 'The data in this workspace will be cleared and only the configuration will be saved.', + })} +

+
+ + + )} + + } + /> + ); +} diff --git a/x-pack/legacy/plugins/graph/public/services/save.tsx b/x-pack/legacy/plugins/graph/public/services/save.tsx new file mode 100644 index 0000000000000..4903fb4913a3f --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/services/save.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; +import { SavedGraphWorkspace } from '../types/persistence'; +import { GraphSaveModal, OnSaveGraphProps } from '../components/graph_save_modal'; +import { GraphSavePolicy } from '../types/config'; + +export function save({ + savePolicy, + hasData, + workspace, + saveWorkspace, +}: { + savePolicy: GraphSavePolicy; + hasData: boolean; + workspace: SavedGraphWorkspace; + saveWorkspace: ( + saveOptions: { + confirmOverwrite: boolean; + isTitleDuplicateConfirmed: boolean; + onTitleDuplicate: () => void; + }, + dataConsent: boolean + ) => Promise<{ id?: string } | { error: string }>; +}) { + const currentTitle = workspace.title; + const currentDescription = workspace.description; + const onSave = ({ + newTitle, + newDescription, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + dataConsent, + }: OnSaveGraphProps) => { + workspace.title = newTitle; + workspace.description = newDescription; + workspace.copyOnSave = newCopyOnSave; + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + return saveWorkspace(saveOptions, dataConsent).then(response => { + // If the save wasn't successful, put the original values back. + if (!('id' in response) || !Boolean(response.id)) { + workspace.title = currentTitle; + workspace.description = currentDescription; + } + return response; + }); + }; + showSaveModal( + {}} + title={workspace.title} + description={workspace.description} + showCopyOnSave={Boolean(workspace.id)} + /> + ); +} diff --git a/x-pack/legacy/plugins/canvas/public/components/remove_icon/index.js b/x-pack/legacy/plugins/graph/public/types/config.ts similarity index 64% rename from x-pack/legacy/plugins/canvas/public/components/remove_icon/index.js rename to x-pack/legacy/plugins/graph/public/types/config.ts index 0c4f97372670d..eaff11d9d4548 100644 --- a/x-pack/legacy/plugins/canvas/public/components/remove_icon/index.js +++ b/x-pack/legacy/plugins/graph/public/types/config.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pure } from 'recompose'; -import { RemoveIcon as Component } from './remove_icon'; - -export const RemoveIcon = pure(Component); +export type GraphSavePolicy = 'configAndDataWithConsent' | 'configAndData' | 'config' | 'none'; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index 9a946150927f2..92c6ddd193609 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -35,7 +35,7 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ > , this.props.onViewChange(view); - // TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example. + // TODO: Change this to a real implementation using the tickFormatter from the prototype as an example. private formatter = (val: string | number) => { const { metric } = this.props.options; const metricFormatter = get( diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx index 8ae644a497e19..2171103c349e0 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx @@ -4,14 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { useEffect } from 'react'; import * as rt from 'io-ts'; import { useUrlState } from '../../../utils/use_url_state'; -import { timeRangeRT } from '../../../../common/http_api/shared/time_range'; -const autoRefreshRT = rt.union([rt.boolean, rt.undefined]); -const urlTimeRangeRT = rt.union([timeRangeRT, rt.undefined]); +const autoRefreshRT = rt.union([ + rt.type({ + interval: rt.number, + isPaused: rt.boolean, + }), + rt.undefined, +]); + +export const stringTimeRangeRT = rt.type({ + startTime: rt.string, + endTime: rt.string, +}); +export type StringTimeRange = rt.TypeOf; + +const urlTimeRangeRT = rt.union([stringTimeRangeRT, rt.undefined]); const TIME_RANGE_URL_STATE_KEY = 'timeRange'; const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh'; @@ -19,11 +30,8 @@ const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh'; export const useLogAnalysisResultsUrlState = () => { const [timeRange, setTimeRange] = useUrlState({ defaultState: { - startTime: moment - .utc() - .subtract(2, 'weeks') - .valueOf(), - endTime: moment.utc().valueOf(), + startTime: 'now-2w', + endTime: 'now', }, decodeUrlState: (value: unknown) => urlTimeRangeRT.decode(value).getOrElse(undefined), encodeUrlState: urlTimeRangeRT.encode, @@ -34,21 +42,24 @@ export const useLogAnalysisResultsUrlState = () => { setTimeRange(timeRange); }, []); - const [autoRefreshEnabled, setAutoRefresh] = useUrlState({ - defaultState: false, + const [autoRefresh, setAutoRefresh] = useUrlState({ + defaultState: { + isPaused: false, + interval: 30000, + }, decodeUrlState: (value: unknown) => autoRefreshRT.decode(value).getOrElse(undefined), encodeUrlState: autoRefreshRT.encode, urlStateKey: AUTOREFRESH_URL_STATE_KEY, }); useEffect(() => { - setAutoRefresh(autoRefreshEnabled); + setAutoRefresh(autoRefresh); }, []); return { timeRange, setTimeRange, - autoRefreshEnabled, + autoRefresh, setAutoRefresh, }; }; diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index 8e6e61bc44f08..ca92c4086485a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -57,7 +57,7 @@ export const RedirectToHostDetailViaIP = injectI18n( )(''); if (name) { - return ; + return ; } return ( diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx index d9f62f48a8a86..ad51307641780 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -27,7 +27,7 @@ export const RedirectToNodeDetail = ({ getToFromLocation(location) )(''); - return ; + return ; }; export const getNodeDetailUrl = ({ diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx index 71c46230d5d7c..0ec027958cc33 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import datemath from '@elastic/datemath'; import { - EuiSuperDatePicker, + EuiBadge, EuiFlexGroup, EuiFlexItem, EuiPage, @@ -17,31 +14,25 @@ import { EuiPageContent, EuiPageContentBody, EuiPanel, - EuiBadge, + EuiSuperDatePicker, } from '@elastic/eui'; -import dateMath from '@elastic/datemath'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; +import React, { useCallback, useMemo, useState } from 'react'; import euiStyled from '../../../../../../common/eui_styled_components'; -import { useTrackPageview } from '../../../hooks/use_track_metric'; -import { useInterval } from '../../../hooks/use_interval'; -import { useLogAnalysisResults } from '../../../containers/logs/log_analysis'; -import { useLogAnalysisResultsUrlState } from '../../../containers/logs/log_analysis'; +import { TimeRange } from '../../../../common/http_api/shared/time_range'; +import { bucketSpan } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; -import { LogRateResults } from './sections/log_rate'; +import { + StringTimeRange, + useLogAnalysisResults, + useLogAnalysisResultsUrlState, +} from '../../../containers/logs/log_analysis'; +import { useTrackPageview } from '../../../hooks/use_track_metric'; import { FirstUseCallout } from './first_use'; - -const DATE_PICKER_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; - -const getLoadingState = () => { - return ( - - ); -}; +import { LogRateResults } from './sections/log_rate'; export const AnalysisResultsContent = ({ sourceId, @@ -54,26 +45,15 @@ export const AnalysisResultsContent = ({ useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 }); const { - timeRange, - setTimeRange, - autoRefreshEnabled, + timeRange: selectedTimeRange, + setTimeRange: setSelectedTimeRange, + autoRefresh, setAutoRefresh, } = useLogAnalysisResultsUrlState(); - const [refreshInterval, setRefreshInterval] = useState(300000); - - const setTimeRangeToNow = useCallback(() => { - const range = timeRange.endTime - timeRange.startTime; - const nowInMs = moment() - .utc() - .valueOf(); - setTimeRange({ - startTime: nowInMs - range, - endTime: nowInMs, - }); - }, [timeRange.startTime, timeRange.endTime, setTimeRange]); - - useInterval(setTimeRangeToNow, autoRefreshEnabled ? refreshInterval : null); + const [queryTimeRange, setQueryTimeRange] = useState( + stringToNumericTimeRange(selectedTimeRange) + ); const bucketDuration = useMemo(() => { // This function takes the current time range in ms, @@ -83,33 +63,52 @@ export const AnalysisResultsContent = ({ // 900000 (15 minutes) to it, so that we don't end up with // jaggy bucket boundaries between the ML buckets and our // aggregation buckets. - const msRange = timeRange.endTime - timeRange.startTime; + const msRange = moment(queryTimeRange.endTime).diff(moment(queryTimeRange.startTime)); const bucketIntervalInMs = msRange / 200; - const bucketSpan = 900000; // TODO: Pull this from 'common' when setup hook PR is merged const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); const roundedResult = parseInt(Number(result).toFixed(0), 10); return roundedResult < bucketSpan ? bucketSpan : roundedResult; - }, [timeRange]); + }, [queryTimeRange.startTime, queryTimeRange.endTime]); + const { isLoading, logEntryRate } = useLogAnalysisResults({ sourceId, - startTime: timeRange.startTime, - endTime: timeRange.endTime, + startTime: queryTimeRange.startTime, + endTime: queryTimeRange.endTime, bucketDuration, }); const hasResults = useMemo(() => logEntryRate && logEntryRate.histogramBuckets.length > 0, [ logEntryRate, ]); - const handleTimeRangeChange = useCallback( - ({ start, end }: { start: string; end: string }) => { - const parsedStart = dateMath.parse(start); - const parsedEnd = dateMath.parse(end); - setTimeRange({ - startTime: - !parsedStart || !parsedStart.isValid() ? timeRange.startTime : parsedStart.valueOf(), - endTime: !parsedEnd || !parsedEnd.isValid() ? timeRange.endTime : parsedEnd.valueOf(), + + const handleQueryTimeRangeChange = useCallback( + ({ start: startTime, end: endTime }: { start: string; end: string }) => { + setQueryTimeRange(stringToNumericTimeRange({ startTime, endTime })); + }, + [setQueryTimeRange] + ); + + const handleSelectedTimeRangeChange = useCallback( + (selectedTime: { start: string; end: string; isInvalid: boolean }) => { + if (selectedTime.isInvalid) { + return; + } + setSelectedTimeRange({ + startTime: selectedTime.start, + endTime: selectedTime.end, + }); + handleQueryTimeRangeChange(selectedTime); + }, + [setSelectedTimeRange, handleQueryTimeRangeChange] + ); + + const handleAutoRefreshChange = useCallback( + ({ isPaused, refreshInterval: interval }: { isPaused: boolean; refreshInterval: number }) => { + setAutoRefresh({ + isPaused, + interval, }); }, - [setTimeRange, timeRange] + [setAutoRefresh] ); const anomaliesDetected = useMemo(() => { @@ -117,18 +116,10 @@ export const AnalysisResultsContent = ({ return null; } else { if (logEntryRate.histogramBuckets && logEntryRate.histogramBuckets.length) { - return logEntryRate.histogramBuckets.reduce((acc: any, bucket) => { - if (bucket.anomalies.length > 0) { - return ( - acc + - bucket.anomalies.reduce((anomalyAcc: any, anomaly) => { - return anomalyAcc + 1; - }, 0) - ); - } else { - return acc; - } - }, 0); + return logEntryRate.histogramBuckets.reduce( + (acc, bucket) => acc + bucket.anomalies.length, + 0 + ); } else { return null; } @@ -138,7 +129,11 @@ export const AnalysisResultsContent = ({ return ( <> {isLoading && !logEntryRate ? ( - <>{getLoadingState()} + ) : ( <> @@ -148,41 +143,33 @@ export const AnalysisResultsContent = ({ {anomaliesDetected !== null ? ( - <> - - 0 - ) : ( - {anomaliesDetected} - ), - }} - /> - - + + + {anomaliesDetected} + + ), + number: anomaliesDetected, + }} + /> + ) : null} { - if (isPaused) { - setAutoRefresh(false); - } else { - setRefreshInterval(interval); - setAutoRefresh(true); - } - }} + start={selectedTimeRange.startTime} + end={selectedTimeRange.endTime} + onTimeChange={handleSelectedTimeRangeChange} + isPaused={autoRefresh.isPaused} + refreshInterval={autoRefresh.interval} + onRefreshChange={handleAutoRefreshChange} + onRefresh={handleQueryTimeRangeChange} /> @@ -196,7 +183,7 @@ export const AnalysisResultsContent = ({ @@ -208,6 +195,20 @@ export const AnalysisResultsContent = ({ ); }; +const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({ + startTime: moment( + datemath.parse(timeRange.startTime, { + momentInstance: moment, + }) + ).valueOf(), + endTime: moment( + datemath.parse(timeRange.endTime, { + momentInstance: moment, + roundUp: true, + }) + ).valueOf(), +}); + const ExpandingPage = euiStyled(EuiPage)` flex: 1 0 0%; `; diff --git a/x-pack/legacy/plugins/infra/public/routes.tsx b/x-pack/legacy/plugins/infra/public/routes.tsx index dcdb40cb5ed7e..6e5ec8ea560b4 100644 --- a/x-pack/legacy/plugins/infra/public/routes.tsx +++ b/x-pack/legacy/plugins/infra/public/routes.tsx @@ -6,7 +6,7 @@ import { History } from 'history'; import React from 'react'; -import { Redirect, Route, Router, Switch } from 'react-router-dom'; +import { Route, Router, Switch } from 'react-router-dom'; import { UICapabilities } from 'ui/capabilities'; import { injectUICapabilities } from 'ui/capabilities/react'; @@ -15,6 +15,7 @@ import { InfrastructurePage } from './pages/infrastructure'; import { LinkToPage } from './pages/link_to'; import { LogsPage } from './pages/logs'; import { MetricDetail } from './pages/metrics'; +import { RedirectWithQueryParams } from './utils/redirect_with_query_params'; interface RouterProps { history: History; @@ -26,26 +27,39 @@ const PageRouterComponent: React.SFC = ({ history, uiCapabilities } {uiCapabilities.infrastructure.show && ( - + )} {uiCapabilities.infrastructure.show && ( - + )} {uiCapabilities.infrastructure.show && ( - + )} {uiCapabilities.infrastructure.show && ( - + + )} + {uiCapabilities.infrastructure.show && ( + + )} + {uiCapabilities.infrastructure.show && ( + + )} + {uiCapabilities.logs.show && ( + )} - {uiCapabilities.logs.show && } {uiCapabilities.logs.show && } {uiCapabilities.infrastructure.show && ( )} - {uiCapabilities.infrastructure.show && ( - - )} diff --git a/x-pack/legacy/plugins/infra/public/utils/redirect_with_query_params.tsx b/x-pack/legacy/plugins/infra/public/utils/redirect_with_query_params.tsx new file mode 100644 index 0000000000000..915b82860dd1b --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/utils/redirect_with_query_params.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Route, Redirect, RouteProps, RedirectProps } from 'react-router-dom'; + +interface RedirectWithQueryParamsProps extends Omit { + from: string; + to: string | RouteProps['location']; +} + +// This workaround preserves query parameters in the redirect +// https://github.com/ReactTraining/react-router/issues/5818#issuecomment-379212014 +export const RedirectWithQueryParams: React.FunctionComponent = ({ + from, + to, + ...rest +}) => ( + + location ? ( + + ) : null + } + /> +); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss index 9c8af22319918..1e9b21c434290 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss @@ -10,16 +10,13 @@ } .mapLayerPanel__body { - @include euiOverflowShadow; + @include euiScrollBar; padding-bottom: 0; /* 1 */ flex-grow: 1; flex-basis: 1px; /* Fixes scrolling for Firefox */ - overflow-y: hidden; + overflow-y: auto; .mapLayerPanel__bodyOverflow { - @include euiScrollBar; - height: 100%; - overflow-y: auto; padding: $euiSize; > *:last-child { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap index d61f08152f0c8..d0ea7043932d9 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap @@ -9,8 +9,6 @@ exports[`AttributionControl is rendered 1`] = ` > - attribution with no link - , attribution with link + , + attribution with no link diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js index 4a8a4a5a440a1..0b1c6d6d4e764 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js @@ -56,6 +56,8 @@ export class AttributionControl extends React.Component { } } } + // Reflect top-to-bottom layer order as left-to-right in attribs + uniqueAttributions.reverse(); if (!_.isEqual(this.state.uniqueAttributions, uniqueAttributions)) { this.setState({ uniqueAttributions }); } diff --git a/x-pack/legacy/plugins/maps/public/index_pattern_util.js b/x-pack/legacy/plugins/maps/public/index_pattern_util.js index 48d25ebb1639b..ca43826f307eb 100644 --- a/x-pack/legacy/plugins/maps/public/index_pattern_util.js +++ b/x-pack/legacy/plugins/maps/public/index_pattern_util.js @@ -23,3 +23,11 @@ export function getTermsFields(fields) { return field.aggregatable && ['number', 'boolean', 'date', 'ip', 'string'].includes(field.type); }); } + +// Returns filtered fields list containing only fields that exist in _source. +export function getSourceFields(fields) { + return fields.filter(field => { + // Multi fields are not stored in _source and only exist in index. + return field.subType !== 'multi'; + }); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js index dac05128e8d29..8d1d1439a967e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js @@ -49,12 +49,21 @@ export class InnerJoin { } joinPropertiesToFeature(feature, propertiesMap, rightMetricFields) { + // delete feature properties added by previous join for (let j = 0; j < rightMetricFields.length; j++) { - const { propertyKey } = rightMetricFields[j]; - delete feature.properties[propertyKey]; - const stylePropertyName = VectorStyle.getComputedFieldName(propertyKey); - delete feature.properties[stylePropertyName]; + const { propertyKey: metricPropertyKey } = rightMetricFields[j]; + delete feature.properties[metricPropertyKey]; + + // delete all dynamic properties for metric field + const stylePropertyPrefix = VectorStyle.getComputedFieldNamePrefix(metricPropertyKey); + Object.keys(feature.properties).forEach(featurePropertyKey => { + if (featurePropertyKey.length >= stylePropertyPrefix.length && + featurePropertyKey.substring(0, stylePropertyPrefix.length) === stylePropertyPrefix) { + delete feature.properties[featurePropertyKey]; + } + }); } + const joinKey = feature.properties[this._descriptor.leftField]; const coercedKey = typeof joinKey === 'undefined' || joinKey === null ? null : joinKey.toString(); if (propertiesMap && coercedKey !== null && propertiesMap.has(coercedKey)) { diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js index e6fb1957fec41..99177af4b7a40 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.test.js @@ -58,7 +58,7 @@ describe('joinPropertiesToFeature', () => { properties: { iso2: 'CN', [COUNT_PROPERTY_NAME]: 61, - [`__kbn__scaled(${COUNT_PROPERTY_NAME})`]: 1, + [`__kbn__dynamic__${COUNT_PROPERTY_NAME}__fillColor`]: 1, } }; const propertiesMap = new Map(); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index a3b09de704595..db255074fa261 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -24,10 +24,10 @@ export class GeojsonFileSource extends AbstractVectorSource { static type = GEOJSON_FILE; static title = i18n.translate('xpack.maps.source.geojsonFileTitle', { - defaultMessage: 'Upload GeoJSON vector file' + defaultMessage: 'Uploaded GeoJSON' }); static description = i18n.translate('xpack.maps.source.geojsonFileDescription', { - defaultMessage: 'Upload a GeoJSON file and index in Elasticsearch' + defaultMessage: 'Upload and index GeoJSON data in Elasticsearch' }); static icon = 'importAction'; static isIndexingSource = true; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index e18fe0cea1274..f62693dae4db6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -19,10 +19,10 @@ export class EMSFileSource extends AbstractVectorSource { static type = EMS_FILE; static title = i18n.translate('xpack.maps.source.emsFileTitle', { - defaultMessage: 'Vector shapes' + defaultMessage: 'EMS Boundaries' }); static description = i18n.translate('xpack.maps.source.emsFileDescription', { - defaultMessage: 'Vector shapes of administrative boundaries from Elastic Maps Service' + defaultMessage: 'Administrative boundaries from Elastic Maps Service' }); static icon = 'emsApp'; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js index 5daa5c1dfde2a..ff57bdc8dbfb3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js @@ -20,10 +20,10 @@ export class EMSTMSSource extends AbstractTMSSource { static type = EMS_TMS; static title = i18n.translate('xpack.maps.source.emsTileTitle', { - defaultMessage: 'Tiles' + defaultMessage: 'EMS Basemaps' }); static description = i18n.translate('xpack.maps.source.emsTileDescription', { - defaultMessage: 'Map tiles from Elastic Maps Service' + defaultMessage: 'Tile map service from Elastic Maps Service' }); static icon = 'emsApp'; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 0b8b9aceae05a..d0f175136e9b9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -16,6 +16,7 @@ import { AggConfigs } from 'ui/vis/agg_configs'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { convertToGeoJson } from './convert_to_geojson'; import { VectorStyle } from '../../styles/vector_style'; +import { vectorStyles } from '../../styles/vector_style_defaults'; import { RENDER_AS } from './render_as'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; @@ -261,7 +262,7 @@ export class ESGeoGridSource extends AbstractESSource { ...options }); descriptor.style = VectorStyle.createDescriptor({ - fillColor: { + [vectorStyles.FILL_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { field: { @@ -272,7 +273,7 @@ export class ESGeoGridSource extends AbstractESSource { color: 'Blues' } }, - iconSize: { + [vectorStyles.ICON_SIZE]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { field: { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index e1b930c8ab39b..4514c8fac309a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -13,6 +13,7 @@ import { VectorLayer } from '../../vector_layer'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { VectorStyle } from '../../styles/vector_style'; +import { vectorStyles } from '../../styles/vector_style_defaults'; import { i18n } from '@kbn/i18n'; import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -42,10 +43,10 @@ export class ESPewPewSource extends AbstractESSource { static type = ES_PEW_PEW; static title = i18n.translate('xpack.maps.source.pewPewTitle', { - defaultMessage: 'Source-destination connections' + defaultMessage: 'Point to point' }); static description = i18n.translate('xpack.maps.source.pewPewDescription', { - defaultMessage: 'Aggregated data paths between the origin and destinations.' + defaultMessage: 'Aggregated data paths between the source and destination' }); static createDescriptor({ indexPatternId, sourceGeoField, destGeoField }) { @@ -141,7 +142,7 @@ export class ESPewPewSource extends AbstractESSource { createDefaultLayer(options) { const styleDescriptor = VectorStyle.createDescriptor({ - lineColor: { + [vectorStyles.LINE_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { field: { @@ -152,7 +153,7 @@ export class ESPewPewSource extends AbstractESSource { color: 'Blues' } }, - lineWidth: { + [vectorStyles.LINE_WIDTH]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { field: { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index d0cf7420d84a7..feebb50eeca42 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -18,7 +18,7 @@ import { ES_SEARCH, ES_GEO_FIELD_TYPE, ES_SIZE_LIMIT } from '../../../../common/ import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { ESTooltipProperty } from '../../tooltips/es_tooltip_property'; -import { getTermsFields } from '../../../index_pattern_util'; +import { getSourceFields } from '../../../index_pattern_util'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; @@ -29,7 +29,7 @@ export class ESSearchSource extends AbstractESSource { defaultMessage: 'Documents' }); static description = i18n.translate('xpack.maps.source.esSearchDescription', { - defaultMessage: 'Geospatial data from a Kibana index pattern' + defaultMessage: 'Vector data from a Kibana index pattern' }); static renderEditor({ onPreviewSource, inspectorAdapters }) { @@ -325,7 +325,8 @@ export class ESSearchSource extends AbstractESSource { async getLeftJoinFields() { const indexPattern = await this._getIndexPattern(); - return getTermsFields(indexPattern.fields) + // Left fields are retrieved from _source. + return getSourceFields(indexPattern.fields) .map(field => { return { name: field.name, label: field.name }; }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js index 7b9ae98d31e8e..37de07921854b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js @@ -15,7 +15,7 @@ import { TooltipSelector } from '../../../components/tooltip_selector'; import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; -import { getTermsFields } from '../../../index_pattern_util'; +import { getTermsFields, getSourceFields } from '../../../index_pattern_util'; import { ValidatedRange } from '../../../components/validated_range'; export class UpdateSourceEditor extends Component { @@ -74,11 +74,7 @@ export class UpdateSourceEditor extends Component { this.setState({ dateFields, - tooltipFields: indexPattern.fields.filter(field => { - // Do not show multi fields as tooltip field options - // since they do not have values in _source and exist for indexing only - return field.subType !== 'multi'; - }), + tooltipFields: getSourceFields(indexPattern.fields), termFields: getTermsFields(indexPattern.fields), }); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index 8eccda12b44a0..0f52937f2211f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -15,10 +15,10 @@ export class KibanaRegionmapSource extends AbstractVectorSource { static type = 'REGIONMAP_FILE'; static title = i18n.translate('xpack.maps.source.kbnRegionMapTitle', { - defaultMessage: 'Custom vector shapes' + defaultMessage: 'Configured GeoJSON' }); static description = i18n.translate('xpack.maps.source.kbnRegionMapDescription', { - defaultMessage: 'Vector shapes from static files configured in kibana.yml' + defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml' }) ; static icon = 'logoKibana'; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js index 2c507738bf7e2..f32396108e40c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js @@ -16,10 +16,10 @@ export class KibanaTilemapSource extends AbstractTMSSource { static type = 'KIBANA_TILEMAP'; static title = i18n.translate('xpack.maps.source.kbnTMSTitle', { - defaultMessage: 'Custom Tile Map Service' + defaultMessage: 'Configured Tile Map Service' }); static description = i18n.translate('xpack.maps.source.kbnTMSDescription', { - defaultMessage: 'Map tiles configured in kibana.yml' + defaultMessage: 'Tile map service configured in kibana.yml' }); static icon = 'logoKibana'; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/xyz_tms_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/xyz_tms_source.js index 9a3fbf286c7a1..4a370e7236933 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/xyz_tms_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/xyz_tms_source.js @@ -20,10 +20,10 @@ export class XYZTMSSource extends AbstractTMSSource { static type = 'EMS_XYZ'; static title = i18n.translate('xpack.maps.source.ems_xyzTitle', { - defaultMessage: 'Tile Map Service from URL' + defaultMessage: 'Tile Map Service' }); static description = i18n.translate('xpack.maps.source.ems_xyzDescription', { - defaultMessage: 'Map tiles from a URL that includes the XYZ coordinates' + defaultMessage: 'Tile map service configured in interface' }); static icon = 'grid'; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/get_vector_style_label.js b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/get_vector_style_label.js index 0e3f860dbe136..a39fabf20bae4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/get_vector_style_label.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/get_vector_style_label.js @@ -6,21 +6,23 @@ import { i18n } from '@kbn/i18n'; +import { vectorStyles } from '../../vector_style_defaults'; + export function getVectorStyleLabel(styleName) { switch (styleName) { - case 'fillColor': + case vectorStyles.FILL_COLOR: return i18n.translate('xpack.maps.styles.vector.fillColorLabel', { defaultMessage: 'Fill color' }); - case 'lineColor': + case vectorStyles.LINE_COLOR: return i18n.translate('xpack.maps.styles.vector.borderColorLabel', { defaultMessage: 'Border color' }); - case 'lineWidth': + case vectorStyles.LINE_WIDTH: return i18n.translate('xpack.maps.styles.vector.borderWidthLabel', { defaultMessage: 'Border width' }); - case 'iconSize': + case vectorStyles.ICON_SIZE: return i18n.translate('xpack.maps.styles.vector.symbolSizeLabel', { defaultMessage: 'Symbol size' }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/vector_style_editor.js index f5dc8ebfb13bc..4d25953f5610b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/components/vector/vector_style_editor.js @@ -14,7 +14,8 @@ import { VectorStyleSymbolEditor } from './vector_style_symbol_editor'; import { OrientationEditor } from './orientation/orientation_editor'; import { getDefaultDynamicProperties, - getDefaultStaticProperties + getDefaultStaticProperties, + vectorStyles } from '../../vector_style_defaults'; import { DEFAULT_FILL_COLORS, @@ -97,7 +98,7 @@ export class VectorStyleEditor extends Component { _renderFillColor() { return ( Minimal initialization. 1`] = ` + +
+ +
+
+`; diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.test.tsx b/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.test.tsx new file mode 100644 index 0000000000000..d98077da230c8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.test.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { uiTimefilterMock } from '../../../contexts/ui/__mocks__/mocks'; +import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; + +import { MlSuperDatePickerWithUpdate, TopNav } from './top_nav'; + +uiTimefilterMock.enableAutoRefreshSelector(); +uiTimefilterMock.enableTimeRangeSelector(); + +jest.mock('../../../contexts/ui/use_ui_context'); + +const noop = () => {}; + +describe('Navigation Menu: ', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('Minimal initialization.', () => { + const refreshListener = jest.fn(); + const refreshSubscription = mlTimefilterRefresh$.subscribe(refreshListener); + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(refreshListener).toBeCalledTimes(0); + + refreshSubscription.unsubscribe(); + }); + + // The following tests are written against MlSuperDatePickerWithUpdate + // instead of TopNav. TopNav uses hooks and we cannot writing tests + // with async hook updates yet until React 16.9 is available. + + // MlSuperDatePickerWithUpdate fixes an issue with EuiSuperDatePicker + // which didn't make it into Kibana 7.4. We should be able to just + // use EuiSuperDatePicker again once the following PR is in EUI: + // https://github.com/elastic/eui/pull/2298 + + test('Listen for consecutive super date picker refreshs.', async () => { + const onRefresh = jest.fn(); + + const componentRefresh = mount( + + ); + + const instanceRefresh = componentRefresh.instance(); + + jest.advanceTimersByTime(10); + // @ts-ignore + await instanceRefresh.asyncInterval.__pendingFn; + jest.advanceTimersByTime(10); + // @ts-ignore + await instanceRefresh.asyncInterval.__pendingFn; + + expect(onRefresh).toBeCalledTimes(2); + }); + + test('Switching refresh interval to pause should stop onRefresh being called.', async () => { + const onRefresh = jest.fn(); + + const componentRefresh = mount( + + ); + + const instanceRefresh = componentRefresh.instance(); + + jest.advanceTimersByTime(10); + // @ts-ignore + await instanceRefresh.asyncInterval.__pendingFn; + componentRefresh.setProps({ isPaused: true, refreshInterval: 0 }); + jest.advanceTimersByTime(10); + // @ts-ignore + await instanceRefresh.asyncInterval.__pendingFn; + + expect(onRefresh).toBeCalledTimes(1); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.tsx b/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.tsx index 6e38b37c33a24..f32632cd321ae 100644 --- a/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.tsx +++ b/x-pack/legacy/plugins/ml/public/components/navigation_menu/top_nav/top_nav.tsx @@ -4,14 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; +import React, { Component, FC, Fragment, useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; -import { EuiSuperDatePicker } from '@elastic/eui'; -import { TimeHistory, TimeRange } from 'ui/timefilter'; +import { EuiSuperDatePicker, EuiSuperDatePickerProps } from '@elastic/eui'; +import { TimeHistory } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service'; import { useUiContext } from '../../../contexts/ui/use_ui_context'; +interface ComponentWithConstructor extends Component { + new (): Component; +} + +const MlSuperDatePicker = (EuiSuperDatePicker as any) as ComponentWithConstructor< + EuiSuperDatePickerProps +>; + +// This part fixes a problem with EuiSuperDater picker where it would not reflect +// a prop change of isPaused on the internal interval. This fix will be released +// with EUI 13.7.0 but only 13.6.1 without the fix made it into Kibana 7.4 so +// it's copied here. +export class MlSuperDatePickerWithUpdate extends MlSuperDatePicker { + componentDidUpdate = () => { + // @ts-ignore + this.stopInterval(); + if (!this.props.isPaused) { + // @ts-ignore + this.startInterval(this.props.refreshInterval); + } + }; +} + interface Duration { start: string; end: string; @@ -96,20 +120,14 @@ export const TopNav: FC = () => { {(isAutoRefreshSelectorEnabled || isTimeRangeSelectorEnabled) && (
- { - // This check is a workaround to catch a bug in EuiSuperDatePicker which - // might not have disabled the refresh interval after a props change. - if (!refreshInterval.pause) { - mlTimefilterRefresh$.next(); - } - }} + onRefresh={() => mlTimefilterRefresh$.next()} onRefreshChange={updateInterval} recentlyUsedRanges={recentlyUsedRanges} dateFormat={dateFormat} diff --git a/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/mocks.ts b/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/mocks.ts index 90bc028a7cd37..e6e51f0bdecf4 100644 --- a/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/mocks.ts +++ b/x-pack/legacy/plugins/ml/public/contexts/ui/__mocks__/mocks.ts @@ -11,7 +11,7 @@ export const uiChromeMock = { get: (key: string) => { switch (key) { case 'dateFormat': - return {}; + return 'MMM D, YYYY @ HH:mm:ss.SSS'; case 'theme:darkMode': return false; case 'timepicker:timeDefaults': @@ -26,12 +26,45 @@ export const uiChromeMock = { }, }; +interface RefreshInterval { + value: number; + pause: boolean; +} + +const time = { + from: 'Thu Aug 29 2019 02:04:19 GMT+0200', + to: 'Sun Sep 29 2019 01:45:36 GMT+0200', +}; + export const uiTimefilterMock = { - getRefreshInterval: () => '30s', - getTime: () => ({ from: 0, to: 0 }), + enableAutoRefreshSelector() { + this.isAutoRefreshSelectorEnabled = true; + }, + enableTimeRangeSelector() { + this.isTimeRangeSelectorEnabled = true; + }, + getEnabledUpdated$() { + return { subscribe: jest.fn() }; + }, + getRefreshInterval() { + return this.refreshInterval; + }, + getRefreshIntervalUpdate$() { + return { subscribe: jest.fn() }; + }, + getTime: () => time, + getTimeUpdate$() { + return { subscribe: jest.fn() }; + }, + isAutoRefreshSelectorEnabled: false, + isTimeRangeSelectorEnabled: false, + refreshInterval: { value: 0, pause: true }, on: (event: string, reload: () => void) => {}, + setRefreshInterval(refreshInterval: RefreshInterval) { + this.refreshInterval = refreshInterval; + }, }; export const uiTimeHistoryMock = { - get: () => [{ from: 0, to: 0 }], + get: () => [time], }; diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_create/step_create_form.tsx b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_create/step_create_form.tsx index 4815d658f6540..af26e99b3af99 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_create/step_create_form.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_create/step_create_form.tsx @@ -84,7 +84,7 @@ export const StepCreateForm: SFC = React.memo( await ml.dataFrame.createDataFrameTransform(transformId, transformConfig); toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.stepCreateForm.createTransformSuccessMessage', { - defaultMessage: 'Data frame transform {transformId} created successfully.', + defaultMessage: 'Request to create data frame transform {transformId} acknowledged.', values: { transformId }, }) ); @@ -114,7 +114,7 @@ export const StepCreateForm: SFC = React.memo( await ml.dataFrame.startDataFrameTransforms([{ id: transformId }]); toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.stepCreateForm.startTransformSuccessMessage', { - defaultMessage: 'Data frame transform {transformId} started successfully.', + defaultMessage: 'Request to start data frame transform {transformId} acknowledged.', values: { transformId }, }) ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_define/pivot_preview.tsx b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_define/pivot_preview.tsx index 7a85693b3c581..09b97219ecdfa 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_define/pivot_preview.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/data_frame_new_pivot/components/step_define/pivot_preview.tsx @@ -229,43 +229,41 @@ export const PivotPreview: SFC = React.memo(({ aggs, groupBy, const columnKeys = getFlattenedFields(dataFramePreviewData[0]); columnKeys.sort(sortColumns(groupByArr)); - const columns = columnKeys - .filter(k => typeof dataFramePreviewMappings.properties[k] !== 'undefined') - .map(k => { - const column: ColumnType = { - field: k, - name: k, - sortable: true, - truncateText: true, - }; - if (typeof dataFramePreviewMappings.properties[k] !== 'undefined') { - const esFieldType = dataFramePreviewMappings.properties[k].type; - switch (esFieldType) { - case ES_FIELD_TYPES.BOOLEAN: - column.dataType = 'boolean'; - break; - case ES_FIELD_TYPES.DATE: - column.align = 'right'; - column.render = (d: any) => formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - break; - case ES_FIELD_TYPES.BYTE: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.SHORT: - column.dataType = 'number'; - break; - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.TEXT: - column.dataType = 'string'; - break; - } + const columns = columnKeys.map(k => { + const column: ColumnType = { + field: k, + name: k, + sortable: true, + truncateText: true, + }; + if (typeof dataFramePreviewMappings.properties[k] !== 'undefined') { + const esFieldType = dataFramePreviewMappings.properties[k].type; + switch (esFieldType) { + case ES_FIELD_TYPES.BOOLEAN: + column.dataType = 'boolean'; + break; + case ES_FIELD_TYPES.DATE: + column.align = 'right'; + column.render = (d: any) => formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); + break; + case ES_FIELD_TYPES.BYTE: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.SHORT: + column.dataType = 'number'; + break; + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.TEXT: + column.dataType = 'string'; + break; } - return column; - }); + } + return column; + }); if (columns.length === 0) { return null; diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/components/transform_list/expanded_row_preview_pane.tsx index 0b1d741d8245d..dd8de60eb0fdb 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,7 +6,12 @@ import React, { FC, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { SortDirection, SORT_DIRECTION } from '../../../../../../common/types/eui/in_memory_table'; +import moment from 'moment-timezone'; +import { + SortDirection, + SORT_DIRECTION, + FieldDataColumnType, +} from '../../../../../../common/types/eui/in_memory_table'; import { ml } from '../../../../../services/ml_api_service'; @@ -16,15 +21,10 @@ import { PreviewRequestBody, DataFrameTransformPivotConfig, } from '../../../../common'; +import { ES_FIELD_TYPES } from '../../../../../../common/constants/field_types'; +import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { TransformTable } from './transform_table'; -interface Column { - field: string; - name: string; - sortable: boolean; - truncateText: boolean; -} - interface Props { transformConfig: DataFrameTransformPivotConfig; } @@ -72,7 +72,7 @@ function getDataFromTransform( export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { const [dataFramePreviewData, setDataFramePreviewData] = useState([]); - const [columns, setColumns] = useState([]); + const [columns, setColumns] = useState([]); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [sortField, setSortField] = useState(''); @@ -99,13 +99,44 @@ export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { if (resp.preview.length > 0) { const columnKeys = getFlattenedFields(resp.preview[0]); columnKeys.sort(sortColumns(groupByArr)); - const tableColumns = columnKeys.map(k => { - return { + + const tableColumns: FieldDataColumnType[] = columnKeys.map(k => { + const column: FieldDataColumnType = { field: k, name: k, sortable: true, truncateText: true, }; + + if (typeof resp.mappings.properties[k] !== 'undefined') { + const esFieldType = resp.mappings.properties[k].type; + switch (esFieldType) { + case ES_FIELD_TYPES.BOOLEAN: + column.dataType = 'boolean'; + break; + case ES_FIELD_TYPES.DATE: + column.align = 'right'; + column.render = (d: any) => + formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); + break; + case ES_FIELD_TYPES.BYTE: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.SHORT: + column.dataType = 'number'; + break; + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.TEXT: + column.dataType = 'string'; + break; + } + } + + return column; }); setDataFramePreviewData(resp.preview); diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/delete_transform.ts b/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/delete_transform.ts index 796af91d04d40..b661890330b16 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/delete_transform.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/delete_transform.ts @@ -31,7 +31,7 @@ export const deleteTransforms = async (dataFrames: DataFrameTransformListRow[]) if (results[transformId].success === true) { toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.transformList.deleteTransformSuccessMessage', { - defaultMessage: 'Data frame transform {transformId} deleted successfully.', + defaultMessage: 'Request to delete data frame transform {transformId} acknowledged.', values: { transformId }, }) ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/start_transform.ts b/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/start_transform.ts index 7887607ee573e..44bded99c6c31 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/start_transform.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/start_transform.ts @@ -33,7 +33,7 @@ export const startTransforms = async (dataFrames: DataFrameTransformListRow[]) = if (results[transformId].success === true) { toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.transformList.startTransformSuccessMessage', { - defaultMessage: 'Data frame transform {transformId} started successfully.', + defaultMessage: 'Request to start data frame transform {transformId} acknowledged.', values: { transformId }, }) ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/stop_transform.ts b/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/stop_transform.ts index d7e5485d22656..717d8841335bb 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/stop_transform.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame/pages/transform_management/services/transform_service/stop_transform.ts @@ -33,7 +33,7 @@ export const stopTransforms = async (dataFrames: DataFrameTransformListRow[]) => if (results[transformId].success === true) { toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.transformList.stopTransformSuccessMessage', { - defaultMessage: 'Data frame transform {transformId} stopped successfully.', + defaultMessage: 'Request to stop data frame transform {transformId} acknowledged.', values: { transformId }, }) ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts index 5faf20991056a..95310b414d6f9 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts @@ -65,7 +65,7 @@ export interface DataFrameAnalyticsConfig { results_field: string; }; source: { - index: IndexName; + index: IndexName | IndexName[]; }; analysis: AnalysisConfig; analyzed_fields: { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx new file mode 100644 index 0000000000000..027acf6fa2e79 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; + +import { CreateAnalyticsButton } from './create_analytics_button'; + +import { KibanaContext } from '../../../../../contexts/kibana'; +import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; + +import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; + +jest.mock('ui/index_patterns', () => ({ + validateIndexPattern: () => true, + INDEX_PATTERN_ILLEGAL_CHARACTERS: [], +})); + +const getMountedHook = () => + mountHook( + () => useCreateAnalyticsForm(), + ({ children }) => ( + {children} + ) + ); + +// workaround to make React.memo() work with enzyme +jest.mock('react', () => { + const r = jest.requireActual('react'); + return { ...r, memo: (x: any) => x }; +}); + +describe('Data Frame Analytics: ', () => { + test('Minimal initialization', () => { + const { getLastHookValue } = getMountedHook(); + const props = getLastHookValue(); + const wrapper = mount(); + + expect(wrapper.find('EuiButton').text()).toBe('Create analytics job'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index dd48fdd2fea5a..0d4c606db261e 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -32,7 +32,7 @@ export const CreateAnalyticsButton: FC = props => { data-test-subj="mlDataFrameAnalyticsButtonCreate" > {i18n.translate('xpack.ml.dataframe.analyticsList.createDataFrameAnalyticsButton', { - defaultMessage: 'Create outlier detection job', + defaultMessage: 'Create analytics job', })} ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx new file mode 100644 index 0000000000000..d9118fab267d2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.test.tsx @@ -0,0 +1,54 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; + +import { CreateAnalyticsForm } from './create_analytics_form'; + +import { KibanaContext } from '../../../../../contexts/kibana'; +import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; + +import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; + +jest.mock('ui/index_patterns', () => ({ + validateIndexPattern: () => true, + INDEX_PATTERN_ILLEGAL_CHARACTERS: [], +})); + +const getMountedHook = () => + mountHook( + () => useCreateAnalyticsForm(), + ({ children }) => ( + {children} + ) + ); + +// workaround to make React.memo() work with enzyme +jest.mock('react', () => { + const r = jest.requireActual('react'); + return { ...r, memo: (x: any) => x }; +}); + +describe('Data Frame Analytics: ', () => { + test('Minimal initialization', () => { + const { getLastHookValue } = getMountedHook(); + const props = getLastHookValue(); + const wrapper = mount(); + + const euiFormRows = wrapper.find('EuiFormRow'); + expect(euiFormRows.length).toBe(5); + + const row1 = euiFormRows.at(0); + expect(row1.find('label').text()).toBe('Job type'); + expect(row1.find('EuiText').text()).toBe('Outlier detection'); + expect(row1.find('EuiLink').text()).toBe('advanced editor'); + + const row2 = euiFormRows.at(1); + expect(row2.find('label').text()).toBe('Job ID'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 59a0ba426894a..eb59bc9af164b 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -7,7 +7,6 @@ import React, { Fragment, FC } from 'react'; import { - EuiButtonEmpty, EuiCallOut, EuiComboBox, EuiForm, @@ -20,6 +19,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { metadata } from 'ui/metadata'; import { INDEX_PATTERN_ILLEGAL_CHARACTERS } from 'ui/index_patterns'; @@ -75,9 +75,36 @@ export const CreateAnalyticsForm: FC = ({ actions, sta ))} {!isJobCreated && ( + + + + ), + }} + /> + } + > + + {i18n.translate('xpack.ml.dataframe.analytics.create.outlierDetectionText', { + defaultMessage: 'Outlier detection', + })} + + = ({ actions, sta > setFormState({ jobId: e.target.value })} aria-label={i18n.translate( @@ -119,7 +148,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta helpText={ !sourceIndexNameEmpty && !indexPatternsWithNumericFields.includes(sourceIndex) && - i18n.translate('xpack.ml.dataframe.stepDetailsForm.sourceIndexHelpText', { + i18n.translate('xpack.ml.dataframe.analytics.create.sourceIndexHelpText', { defaultMessage: 'This index pattern does not contain any numeric type fields. The analytics job may not be able to come up with any outliers.', }) @@ -243,17 +272,6 @@ export const CreateAnalyticsForm: FC = ({ actions, sta onChange={() => setFormState({ createIndexPattern: !createIndexPattern })} /> - - {i18n.translate('xpack.ml.dataframe.analytics.create.switchToAdvancedEditorButton', { - defaultMessage: 'Switch to advanced editor', - })} - - - {i18n.translate('xpack.ml.dataframe.analytics.create.switchToAdvancedEditorHelpText', { - defaultMessage: - 'Note you cannot switch back to this form once the advanced editor is enabled.', - })} - )} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.test.tsx new file mode 100644 index 0000000000000..7d505a86d08a9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { mountHook } from '../../../../../../../../../test_utils/enzyme_helpers'; + +import { CreateAnalyticsModal } from './create_analytics_modal'; + +import { KibanaContext } from '../../../../../contexts/kibana'; +import { kibanaContextValueMock } from '../../../../../contexts/kibana/__mocks__/kibana_context_value'; + +import { useCreateAnalyticsForm } from '../../hooks/use_create_analytics_form'; + +jest.mock('ui/index_patterns', () => ({ + validateIndexPattern: () => true, + INDEX_PATTERN_ILLEGAL_CHARACTERS: [], +})); + +const getMountedHook = () => + mountHook( + () => useCreateAnalyticsForm(), + ({ children }) => ( + {children} + ) + ); + +// workaround to make React.memo() work with enzyme +jest.mock('react', () => { + const r = jest.requireActual('react'); + return { ...r, memo: (x: any) => x }; +}); + +describe('Data Frame Analytics: ', () => { + test('Minimal initialization', () => { + const { getLastHookValue } = getMountedHook(); + const props = getLastHookValue(); + const wrapper = mount(); + + expect(wrapper.find('EuiModalHeaderTitle').text()).toBe('Create analytics job'); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx index 05a5bf3524392..5f735fff73536 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_modal/create_analytics_modal.tsx @@ -43,7 +43,7 @@ export const CreateAnalyticsModal: FC = ({ {i18n.translate('xpack.ml.dataframe.analytics.create.modalHeaderTitle', { - defaultMessage: 'Create outlier detection job', + defaultMessage: 'Create analytics job', })} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index b74611fe9c77b..46cf833610073 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -4,14 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ +import { merge } from 'lodash'; + +import { DataFrameAnalyticsConfig } from '../../../../common'; + import { ACTION } from './actions'; -import { reducer } from './reducer'; +import { reducer, validateAdvancedEditor } from './reducer'; import { getInitialState } from './state'; jest.mock('ui/index_patterns', () => ({ validateIndexPattern: () => true, })); +type SourceIndex = DataFrameAnalyticsConfig['source']['index']; + +const getMockState = (index: SourceIndex) => + merge(getInitialState(), { + form: { + jobIdEmpty: false, + jobIdValid: true, + jobIdExists: false, + createIndexPattern: false, + }, + jobConfig: { + source: { index }, + dest: { index: 'the-destination-index' }, + }, + }); + describe('useCreateAnalyticsForm', () => { test('reducer(): provide a minimum required valid job config, then reset.', () => { const initialState = getInitialState(); @@ -65,4 +85,30 @@ describe('useCreateAnalyticsForm', () => { }); expect(resetMessageState.requestMessages).toHaveLength(0); }); + + test('validateAdvancedEditor(): check index pattern variations', () => { + // valid single index pattern + expect(validateAdvancedEditor(getMockState('the-source-index')).isValid).toBe(true); + // valid array with one ES index pattern + expect(validateAdvancedEditor(getMockState(['the-source-index'])).isValid).toBe(true); + // valid array with two ES index patterns + expect( + validateAdvancedEditor(getMockState(['the-source-index-1', 'the-source-index-2'])).isValid + ).toBe(true); + // invalid comma-separated index pattern, this is only allowed in the simple form + // but not the advanced editor. + expect( + validateAdvancedEditor(getMockState('the-source-index-1,the-source-index-2')).isValid + ).toBe(false); + expect( + validateAdvancedEditor( + getMockState(['the-source-index-1,the-source-index-2', 'the-source-index-3']) + ).isValid + ).toBe(false); + // invalid formats ("fake" TS casting to get valid TS and be able to run the tests) + expect(validateAdvancedEditor(getMockState({} as SourceIndex)).isValid).toBe(false); + expect( + validateAdvancedEditor(getMockState((undefined as unknown) as SourceIndex)).isValid + ).toBe(false); + }); }); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 1c4aefcd26ced..a52d88365d7ac 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -16,15 +16,45 @@ import { isAnalyticsIdValid } from '../../../../common'; import { Action, ACTION } from './actions'; import { getInitialState, getJobConfigFromFormState, State } from './state'; -const validateAdvancedEditor = (state: State): State => { +const getSourceIndexString = (state: State) => { + const { jobConfig } = state; + + const sourceIndex = idx(jobConfig, _ => _.source.index); + + if (typeof sourceIndex === 'string') { + return sourceIndex; + } + + if (Array.isArray(sourceIndex)) { + return sourceIndex.join(','); + } + + return ''; +}; + +export const validateAdvancedEditor = (state: State): State => { const { jobIdEmpty, jobIdValid, jobIdExists, createIndexPattern } = state.form; const { jobConfig } = state; state.advancedEditorMessages = []; - const sourceIndexName = idx(jobConfig, _ => _.source.index) || ''; + const sourceIndexName = getSourceIndexString(state); const sourceIndexNameEmpty = sourceIndexName === ''; - const sourceIndexNameValid = validateIndexPattern(sourceIndexName); + // general check against Kibana index pattern names, but since this is about the advanced editor + // with support for arrays in the job config, we also need to check that each individual name + // doesn't include a comma if index names are supplied as an array. + // `validateIndexPattern()` returns a map of messages, we're only interested here if it's valid or not. + // If there are no messages, it means the index pattern is valid. + let sourceIndexNameValid = Object.keys(validateIndexPattern(sourceIndexName)).length === 0; + const sourceIndex = idx(jobConfig, _ => _.source.index); + if (sourceIndexNameValid) { + if (typeof sourceIndex === 'string') { + sourceIndexNameValid = !sourceIndex.includes(','); + } + if (Array.isArray(sourceIndex)) { + sourceIndexNameValid = !sourceIndex.some(d => d.includes(',')); + } + } const destinationIndexName = idx(jobConfig, _ => _.dest.index) || ''; const destinationIndexNameEmpty = destinationIndexName === ''; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts index f0d3bdf186994..ec70c54892a0e 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts @@ -21,5 +21,13 @@ describe('useCreateAnalyticsForm', () => { expect(idx(jobConfig, _ => _.source.index)).toBe('the-source-index'); expect(idx(jobConfig, _ => _.analyzed_fields.excludes)).toStrictEqual([]); expect(typeof idx(jobConfig, _ => _.analyzed_fields.includes)).toBe('undefined'); + + // test the conversion of comma-separated Kibana index patterns to ES array based index patterns + state.form.sourceIndex = 'the-source-index-1,the-source-index-2'; + const jobConfigSourceIndexArray = getJobConfigFromFormState(state.form); + expect(idx(jobConfigSourceIndexArray, _ => _.source.index)).toStrictEqual([ + 'the-source-index-1', + 'the-source-index-2', + ]); }); }); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 1aacab853f522..a4a0087c9d869 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -92,7 +92,12 @@ export const getJobConfigFromFormState = ( ): DeepPartial => { return { source: { - index: formState.sourceIndex, + // If a Kibana index patterns includes commas, we need to split + // the into an array of indices to be in the correct format for + // the data frame analytics API. + index: formState.sourceIndex.includes(',') + ? formState.sourceIndex.split(',').map(d => d.trim()) + : formState.sourceIndex, }, dest: { index: formState.destinationIndex, diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index dc4681169850a..418eb75290e1e 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -105,7 +105,7 @@ export const useCreateAnalyticsForm = () => { message: i18n.translate( 'xpack.ml.dataframe.stepCreateForm.createDataFrameAnalyticsSuccessMessage', { - defaultMessage: 'Analytics job {jobId} created.', + defaultMessage: 'Request to create data frame analytics {jobId} acknowledged.', values: { jobId }, } ), @@ -273,7 +273,7 @@ export const useCreateAnalyticsForm = () => { message: i18n.translate( 'xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage', { - defaultMessage: 'Analytics job {jobId} started.', + defaultMessage: 'Request to start data frame analytics {jobId} acknowledged.', values: { jobId }, } ), diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index 27e453354cafb..dde4f8efc899c 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -27,7 +27,7 @@ export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { await ml.dataFrameAnalytics.deleteDataFrameAnalytics(d.config.id); toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', { - defaultMessage: 'Data frame analytics {analyticsId} delete request acknowledged.', + defaultMessage: 'Request to delete data frame analytics {analyticsId} acknowledged.', values: { analyticsId: d.config.id }, }) ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts index 5ce05c5ddbfce..da09c4842b843 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/start_analytics.ts @@ -17,7 +17,7 @@ export const startAnalytics = async (d: DataFrameAnalyticsListRow) => { await ml.dataFrameAnalytics.startDataFrameAnalytics(d.config.id); toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.analyticsList.startAnalyticsSuccessMessage', { - defaultMessage: 'Data frame analytics {analyticsId} start request acknowledged.', + defaultMessage: 'Request to start data frame analytics {analyticsId} acknowledged.', values: { analyticsId: d.config.id }, }) ); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts index 5af3e7e6696a8..44286c8b1f7dd 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts @@ -24,7 +24,7 @@ export const stopAnalytics = async (d: DataFrameAnalyticsListRow) => { ); toastNotifications.addSuccess( i18n.translate('xpack.ml.dataframe.analyticsList.stopAnalyticsSuccessMessage', { - defaultMessage: 'Data frame analytics {analyticsId} stop request acknowledged.', + defaultMessage: 'Request to stop data frame analytics {analyticsId} acknowledged.', values: { analyticsId: d.config.id }, }) ); diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer.js b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer.js index d055a2ee09d6f..a17c563b78dc3 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer.js +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/components/import_view/importer/importer.js @@ -10,7 +10,8 @@ import { chunk } from 'lodash'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; -const CHUNK_SIZE = 10000; +const CHUNK_SIZE = 5000; +const MAX_CHUNK_CHAR_COUNT = 1000000; const IMPORT_RETRIES = 5; export class Importer { @@ -21,6 +22,7 @@ export class Importer { this.data = []; this.docArray = []; + this.docSizeArray = []; } async initializeImport(index) { @@ -58,7 +60,7 @@ export class Importer { }; } - const chunks = chunk(this.docArray, CHUNK_SIZE); + const chunks = createDocumentChunks(this.docArray); const ingestPipeline = { id: pipelineId, @@ -86,13 +88,18 @@ export class Importer { }; while (resp.success === false && retries > 0) { - resp = await ml.fileDatavisualizer.import(aggs); + try { + resp = await ml.fileDatavisualizer.import(aggs); - if (retries < IMPORT_RETRIES) { - console.log(`Retrying import ${IMPORT_RETRIES - retries}`); - } + if (retries < IMPORT_RETRIES) { + console.log(`Retrying import ${IMPORT_RETRIES - retries}`); + } - retries--; + retries--; + } catch (err) { + resp = { success: false, error: err }; + retries = 0; + } } if (resp.success) { @@ -152,3 +159,32 @@ function updatePipelineTimezone(ingestPipeline) { } } } + +function createDocumentChunks(docArray) { + const chunks = []; + // chop docArray into 5000 doc chunks + const tempChunks = chunk(docArray, CHUNK_SIZE); + + // loop over tempChunks and check that the total character length + // for each chunk is within the MAX_CHUNK_CHAR_COUNT. + // if the length is too long, split the chunk into smaller chunks + // based on how much larger it is than MAX_CHUNK_CHAR_COUNT + // note, each document is a different size, so dividing by charCountOfDocs + // only produces an average chunk size that should be smaller than the max length + for (let i = 0; i < tempChunks.length; i++) { + const docs = tempChunks[i]; + const numberOfDocs = docs.length; + + const charCountOfDocs = JSON.stringify(docs).length; + if (charCountOfDocs > MAX_CHUNK_CHAR_COUNT) { + // calculate new chunk size which should produce a chunk + // who's length is on average around MAX_CHUNK_CHAR_COUNT + const adjustedChunkSize = Math.floor((MAX_CHUNK_CHAR_COUNT / charCountOfDocs) * numberOfDocs); + const smallerChunks = chunk(docs, adjustedChunkSize); + chunks.push(...smallerChunks); + } else { + chunks.push(docs); + } + } + return chunks; +} diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js index 8243f64f6114e..172c198509d28 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_actions/results.js @@ -52,6 +52,7 @@ function ResultLinksUI({ jobs, intl }) { aria-label={openJobsInSingleMetricViewerText} className="results-button" isDisabled={(singleMetricEnabled === false || jobActionsDisabled === true)} + data-test-subj={`openJobsInSingleMetricViewer openJobsInSingleMetricViewer-${jobs[0].id}`} /> } @@ -65,6 +66,7 @@ function ResultLinksUI({ jobs, intl }) { aria-label={openJobsInAnomalyExplorerText} className="results-button" isDisabled={(jobActionsDisabled === true)} + data-test-subj={`openJobsInAnomalyExplorer openJobsInSingleAnomalyExplorer-${jobs[0].id}`} />
diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js index 2d8fcb55d7f74..40e0c4392d959 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/extract_job_details.js @@ -16,6 +16,7 @@ export function extractJobDetails(job) { } const general = { + id: 'general', title: i18n.translate('xpack.ml.jobsList.jobDetails.generalTitle', { defaultMessage: 'General' }), @@ -25,6 +26,7 @@ export function extractJobDetails(job) { const customUrl = { + id: 'customUrl', title: i18n.translate('xpack.ml.jobsList.jobDetails.customUrlsTitle', { defaultMessage: 'Custom URLs' }), @@ -36,6 +38,7 @@ export function extractJobDetails(job) { } const node = { + id: 'node', title: i18n.translate('xpack.ml.jobsList.jobDetails.nodeTitle', { defaultMessage: 'Node' }), @@ -47,6 +50,7 @@ export function extractJobDetails(job) { } const calendars = { + id: 'calendars', title: i18n.translate('xpack.ml.jobsList.jobDetails.calendarsTitle', { defaultMessage: 'Calendars' }), @@ -64,6 +68,7 @@ export function extractJobDetails(job) { } const detectors = { + id: 'detectors', title: i18n.translate('xpack.ml.jobsList.jobDetails.detectorsTitle', { defaultMessage: 'Detectors' }), @@ -81,6 +86,7 @@ export function extractJobDetails(job) { } const influencers = { + id: 'influencers', title: i18n.translate('xpack.ml.jobsList.jobDetails.influencersTitle', { defaultMessage: 'Influencers' }), @@ -89,6 +95,7 @@ export function extractJobDetails(job) { }; const analysisConfig = { + id: 'analysisConfig', title: i18n.translate('xpack.ml.jobsList.jobDetails.analysisConfigTitle', { defaultMessage: 'Analysis config' }), @@ -97,6 +104,7 @@ export function extractJobDetails(job) { }; const analysisLimits = { + id: 'analysisLimits', title: i18n.translate('xpack.ml.jobsList.jobDetails.analysisLimitsTitle', { defaultMessage: 'Analysis limits' }), @@ -105,6 +113,7 @@ export function extractJobDetails(job) { }; const dataDescription = { + id: 'dataDescription', title: i18n.translate('xpack.ml.jobsList.jobDetails.dataDescriptionTitle', { defaultMessage: 'Data description' }), @@ -113,6 +122,7 @@ export function extractJobDetails(job) { }; const datafeed = { + id: 'datafeed', title: i18n.translate('xpack.ml.jobsList.jobDetails.datafeedTitle', { defaultMessage: 'Datafeed' }), @@ -132,6 +142,7 @@ export function extractJobDetails(job) { } const counts = { + id: 'counts', title: i18n.translate('xpack.ml.jobsList.jobDetails.countsTitle', { defaultMessage: 'Counts' }), @@ -140,6 +151,7 @@ export function extractJobDetails(job) { }; const modelSizeStats = { + id: 'modelSizeStats', title: i18n.translate('xpack.ml.jobsList.jobDetails.modelSizeStatsTitle', { defaultMessage: 'Model size stats' }), @@ -148,6 +160,7 @@ export function extractJobDetails(job) { }; const datafeedTimingStats = { + id: 'datafeedTimingStats', title: i18n.translate('xpack.ml.jobsList.jobDetails.datafeedTimingStatsTitle', { defaultMessage: 'Timing stats' }), diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js index fc7d99c01a8cc..97e7c1b72e565 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_details/job_details.js @@ -51,7 +51,9 @@ class JobDetailsUI extends Component { const { job } = this.state; if (job === undefined) { return ( -
+
+ +
); } else { @@ -75,35 +77,43 @@ class JobDetailsUI extends Component { const tabs = [{ id: 'job-settings', + 'data-test-subj': 'tab-job-settings', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.jobSettingsLabel', defaultMessage: 'Job settings' }), - content: , + content: , time: job.open_time }, { id: 'job-config', + 'data-test-subj': 'tab-job-config', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.jobConfigLabel', defaultMessage: 'Job config' }), - content: , + content: , }, { id: 'counts', + 'data-test-subj': 'tab-counts', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.countsLabel', defaultMessage: 'Counts' }), - content: , + content: , }, { id: 'json', + 'data-test-subj': 'tab-json', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.jsonLabel', defaultMessage: 'JSON' }), - content: , + content: , }, { id: 'job-messages', + 'data-test-subj': 'tab-job-messages', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.jobMessagesLabel', defaultMessage: 'Job messages' @@ -116,15 +126,17 @@ class JobDetailsUI extends Component { // Datafeed should be at index 2 in tabs array for full details tabs.splice(2, 0, { id: 'datafeed', + 'data-test-subj': 'tab-datafeed', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.datafeedLabel', defaultMessage: 'Datafeed' }), - content: , + content: , }); tabs.push({ id: 'datafeed-preview', + 'data-test-subj': 'tab-datafeed-preview', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.datafeedPreviewLabel', defaultMessage: 'Datafeed preview' @@ -132,6 +144,7 @@ class JobDetailsUI extends Component { content: , }, { id: 'forecasts', + 'data-test-subj': 'tab-forecasts', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.forecastsLabel', defaultMessage: 'Forecasts' @@ -143,6 +156,7 @@ class JobDetailsUI extends Component { if (mlAnnotationsEnabled && showFullDetails) { tabs.push({ id: 'annotations', + 'data-test-subj': 'tab-annotations', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobDetails.tabs.annotationsLabel', defaultMessage: 'Annotations' @@ -157,7 +171,7 @@ class JobDetailsUI extends Component { } return ( -
+

{section.title}

-
+
{ section.items.map((item, i) => ()) } @@ -78,7 +78,7 @@ export class JobDetailsPane extends Component { return ( -
+
{ sections @@ -100,5 +100,6 @@ export class JobDetailsPane extends Component { } JobDetailsPane.propTypes = { sections: PropTypes.array.isRequired, + 'data-test-subj': PropTypes.string, }; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js index 345807f0a016e..9ba7aba9dc876 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/job_group/job_group.js @@ -16,6 +16,7 @@ export function JobGroup({ name }) { return (
), width: '3%' }, { field: 'id', + 'data-test-subj': 'id', name: intl.formatMessage({ id: 'xpack.ml.jobsList.idLabel', defaultMessage: 'ID' @@ -193,6 +195,7 @@ class JobsListUI extends Component { }), sortable: true, field: 'description', + 'data-test-subj': 'description', render: (description, item) => ( ), @@ -200,6 +203,7 @@ class JobsListUI extends Component { width: '20%' }, { field: 'processed_record_count', + 'data-test-subj': 'recordCount', name: intl.formatMessage({ id: 'xpack.ml.jobsList.processedRecordsLabel', defaultMessage: 'Processed records' @@ -211,6 +215,7 @@ class JobsListUI extends Component { width: '10%' }, { field: 'memory_status', + 'data-test-subj': 'memoryStatus', name: intl.formatMessage({ id: 'xpack.ml.jobsList.memoryStatusLabel', defaultMessage: 'Memory status' @@ -220,6 +225,7 @@ class JobsListUI extends Component { width: '5%' }, { field: 'jobState', + 'data-test-subj': 'jobState', name: intl.formatMessage({ id: 'xpack.ml.jobsList.jobStateLabel', defaultMessage: 'Job state' @@ -229,6 +235,7 @@ class JobsListUI extends Component { width: '8%' }, { field: 'datafeedState', + 'data-test-subj': 'datafeedState', name: intl.formatMessage({ id: 'xpack.ml.jobsList.datafeedStateLabel', defaultMessage: 'Datafeed state' @@ -271,6 +278,7 @@ class JobsListUI extends Component { }), truncateText: false, field: 'latestTimestampSortValue', + 'data-test-subj': 'latestTimestamp', sortable: true, render: (time, item) => ( @@ -323,7 +331,7 @@ class JobsListUI extends Component { return ( ({ + 'data-test-subj': `row row-${item.id}` + })} /> ); } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html index 2f577bbba5435..1dc3aea215d93 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job/wizard/steps/job_type/job_type.html @@ -231,11 +231,10 @@
-
+
diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/index.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/index.ts index eca52c064ce67..850f3259c582a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/index.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/index.ts @@ -9,6 +9,7 @@ export { SingleMetricJobCreator } from './single_metric_job_creator'; export { MultiMetricJobCreator } from './multi_metric_job_creator'; export { PopulationJobCreator } from './population_job_creator'; export { + JobCreatorType, isSingleMetricJobCreator, isMultiMetricJobCreator, isPopulationJobCreator, diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/type_guards.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/type_guards.ts index 9bba4981ea078..194d30bb69026 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/type_guards.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/type_guards.ts @@ -9,20 +9,22 @@ import { MultiMetricJobCreator } from './multi_metric_job_creator'; import { PopulationJobCreator } from './population_job_creator'; import { JOB_TYPE } from './util/constants'; +export type JobCreatorType = SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator; + export function isSingleMetricJobCreator( - jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator + jobCreator: JobCreatorType ): jobCreator is SingleMetricJobCreator { return jobCreator.type === JOB_TYPE.SINGLE_METRIC; } export function isMultiMetricJobCreator( - jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator + jobCreator: JobCreatorType ): jobCreator is MultiMetricJobCreator { return jobCreator.type === JOB_TYPE.MULTI_METRIC; } export function isPopulationJobCreator( - jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator + jobCreator: JobCreatorType ): jobCreator is PopulationJobCreator { return jobCreator.type === JOB_TYPE.POPULATION; } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/general.ts index c059b04a8f4c4..acfc9fceb4e29 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/general.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_creator/util/general.ts @@ -13,14 +13,7 @@ import { } from '../../../../../../common/constants/aggregation_types'; import { EVENT_RATE_FIELD_ID } from '../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; -import { - JobCreator, - SingleMetricJobCreator, - MultiMetricJobCreator, - PopulationJobCreator, - isMultiMetricJobCreator, - isPopulationJobCreator, -} from '../'; +import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../'; import { CREATED_BY_LABEL, JOB_TYPE } from './constants'; // populate the detectors with Field and Agg objects loaded from the job capabilities service @@ -133,7 +126,7 @@ export function isSparseDataJob(job: Job, datafeed: Datafeed): boolean { } function stashCombinedJob( - jobCreator: JobCreator, + jobCreator: JobCreatorType, skipTimeRangeStep: boolean = false, advanced: boolean = false ) { @@ -152,7 +145,7 @@ function stashCombinedJob( } } -export function convertToMultiMetricJob(jobCreator: JobCreator) { +export function convertToMultiMetricJob(jobCreator: JobCreatorType) { jobCreator.createdBy = CREATED_BY_LABEL.MULTI_METRIC; stashCombinedJob(jobCreator, true, false); @@ -162,9 +155,7 @@ export function convertToMultiMetricJob(jobCreator: JobCreator) { ); } -export function convertToAdvancedJob( - jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator -) { +export function convertToAdvancedJob(jobCreator: JobCreatorType) { jobCreator.createdBy = null; stashCombinedJob(jobCreator, false, true); @@ -178,7 +169,7 @@ export function convertToAdvancedJob( window.location.href = window.location.href.replace(jobType, JOB_TYPE.ADVANCED); } -export function resetJob(jobCreator: JobCreator) { +export function resetJob(jobCreator: JobCreatorType) { jobCreator.jobId = ''; stashCombinedJob(jobCreator, true, false); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts index a11b1aadfe6ae..b801494199290 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_runner/job_runner.ts @@ -12,6 +12,9 @@ import { DatafeedId, JobId } from '../job_creator/configs'; import { DATAFEED_STATE } from '../../../../../common/constants/states'; const REFRESH_INTERVAL_MS = 100; +const TARGET_PROGRESS_DELTA = 2; +const REFRESH_RATE_ADJUSTMENT_DELAY_MS = 2000; + type Progress = number; export type ProgressSubscriber = (progress: number) => void; @@ -30,6 +33,9 @@ export class JobRunner { }; private _subscribers: ProgressSubscriber[]; + private _datafeedStartTime: number = 0; + private _performRefreshRateAdjustment: boolean = false; + constructor(jobCreator: JobCreator) { this._jobId = jobCreator.jobId; this._datafeedId = jobCreator.datafeedId; @@ -71,6 +77,7 @@ export class JobRunner { pollProgress: boolean ): Promise { try { + this._datafeedStartTime = Date.now(); // link the _subscribers list from the JobCreator // to the progress BehaviorSubject. const subscriptions = @@ -90,6 +97,7 @@ export class JobRunner { const check = async () => { const { isRunning, progress } = await this.getProgress(); + this._adjustRefreshInterval(progress); this._percentageComplete = progress; this._progress$.next(this._percentageComplete); @@ -118,6 +126,33 @@ export class JobRunner { } } + private _adjustRefreshInterval(progress: number) { + if (this._performRefreshRateAdjustment === false) { + // for the first couple of seconds of the job running, don't + // adjust the refresh interval + const timeDeltaMs = Date.now() - this._datafeedStartTime; + if (timeDeltaMs > REFRESH_RATE_ADJUSTMENT_DELAY_MS) { + this._performRefreshRateAdjustment = true; + } else { + return; + } + } + + const progressDelta = progress - this._percentageComplete; + if (progressDelta !== 0) { + // adjust the refresh interval so that it produces a change in percentage + // that is close to the target + this._refreshInterval = Math.floor( + this._refreshInterval * (TARGET_PROGRESS_DELTA / progressDelta) + ); + + // don't let the interval fall below the initial default. + if (this._refreshInterval < REFRESH_INTERVAL_MS) { + this._refreshInterval = REFRESH_INTERVAL_MS; + } + } + } + public async startDatafeed() { return await this._startDatafeed(this._start, this._end, true); } diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts index 2d5680f1a61a0..669dc4d231aeb 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/job_validator/job_validator.ts @@ -6,7 +6,7 @@ import { basicJobValidation } from '../../../../../common/util/job_utils'; import { newJobLimits } from '../../../new_job/utils/new_job_defaults'; -import { JobCreator } from '../job_creator'; +import { JobCreatorType } from '../job_creator'; import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; @@ -34,7 +34,7 @@ export interface BasicValidations { } export class JobValidator { - private _jobCreator: JobCreator; + private _jobCreator: JobCreatorType; private _validationSummary: ValidationSummary; private _lastJobConfig: string; private _validateTimeout: NodeJS.Timeout; @@ -47,7 +47,7 @@ export class JobValidator { duplicateDetectors: { valid: true }, }; - constructor(jobCreator: JobCreator, existingJobsAndGroups: ExistingJobsAndGroups) { + constructor(jobCreator: JobCreatorType, existingJobsAndGroups: ExistingJobsAndGroups) { this._jobCreator = jobCreator; this._lastJobConfig = this._jobCreator.formattedJobJson; this._validationSummary = { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts index 76bc030b4cad5..0daf23ca7f3a8 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/common/results_loader/results_loader.ts @@ -6,13 +6,7 @@ import { BehaviorSubject } from 'rxjs'; import { parseInterval } from 'ui/utils/parse_interval'; -import { - SingleMetricJobCreator, - MultiMetricJobCreator, - isMultiMetricJobCreator, - PopulationJobCreator, - isPopulationJobCreator, -} from '../job_creator'; +import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../job_creator'; import { mlResultsService, ModelPlotOutputResults } from '../../../../services/results_service'; import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; import { getSeverityType } from '../../../../../common/util/anomaly_utils'; @@ -57,12 +51,10 @@ const LAST_UPDATE_DELAY_MS = 500; export type ResultsSubscriber = (results: Results) => void; -type AnyJobCreator = SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator; - export class ResultsLoader { private _results$: BehaviorSubject; private _resultsSearchRunning = false; - private _jobCreator: AnyJobCreator; + private _jobCreator: JobCreatorType; private _chartInterval: MlTimeBuckets; private _lastModelTimeStamp: number = 0; private _lastResultsTimeout: any = null; @@ -77,7 +69,7 @@ export class ResultsLoader { private _detectorSplitFieldFilters: SplitFieldWithValue | null = null; private _splitFieldFiltersLoaded: boolean = false; - constructor(jobCreator: AnyJobCreator, chartInterval: MlTimeBuckets, chartLoader: ChartLoader) { + constructor(jobCreator: JobCreatorType, chartInterval: MlTimeBuckets, chartLoader: ChartLoader) { this._jobCreator = jobCreator; this._chartInterval = chartInterval; this._results$ = new BehaviorSubject(this._results); diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts index 08d94a3a2b4f0..bcc84704e14cd 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/charts/common/settings.ts @@ -8,9 +8,7 @@ import chrome from 'ui/chrome'; import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { - SingleMetricJobCreator, - MultiMetricJobCreator, - PopulationJobCreator, + JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator, } from '../../../../common/job_creator'; @@ -59,10 +57,7 @@ export const seriesStyle = { }, }; -export function getChartSettings( - jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator, - chartInterval: MlTimeBuckets -) { +export function getChartSettings(jobCreator: JobCreatorType, chartInterval: MlTimeBuckets) { const cs = { ...defaultChartSettings, intervalMs: chartInterval.getInterval().asMilliseconds(), diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts index 8e242adc6fee7..76f9b66f42305 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/job_creator_context.ts @@ -7,11 +7,7 @@ import { createContext } from 'react'; import { Field, Aggregation } from '../../../../../common/types/fields'; import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; -import { - SingleMetricJobCreator, - MultiMetricJobCreator, - PopulationJobCreator, -} from '../../common/job_creator'; +import { JobCreatorType, SingleMetricJobCreator } from '../../common/job_creator'; import { ChartLoader } from '../../common/chart_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; @@ -20,7 +16,7 @@ import { ExistingJobsAndGroups } from '../../../../services/job_service'; export interface JobCreatorContextValue { jobCreatorUpdated: number; jobCreatorUpdate: () => void; - jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator; + jobCreator: JobCreatorType; chartLoader: ChartLoader; resultsLoader: ResultsLoader; chartInterval: MlTimeBuckets; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/json_flyout.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/json_flyout.tsx index 7d212078ac690..1658d28b352b2 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/json_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/summary_step/json_flyout.tsx @@ -17,11 +17,11 @@ import { EuiFlyoutBody, EuiSpacer, } from '@elastic/eui'; -import { JobCreator } from '../../../common/job_creator'; +import { JobCreatorType } from '../../../common/job_creator'; import { MLJobEditor } from '../../../../jobs_list/components/ml_job_editor'; interface Props { - jobCreator: JobCreator; + jobCreator: JobCreatorType; closeFlyout: () => void; } export const JsonFlyout: FC = ({ jobCreator, closeFlyout }) => { diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx index 5a00171e6ebed..a6ce188ddbad9 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/new_job/wizard.tsx @@ -19,22 +19,19 @@ import { JobDetailsStep } from '../components/job_details_step'; import { ValidationStep } from '../components/validation_step'; import { SummaryStep } from '../components/summary_step'; import { MlTimeBuckets } from '../../../../util/ml_time_buckets'; +import { useKibanaContext } from '../../../../contexts/kibana'; import { JobCreatorContext, JobCreatorContextValue } from '../components/job_creator_context'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; -import { - SingleMetricJobCreator, - MultiMetricJobCreator, - PopulationJobCreator, -} from '../../common/job_creator'; +import { JobCreatorType } from '../../common/job_creator'; import { ChartLoader } from '../../common/chart_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; import { newJobCapsService } from '../../../../services/new_job_capabilities_service'; interface Props { - jobCreator: SingleMetricJobCreator | MultiMetricJobCreator | PopulationJobCreator; + jobCreator: JobCreatorType; chartLoader: ChartLoader; resultsLoader: ResultsLoader; chartInterval: MlTimeBuckets; @@ -52,6 +49,7 @@ export const Wizard: FC = ({ existingJobsAndGroups, skipTimeRangeStep = false, }) => { + const kibanaContext = useKibanaContext(); const [jobCreatorUpdated, setJobCreatorUpdate] = useReducer<(s: number) => number>(s => s + 1, 0); const jobCreatorUpdate = () => setJobCreatorUpdate(jobCreatorUpdated); @@ -177,6 +175,24 @@ export const Wizard: FC = ({ }, ]; + function getSummaryStepTitle() { + if (kibanaContext.currentSavedSearch.id !== undefined) { + return i18n.translate('xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleSavedSearch', { + defaultMessage: 'New job from saved search {title}', + values: { title: kibanaContext.currentSavedSearch.title }, + }); + } else if (kibanaContext.currentIndexPattern.id !== undefined) { + return i18n.translate( + 'xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleIndexPattern', + { + defaultMessage: 'New job from index pattern {title}', + values: { title: kibanaContext.currentIndexPattern.title }, + } + ); + } + return ''; + } + return ( @@ -243,12 +259,7 @@ export const Wizard: FC = ({ )} {currentStep === WIZARD_STEPS.SUMMARY && ( - - <FormattedMessage - id="xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitle" - defaultMessage="Summary" - /> - + {getSummaryStepTitle()} { diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js index 5441d6ccd74c0..ab29e0ba303a9 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js @@ -22,7 +22,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiButton, EuiSelect, EuiSpacer, EuiText, @@ -170,10 +169,14 @@ export class TimeSeriesExplorer extends React.Component { detectorIndexChangeHandler = (e) => { const id = e.target.value; if (id !== undefined) { - this.setState({ detectorId: id }); + this.setState({ detectorId: id }, () => { + this.updateControlsForDetector( + () => this.loadEntityValues( + () => this.saveSeriesPropertiesAndRefresh() + ) + ); + }); } - this.updateControlsForDetector(); - this.loadEntityValues(); }; toggleShowAnnotationsHandler = () => { @@ -322,7 +325,7 @@ export class TimeSeriesExplorer extends React.Component { } return stateEntity; }) - })); + }), () => this.saveSeriesPropertiesAndRefresh()); }; loadAnomaliesTableData = (earliestMs, latestMs) => { @@ -378,7 +381,7 @@ export class TimeSeriesExplorer extends React.Component { }); } - loadEntityValues = () => { + loadEntityValues = (callback = () => {}) => { const { timefilter } = this.props; const { detectorId, entities, selectedJob } = this.state; @@ -411,7 +414,7 @@ export class TimeSeriesExplorer extends React.Component { } return entity; }) - }); + }, callback); } }); } @@ -464,6 +467,10 @@ export class TimeSeriesExplorer extends React.Component { } refresh = () => { + if (this.state.loading) { + return; + } + const { appStateHandler, timefilter } = this.props; const { detectorId: currentDetectorId, @@ -653,7 +660,7 @@ export class TimeSeriesExplorer extends React.Component { }); } - updateControlsForDetector = () => { + updateControlsForDetector = (callback = () => {}) => { const { appStateHandler } = this.props; const { detectorId, selectedJob } = this.state; // Update the entity dropdown control(s) according to the partitioning fields for the selected detector. @@ -684,7 +691,7 @@ export class TimeSeriesExplorer extends React.Component { entities.push({ fieldName: byFieldName, fieldValue: byFieldValue }); } - this.setState({ entities }); + this.setState({ entities }, callback); } loadForJobId(jobId, jobs) { @@ -739,16 +746,16 @@ export class TimeSeriesExplorer extends React.Component { this.setState( { detectorId, detectors, selectedJob }, () => { - this.updateControlsForDetector(); - - // Populate the map of jobs / detectors / field formatters for the selected IDs and refresh. - mlFieldFormatService.populateFormats([jobId], getIndexPatterns()) - .catch((err) => { console.log('Error populating field formats:', err); }) + this.updateControlsForDetector(() => { + // Populate the map of jobs / detectors / field formatters for the selected IDs and refresh. + mlFieldFormatService.populateFormats([jobId], getIndexPatterns()) + .catch((err) => { console.log('Error populating field formats:', err); }) // Load the data - if the FieldFormats failed to populate // the default formatting will be used for metric values. - .then(() => { - this.refresh(); - }); + .then(() => { + this.refresh(); + }); + }); } ); } @@ -1024,19 +1031,6 @@ export class TimeSeriesExplorer extends React.Component { /> ); })} - - - - {i18n.translate('xpack.ml.timeSeriesExplorer.refreshButtonAriaLabel', { - defaultMessage: 'Refresh' - })} - - - { - message$.subscribe((line: string) => { - logger.debug(line, ['browser']); - }); - consoleMessage$.subscribe((line: string) => { - logger.debug(line, ['browserConsole']); - }); + mergeMap(({ driver$, exit$ }) => { const screenshot$ = driver$.pipe( mergeMap( (browser: HeadlessBrowser) => openUrl(browser, url, conditionalHeaders, logger), diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/__tests__/execute_job.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/__tests__/execute_job.js index c39bb4ac2dde1..3d2c7c3b0ae3c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/__tests__/execute_job.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/__tests__/execute_job.js @@ -10,9 +10,9 @@ import sinon from 'sinon'; import nodeCrypto from '@elastic/node-crypto'; import { CancellationToken } from '../../../../common/cancellation_token'; -import { FieldFormat } from '../../../../../../../../src/legacy/ui/field_formats/field_format.js'; -import { FieldFormatsService } from '../../../../../../../../src/legacy/ui/field_formats/field_formats_service.js'; -import { createStringFormat } from '../../../../../../../../src/legacy/core_plugins/kibana/common/field_formats/types/string.js'; +import { FieldFormat } from '../../../../../../../../src/legacy/ui/field_formats/field_format'; +import { FieldFormatsService } from '../../../../../../../../src/legacy/ui/field_formats/mixin/field_formats_service'; +import { createStringFormat } from '../../../../../../../../src/legacy/core_plugins/kibana/common/field_formats/types/string'; import { executeJobFactory } from '../execute_job'; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js index 164f2f96655e7..acba46c5dd31b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js @@ -6,10 +6,10 @@ import expect from '@kbn/expect'; -import { FieldFormat } from '../../../../../../../../../src/legacy/ui/field_formats/field_format.js'; -import { FieldFormatsService } from '../../../../../../../../../src/legacy/ui/field_formats/field_formats_service.js'; -import { createBytesFormat } from '../../../../../../../../../src/legacy/core_plugins/kibana/common/field_formats/types/bytes.js'; -import { createNumberFormat } from '../../../../../../../../../src/legacy/core_plugins/kibana/common/field_formats/types/number.js'; +import { FieldFormat } from '../../../../../../../../../src/legacy/ui/field_formats/field_format'; +import { FieldFormatsService } from '../../../../../../../../../src/legacy/ui/field_formats/mixin/field_formats_service'; +import { createBytesFormat } from '../../../../../../../../../src/legacy/core_plugins/kibana/common/field_formats/types/bytes'; +import { createNumberFormat } from '../../../../../../../../../src/legacy/core_plugins/kibana/common/field_formats/types/number'; import { fieldFormatMapFactory } from '../field_format_map'; diff --git a/x-pack/legacy/plugins/reporting/index.js b/x-pack/legacy/plugins/reporting/index.js index 6acf77f1dc726..6920028e4573d 100644 --- a/x-pack/legacy/plugins/reporting/index.js +++ b/x-pack/legacy/plugins/reporting/index.js @@ -130,6 +130,11 @@ export const reporting = (kibana) => { }).default(), maxScreenshotDimension: Joi.number().integer().default(1950) }).default() + }).default(), + maxAttempts: Joi.number().integer().greater(0).when('$dist', { + is: true, + then: Joi.default(3), + otherwise: Joi.default(1), }).default() }).default(), csv: Joi.object({ diff --git a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index cc182b817339f..c1812cb40cf23 100644 --- a/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/legacy/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -15,7 +15,6 @@ import { EuiIcon } from '@elastic/eui'; import { Action, - ActionContext, ViewMode, IncompatibleActionError, IEmbeddable, @@ -37,7 +36,12 @@ function isSavedSearchEmbeddable( ): embeddable is ISearchEmbeddable { return embeddable.type === SEARCH_EMBEDDABLE_TYPE; } -class GetCsvReportPanelAction extends Action { + +interface ActionContext { + embeddable: ISearchEmbeddable; +} + +class GetCsvReportPanelAction extends Action { private isDownloading: boolean; public readonly type = CSV_REPORTING_ACTION; @@ -82,7 +86,7 @@ class GetCsvReportPanelAction extends Action { return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search'; }; - public execute = async (context: ActionContext) => { + public execute = async (context: ActionContext) => { const { embeddable } = context; if (!isSavedSearchEmbeddable(embeddable)) { diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 6df8b37dd1d30..01454f2a9fae0 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -6,10 +6,16 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { Browser, Page, LaunchOptions } from 'puppeteer'; +import { + Browser, + Page, + LaunchOptions, + ConsoleMessage, + Request as PuppeteerRequest, +} from 'puppeteer'; import rimraf from 'rimraf'; import * as Rx from 'rxjs'; -import { map, share, mergeMap, filter, partition, ignoreElements, tap } from 'rxjs/operators'; +import { ignoreElements, mergeMap, tap } from 'rxjs/operators'; import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { puppeteerLaunch } from '../puppeteer'; @@ -25,10 +31,6 @@ interface IBrowserConfig { [key: string]: any; } -const compactWhitespace = (str: string) => { - return str.replace(/\s+/, ' '); -}; - export class HeadlessChromiumDriverFactory { private binaryPath: binaryPath; private logger: Logger; @@ -71,9 +73,10 @@ export class HeadlessChromiumDriverFactory { TZ: browserTimezone, }, } as LaunchOptions).catch((error: Error) => { - logger.warning( - `The Reporting plugin encountered issues launching Chromium in a self-test. You may have trouble generating reports: [${error}]` + logger.error( + `The Reporting plugin encountered issues launching Chromium in a self-test. You may have trouble generating reports.` ); + logger.error(error); logger.warning(`See Chromium's log output at "${getChromeLogLocation(this.binaryPath)}"`); return null; }); @@ -87,8 +90,6 @@ export class HeadlessChromiumDriverFactory { browserTimezone: string; }): Rx.Observable<{ driver$: Rx.Observable; - consoleMessage$: Rx.Observable; - message$: Rx.Observable; exit$: Rx.Observable; }> { return Rx.Observable.create(async (observer: InnerSubscriber) => { @@ -157,104 +158,24 @@ export class HeadlessChromiumDriverFactory { .subscribe(observer) ); - // Register with a few useful puppeteer event handlers: - // https://pptr.dev/#?product=Puppeteer&version=v1.10.0&show=api-event-error - // https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page - - const stderr$ = Rx.fromEvent(page as NodeJS.EventEmitter, 'console').pipe( - filter((line: any) => line._type === 'error'), - map((line: any) => line._text), - share() - ); - - const [consoleMessage$, message$] = partition( - (msg: string) => !!msg.match(/\[\d+\/\d+.\d+:\w+:CONSOLE\(\d+\)\]/) - )(stderr$); - - const driver$ = Rx.of( - new HeadlessChromiumDriver(page, { inspect: this.browserConfig.inspect }) - ); + // taps the browser log streams and combine them to Kibana logs + this.getBrowserLogger(page).subscribe(); + this.getProcessLogger(browser).subscribe(); - const processError$ = Rx.fromEvent(page, 'error').pipe( - mergeMap(err => Rx.throwError(new Error(`Unable to spawn Chromium: [${err}]`))) - ); + const driver$ = Rx.of(new HeadlessChromiumDriver(page, { inspect: this.browserConfig.inspect })); // prettier-ignore - const processPageError$ = Rx.fromEvent(page, 'pageerror').pipe( - mergeMap(err => Rx.throwError(new Error(`Uncaught exception within the page: [${err}]`))) - ); + const exit$ = this.getPageExit(browser, page); - const processRequestFailed$ = Rx.fromEvent(page, 'requestfailed').pipe( - mergeMap((req: any) => { - const failure = req.failure && req.failure(); - if (failure) { - return Rx.throwError( - new Error(`Request to [${req.url()}] failed! [${failure.errorText}]`) - ); - } - return Rx.throwError(new Error(`Unknown failure! [${JSON.stringify(req)}]`)); - }) - ); - - const processExit$ = Rx.fromEvent(browser, 'disconnected').pipe( - mergeMap(code => - Rx.throwError(new Error(`Chromium exited with: [${JSON.stringify({ code })}]`)) - ) - ); - - const nssError$ = message$.pipe( - filter((line: string) => line.includes('error while loading shared libraries: libnss3.so')), - mergeMap(() => Rx.throwError(new Error(`You must install nss for Reporting to work`))) - ); - - const fontError$ = message$.pipe( - filter((line: string) => - line.includes('Check failed: InitDefaultFont(). Could not find the default font') - ), - mergeMap(() => - Rx.throwError(new Error('You must install freetype and ttf-font for Reporting to work')) - ) - ); + observer.next({ driver$, exit$ }); - const noUsableSandbox$ = message$.pipe( - filter((line: string) => line.includes('No usable sandbox! Update your kernel')), - mergeMap(() => - Rx.throwError( - new Error( - compactWhitespace(` - Unable to use Chromium sandbox. This can be disabled at your own risk with - 'xpack.reporting.capture.browser.chromium.disableSandbox' - `) - ) - ) - ) - ); - - const exit$ = Rx.merge( - processError$, - processPageError$, - processRequestFailed$, - processExit$, - nssError$, - fontError$, - noUsableSandbox$ - ); - - observer.next({ - driver$, - consoleMessage$, - message$, - exit$, - }); - - const factoryLogger = this.logger.clone(['chromium-driver-factory']); // unsubscribe logic makes a best-effort attempt to delete the user data directory used by chromium observer.add(() => { - factoryLogger.debug(`deleting chromium user data directory at [${userDataDir}]`); + this.logger.debug(`deleting chromium user data directory at [${userDataDir}]`); // the unsubscribe function isn't `async` so we're going to make our best effort at // deleting the userDataDir and if it fails log an error. rimraf(userDataDir, err => { if (err) { - return factoryLogger.error( + return this.logger.error( `error deleting user data directory at [${userDataDir}]: [${err}]` ); } @@ -262,4 +183,71 @@ export class HeadlessChromiumDriverFactory { }); }); } + + getBrowserLogger(page: Page): Rx.Observable { + return Rx.fromEvent(page as NodeJS.EventEmitter, 'console').pipe( + tap((line: ConsoleMessage) => { + if (line.type() === 'error') { + this.logger.error(line.text(), ['headless-browser-console']); + } else { + this.logger.debug(line.text(), [line.type(), 'headless-browser-console']); + } + }) + ); + } + + getProcessLogger(browser: Browser): Rx.Observable { + const childProcess = browser.process(); + // NOTE: The browser driver can not observe stdout and stderr of the child process + // Puppeteer doesn't give a handle to the original ChildProcess object + // See https://github.com/GoogleChrome/puppeteer/issues/1292#issuecomment-521470627 + + // just log closing of the process + const processClose$: Rx.Observable = Rx.fromEvent(childProcess, 'close').pipe( + tap(() => { + this.logger.debug('child process closed', ['headless-browser-process']); + }) + ); + + return processClose$; // ideally, this would also merge with observers for stdout and stderr + } + + getPageExit(browser: Browser, page: Page): Rx.Observable { + const pageError$: Rx.Observable = Rx.fromEvent(page, 'error').pipe( + mergeMap((err: Error) => Rx.throwError(err)) + ); + + const uncaughtExceptionPageError$: Rx.Observable = Rx.fromEvent(page, 'pageerror').pipe( + mergeMap((err: Error) => Rx.throwError(err)) + ); + + const pageRequestFailed$: Rx.Observable = Rx.fromEvent(page, 'requestfailed').pipe( + mergeMap((req: PuppeteerRequest) => { + const failure = req.failure && req.failure(); + if (failure) { + return Rx.throwError( + new Error(`Request to [${req.url()}] failed! [${failure.errorText}]`) + ); + } + return Rx.throwError(new Error(`Unknown failure!`)); + }) + ); + + const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( + mergeMap(() => + Rx.throwError( + new Error( + `Puppeteer was disconnected from the Chromium instance! Chromium has closed or crashed.` + ) + ) + ) + ); + + return Rx.merge( + pageError$, + uncaughtExceptionPageError$, + pageRequestFailed$, + browserDisconnect$ + ); + } } diff --git a/x-pack/legacy/plugins/reporting/server/browsers/chromium/paths.ts b/x-pack/legacy/plugins/reporting/server/browsers/chromium/paths.ts index fee621d293c73..8e3ab55a71f35 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/chromium/paths.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/chromium/paths.ts @@ -14,21 +14,21 @@ export const paths = { platforms: ['darwin', 'freebsd', 'openbsd'], archiveFilename: 'chromium-312d84c-darwin.zip', archiveChecksum: '020303e829745fd332ae9b39442ce570', - rawChecksum: '101dfea297c5818a7a3f3317a99dde02', + binaryChecksum: '5cdec11d45a0eddf782bed9b9f10319f', binaryRelativePath: 'headless_shell-darwin/headless_shell', }, { platforms: ['linux'], archiveFilename: 'chromium-312d84c-linux.zip', archiveChecksum: '15ba9166a42f93ee92e42217b737018d', - rawChecksum: '3455db62ea4bd2d6e891e9155313305a', + binaryChecksum: 'c7fe36ed3e86a6dd23323be0a4e8c0fd', binaryRelativePath: 'headless_shell-linux/headless_shell', }, { platforms: ['win32'], archiveFilename: 'chromium-312d84c-windows.zip', archiveChecksum: '3e36adfb755dacacc226ed5fd6b43105', - rawChecksum: 'ec7aa6cfecb172129474b447311275ec', + binaryChecksum: '9913e431fbfc7dfcd958db74ace4d58b', binaryRelativePath: 'headless_shell-windows\\headless_shell.exe', }, ], diff --git a/x-pack/legacy/plugins/reporting/server/browsers/install.ts b/x-pack/legacy/plugins/reporting/server/browsers/install.ts index ed2fc3df459b8..0f2ab28d2b75e 100644 --- a/x-pack/legacy/plugins/reporting/server/browsers/install.ts +++ b/x-pack/legacy/plugins/reporting/server/browsers/install.ts @@ -38,9 +38,9 @@ export async function installBrowser( } const binaryPath = path.join(installsPath, pkg.binaryRelativePath); - const rawChecksum = await md5(binaryPath).catch(() => ''); + const binaryChecksum = await md5(binaryPath).catch(() => ''); - if (rawChecksum !== pkg.rawChecksum) { + if (binaryChecksum !== pkg.binaryChecksum) { const archive = path.join(browser.paths.archivesPath, pkg.archiveFilename); logger.debug(`Extracting [${archive}] to [${binaryPath}]`); await extract(archive, installsPath); diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 9c202b4d6ce44..98124e33a8fc5 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -23,6 +23,7 @@ function enqueueJobFn(server: KbnServer) { const config = server.config(); const queueConfig = config.get('xpack.reporting.queue'); const browserType = config.get('xpack.reporting.capture.browser.type'); + const maxAttempts = config.get('xpack.reporting.capture.maxAttempts'); const exportTypesRegistry = server.plugins.reporting.exportTypesRegistry; return async function enqueueJob( @@ -42,6 +43,7 @@ function enqueueJobFn(server: KbnServer) { timeout: queueConfig.timeout, created_by: get(user, 'username', false), browser_type: browserType, + max_attempts: maxAttempts, }; return new Promise((resolve, reject) => { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js index 233ecb07ca44c..567c8e7da56bc 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/job.js @@ -67,6 +67,11 @@ describe('Job Class', function () { const init = () => new Job(mockQueue, index, 'type1', [1, 2, 3]); expect(init).to.throwException(/plain.+object/i); }); + + it(`should throw error if invalid maxAttempts`, function () { + const init = () => new Job(mockQueue, index, 'type1', { id: '123' }, { max_attempts: -1 }); + expect(init).to.throwException(/invalid.+max_attempts/i); + }); }); describe('construction', function () { diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js index 8eedf08018f18..c470afe604af7 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/job.js @@ -32,6 +32,10 @@ export class Job extends events.EventEmitter { this.indexSettings = options.indexSettings || {}; this.browser_type = options.browser_type; + if (typeof this.maxAttempts !== 'number' || this.maxAttempts < 1) { + throw new Error(`Invalid max_attempts: ${this.maxAttempts}`); + } + this.debug = (msg, err) => { const logger = options.logger || function () {}; const message = `${this.id} - ${msg}`; diff --git a/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts b/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts index 868ae163b15fc..810ccf253943c 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/level_logger.ts @@ -6,6 +6,10 @@ type ServerLog = (tags: string[], msg: string) => void; +const trimStr = (toTrim: string) => { + return typeof toTrim === 'string' ? toTrim.trim() : toTrim; +}; + export class LevelLogger { private _logger: any; private _tags: string[]; @@ -30,20 +34,20 @@ export class LevelLogger { this.warn = this.warning.bind(this); } - public error(msg: string, tags: string[] = []) { - this._logger([...this._tags, ...tags, 'error'], msg); + public error(err: string | Error, tags: string[] = []) { + this._logger([...this._tags, ...tags, 'error'], err); } public warning(msg: string, tags: string[] = []) { - this._logger([...this._tags, ...tags, 'warning'], msg); + this._logger([...this._tags, ...tags, 'warning'], trimStr(msg)); } public debug(msg: string, tags: string[] = []) { - this._logger([...this._tags, ...tags, 'debug'], msg); + this._logger([...this._tags, ...tags, 'debug'], trimStr(msg)); } public info(msg: string, tags: string[] = []) { - this._logger([...this._tags, ...tags, 'info'], msg); + this._logger([...this._tags, ...tags, 'info'], trimStr(msg)); } public get isVerbose() { diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js index 5e2bfce7ada13..ccd4e1c3a82c6 100644 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js +++ b/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js @@ -12,6 +12,12 @@ import { KibanaRequest } from '../../../../../../../../src/core/server'; import { createCSPRuleString } from '../../../../../../../../src/legacy/server/csp'; export function initAuthenticateApi({ authc: { login, logout }, config }, server) { + function prepareCustomResourceResponse(response, contentType) { + return response + .header('cache-control', 'private, no-cache, no-store') + .header('content-security-policy', createCSPRuleString(server.config().get('csp.rules'))) + .type(contentType); + } server.route({ method: 'POST', @@ -93,22 +99,36 @@ export function initAuthenticateApi({ authc: { login, logout }, config }, server path: '/api/security/v1/oidc/implicit', config: { auth: false }, async handler(request, h) { - const legacyConfig = server.config(); - const basePath = legacyConfig.get('server.basePath'); - - const cspRulesHeader = createCSPRuleString(legacyConfig.get('csp.rules')); - return h.response(` - - Kibana OpenID Connect Login - + `), + 'text/html' + ); + } + }); + + /** + * The route that accompanies `/api/security/v1/oidc/implicit` and renders a JavaScript snippet + * that extracts fragment part from the URL and send it to the `/api/security/v1/oidc` route. + * We need this separate endpoint because of default CSP policy that forbids inline scripts. + */ + server.route({ + method: 'GET', + path: '/api/security/v1/oidc/implicit.js', + config: { auth: false }, + async handler(request, h) { + return prepareCustomResourceResponse( + h.response(` window.location.replace( - '${basePath}/api/security/v1/oidc?authenticationResponseURI=' + encodeURIComponent(window.location.href) + '${server.config().get('server.basePath')}/api/security/v1/oidc?authenticationResponseURI=' + + encodeURIComponent(window.location.href) ); - - `) - .header('cache-control', 'private, no-cache, no-store') - .header('content-security-policy', cspRulesHeader) - .type('text/html'); + `), + 'text/javascript' + ); } }); diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts index f1a762cfab79b..30949abfe315f 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/hosts/selectors.ts @@ -15,3 +15,7 @@ export const ALL_HOSTS_WIDGET_DRAGGABLE_HOSTS = `${ALL_HOSTS_WIDGET} ${ALL_HOSTS /** Clicking this button displays the `Events` tab */ export const EVENTS_TAB_BUTTON = '[data-test-subj="navigation-events"]'; + +export const NAVIGATION_HOSTS_ALL_HOSTS = '[data-test-subj="navigation-link-allHosts"]'; + +export const NAVIGATION_HOSTS_ANOMALIES = '[data-test-subj="navigation-link-anomalies"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts index 77f8e4786b2ed..2d89eabda20ff 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/url_state/index.ts @@ -37,11 +37,13 @@ export const ABSOLUTE_DATE_RANGE = { '/app/siem#/network/?kqlQuery=(filterQuery:!n,queryLocation:network.page)&timerange=(global:(linkTo:!(),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(),timerange:(from:1564776209186,kind:absolute,to:1564779809186)))', urlKqlNetworkNetwork: `/app/siem#/network/?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:network.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, urlKqlNetworkHosts: `/app/siem#/network/?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsNetwork: `/app/siem#/hosts/?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:network.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsHosts: `/app/siem#/hosts/?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlHostsNetwork: `/app/siem#/hosts/allHosts?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:network.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlHostsHosts: `/app/siem#/hosts/allHosts?_g=()&kqlQuery=(filterQuery:(expression:'source.ip:%20"10.142.0.9"',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlHost: + '/app/siem#/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', }; export const DATE_PICKER_START_DATE_POPOVER_BUTTON = - '[data-test-subj="globalDatePicker"] [data-test-subj="superDatePickerstartDatePopoverButton"]'; + 'div[data-test-subj="globalDatePicker"] button[data-test-subj="superDatePickerstartDatePopoverButton"]'; export const DATE_PICKER_END_DATE_POPOVER_BUTTON = '[data-test-subj="globalDatePicker"] [data-test-subj="superDatePickerendDatePopoverButton"]'; export const DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE = @@ -56,3 +58,6 @@ export const DATE_PICKER_APPLY_BUTTON_TIMELINE = export const DATE_PICKER_ABSOLUTE_INPUT = '[data-test-subj="superDatePickerAbsoluteDateInput"]'; export const KQL_INPUT = '[data-test-subj="kqlInput"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; + +export const HOST_DETAIL_SIEM_KIBANA = '[data-test-subj="all-hosts"] a.euiLink'; +export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts index 25cff28836a7c..1d07d2693cfa8 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/urls/index.ts @@ -5,7 +5,7 @@ */ /** The SIEM app's Hosts page */ -export const HOSTS_PAGE = '/app/siem#/hosts'; +export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; /** Kibana's login page */ export const LOGIN_PAGE = '/login'; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts index 5d5feca99457a..41beb9c762c83 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/url_state/url_state.spec.ts @@ -17,6 +17,8 @@ import { DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE, KQL_INPUT, TIMELINE_TITLE, + HOST_DETAIL_SIEM_KIBANA, + BREADCRUMBS, } from '../../lib/url_state'; import { DEFAULT_TIMEOUT, loginAndWaitForPage } from '../../lib/util/helpers'; import { @@ -25,8 +27,10 @@ import { hostExistsQuery, toggleTimelineVisibility, } from '../../lib/timeline/helpers'; -import { NAVIGATION_NETWORK } from '../../lib/navigation/selectors'; +import { NAVIGATION_NETWORK, NAVIGATION_HOSTS } from '../../lib/navigation/selectors'; import { HOSTS_PAGE } from '../../lib/urls'; +import { waitForAllHostsWidget } from '../../lib/hosts/helpers'; +import { NAVIGATION_HOSTS_ALL_HOSTS, NAVIGATION_HOSTS_ANOMALIES } from '../../lib/hosts/selectors'; describe('url state', () => { afterEach(() => { @@ -190,6 +194,65 @@ describe('url state', () => { ); }); + it('sets the url state when kql is set and check if href reflect this change', () => { + loginAndWaitForPage(ABSOLUTE_DATE_RANGE.url); + cy.get(KQL_INPUT, { timeout: 5000 }).type('source.ip: "10.142.0.9" {enter}'); + cy.get(NAVIGATION_HOSTS) + .first() + .click({ force: true }); + cy.get(NAVIGATION_NETWORK).should( + 'have.attr', + 'href', + "#/link-to/network?kqlQuery=(filterQuery:(expression:'source.ip:%20%2210.142.0.9%22%20',kind:kuery),queryLocation:network.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + }); + + it('sets KQL in host page and detail page and check if href match on breadcrumb, tabs and subTabs', () => { + loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlHost); + cy.get(KQL_INPUT, { timeout: 5000 }).type('host.name: "siem-kibana" {enter}'); + cy.get(NAVIGATION_HOSTS_ALL_HOSTS) + .first() + .click({ force: true }); + waitForAllHostsWidget(); + cy.get(HOST_DETAIL_SIEM_KIBANA, { timeout: 5000 }) + .first() + .invoke('text') + .should('eq', 'siem-kibana'); + cy.get(HOST_DETAIL_SIEM_KIBANA) + .first() + .click({ force: true }); + cy.get(KQL_INPUT, { timeout: 5000 }).type('agent.type: "auditbeat" {enter}'); + cy.get(NAVIGATION_HOSTS).should( + 'have.attr', + 'href', + "#/link-to/hosts?kqlQuery=(filterQuery:(expression:'host.name:%20%22siem-kibana%22%20',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + cy.get(NAVIGATION_NETWORK).should( + 'have.attr', + 'href', + '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))' + ); + cy.get(NAVIGATION_HOSTS_ANOMALIES).should( + 'have.attr', + 'href', + "#/hosts/siem-kibana/anomalies?kqlQuery=(filterQuery:(expression:'agent.type:%20%22auditbeat%22%20',kind:kuery),queryLocation:hosts.details)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + cy.get(BREADCRUMBS) + .eq(1) + .should( + 'have.attr', + 'href', + "#/link-to/hosts?kqlQuery=(filterQuery:(expression:'host.name:%20%22siem-kibana%22%20',kind:kuery),queryLocation:hosts.page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + cy.get(BREADCRUMBS) + .eq(2) + .should( + 'have.attr', + 'href', + "#/link-to/hosts/siem-kibana?kqlQuery=(filterQuery:(expression:'agent.type:%20%22auditbeat%22%20',kind:kuery),queryLocation:hosts.details)&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + ); + }); + it('clears kql when navigating to a new page', () => { loginAndWaitForPage(ABSOLUTE_DATE_RANGE.urlKqlHostsHosts); cy.get(NAVIGATION_NETWORK).click({ force: true }); @@ -202,17 +265,10 @@ describe('url state', () => { executeKQL(hostExistsQuery); assertAtLeastOneEventMatchesSearch(); const bestTimelineName = 'The Best Timeline'; - cy.get(TIMELINE_TITLE).type(bestTimelineName); - cy.hash().then(hash => { - const matched = hash.match(/(?<=timelineId=\').+?(?=\')/g); - const newTimelineId = matched && matched.length > 0 ? matched[0] : 'null'; - expect(matched).to.have.lengthOf(1); - cy.log('hash', hash); - cy.log('matched', matched); - cy.log('newTimelineId', newTimelineId); - cy.visit( - `/app/siem#/timelines?timelineId='${newTimelineId}'&timerange=(global:(linkTo:!(),timerange:(from:1565274377369,kind:absolute,to:1565360777369)),timeline:(linkTo:!(),timerange:(from:1565274377369,kind:absolute,to:1565360777369)))` - ).then(() => cy.get(TIMELINE_TITLE).should('have.attr', 'value', bestTimelineName)); - }); + cy.get(TIMELINE_TITLE, { timeout: 5000 }).type(bestTimelineName); + cy.url().should('include', 'timelineId='); + cy.visit( + `/app/siem#/timelines?timerange=(global:(linkTo:!(),timerange:(from:1565274377369,kind:absolute,to:1565360777369)),timeline:(linkTo:!(),timerange:(from:1565274377369,kind:absolute,to:1565360777369)))` + ).then(() => cy.get(TIMELINE_TITLE).should('have.attr', 'value', bestTimelineName)); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index e6751eeb2de70..8ecd59063093b 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -16,7 +16,7 @@ export const mockSourceLayer = { type: 'ES_SEARCH', geoField: 'source.geo.location', filterByMapBounds: false, - tooltipProperties: ['host.name', 'host.ip'], + tooltipProperties: ['host.name', 'source.ip', 'source.domain', 'source.as.organization.name'], useTopHits: false, topHitsTimeField: '@timestamp', topHitsSize: 1, @@ -51,7 +51,12 @@ export const mockDestinationLayer = { type: 'ES_SEARCH', geoField: 'destination.geo.location', filterByMapBounds: true, - tooltipProperties: ['host.name', 'host.ip'], + tooltipProperties: [ + 'host.name', + 'destination.ip', + 'destination.domain', + 'destination.as.organization.name', + ], useTopHits: false, topHitsTimeField: '@timestamp', topHitsSize: 1, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/actions/apply_siem_filter_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/actions/apply_siem_filter_action.test.tsx index 47efbcce16867..731cc034f9778 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/actions/apply_siem_filter_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/actions/apply_siem_filter_action.test.tsx @@ -6,7 +6,11 @@ import { get } from 'lodash/fp'; -import { ApplySiemFilterAction, getExpressionFromArray } from './apply_siem_filter_action'; +import { + ApplySiemFilterAction, + getExpressionFromArray, + getFilterExpression, +} from './apply_siem_filter_action'; // @ts-ignore Missing type defs as maps moves to Typescript import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/common/constants'; import { Action } from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions'; @@ -25,10 +29,6 @@ const isEmbeddable = ( return get('type', embeddable) != null; }; -const isTriggerContext = (triggerContext: unknown): triggerContext is { filters: Filter[] } => { - return typeof triggerContext === 'object'; -}; - describe('ApplySiemFilterAction', () => { let applyFilterQueryFromKueryExpression: (expression: string) => void; @@ -53,7 +53,7 @@ describe('ApplySiemFilterAction', () => { }); describe('#isCompatible', () => { - test('when embeddable type is MAP_SAVED_OBJECT_TYPE and triggerContext filters exist, returns true', async () => { + test('when embeddable type is MAP_SAVED_OBJECT_TYPE and filters exist, returns true', async () => { const action = new ApplySiemFilterAction({ applyFilterQueryFromKueryExpression }); const embeddable = { type: MAP_SAVED_OBJECT_TYPE, @@ -61,9 +61,7 @@ describe('ApplySiemFilterAction', () => { if (isEmbeddable(embeddable)) { const result = await action.isCompatible({ embeddable, - triggerContext: { - filters: [], - }, + filters: [], }); expect(result).toBe(true); } else { @@ -71,7 +69,7 @@ describe('ApplySiemFilterAction', () => { } }); - test('when embeddable type is MAP_SAVED_OBJECT_TYPE and triggerContext does not exist, returns false', async () => { + test('when embeddable type is MAP_SAVED_OBJECT_TYPE and filters do not exist, returns false', async () => { const action = new ApplySiemFilterAction({ applyFilterQueryFromKueryExpression }); const embeddable = { type: MAP_SAVED_OBJECT_TYPE, @@ -79,30 +77,14 @@ describe('ApplySiemFilterAction', () => { if (isEmbeddable(embeddable)) { const result = await action.isCompatible({ embeddable, - }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); expect(result).toBe(false); } else { throw new Error('Invalid embeddable in unit test'); } }); - test('when embeddable type is MAP_SAVED_OBJECT_TYPE and triggerContext filters do not exist, returns false', async () => { - const action = new ApplySiemFilterAction({ applyFilterQueryFromKueryExpression }); - const embeddable = { - type: MAP_SAVED_OBJECT_TYPE, - }; - const triggerContext = {}; - if (isEmbeddable(embeddable) && isTriggerContext(triggerContext)) { - const result = await action.isCompatible({ - embeddable, - triggerContext, - }); - expect(result).toBe(false); - } else { - throw new Error('Invalid embeddable/triggerContext in unit test'); - } - }); - test('when embeddable type is not MAP_SAVED_OBJECT_TYPE, returns false', async () => { const action = new ApplySiemFilterAction({ applyFilterQueryFromKueryExpression }); const embeddable = { @@ -111,9 +93,7 @@ describe('ApplySiemFilterAction', () => { if (isEmbeddable(embeddable)) { const result = await action.isCompatible({ embeddable, - triggerContext: { - filters: [], - }, + filters: [], }); expect(result).toBe(false); } else { @@ -132,7 +112,8 @@ describe('ApplySiemFilterAction', () => { const error = expectError(() => action.execute({ embeddable, - }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any) ); expect(error).toBeInstanceOf(Error); } else { @@ -148,24 +129,27 @@ describe('ApplySiemFilterAction', () => { query: { query: '' }, }), }; - const triggerContext = { - filters: [ - { - query: { - match: { - 'host.name': { - query: 'zeek-newyork-sha-aa8df15', - type: 'phrase', - }, + const filters: Filter[] = [ + { + query: { + match: { + 'host.name': { + query: 'zeek-newyork-sha-aa8df15', + type: 'phrase', }, }, }, - ], - }; - if (isEmbeddable(embeddable) && isTriggerContext(triggerContext)) { + meta: { + disabled: false, + negate: false, + alias: '', + }, + }, + ]; + if (isEmbeddable(embeddable)) { await action.execute({ embeddable, - triggerContext, + filters, }); expect( @@ -173,12 +157,34 @@ describe('ApplySiemFilterAction', () => { .calls[0][0] ).toBe('host.name: "zeek-newyork-sha-aa8df15"'); } else { - throw new Error('Invalid embeddable/triggerContext in unit test'); + throw new Error('Invalid embeddable in unit test'); } }); }); }); +describe('#getFilterExpression', () => { + test('it returns an empty expression if no filterValue is provided', () => { + const layerList = getFilterExpression('host.id', undefined); + expect(layerList).toEqual('(NOT host.id:*)'); + }); + + test('it returns a valid expression when provided single filterValue', () => { + const layerList = getFilterExpression('host.id', 'aa8df15'); + expect(layerList).toEqual('host.id: "aa8df15"'); + }); + + test('it returns a valid expression when provided array filterValue', () => { + const layerList = getFilterExpression('host.id', ['xavier', 'angela', 'frank']); + expect(layerList).toEqual('(host.id: "xavier" OR host.id: "angela" OR host.id: "frank")'); + }); + + test('it returns a valid expression when provided array filterValue with a single value', () => { + const layerList = getFilterExpression('host.id', ['xavier']); + expect(layerList).toEqual('(host.id: "xavier")'); + }); +}); + describe('#getExpressionFromArray', () => { test('it returns an empty expression if no filterValues are provided', () => { const layerList = getExpressionFromArray('host.id', []); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/actions/apply_siem_filter_action.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/actions/apply_siem_filter_action.tsx index f25117ea3a0dc..961ae1207df27 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/actions/apply_siem_filter_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/actions/apply_siem_filter_action.tsx @@ -9,15 +9,17 @@ import { getOr } from 'lodash/fp'; import { i18n } from '@kbn/i18n'; // @ts-ignore Missing type defs as maps moves to Typescript import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/common/constants'; -import { - Action, - ActionContext, -} from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions'; +import { Action } from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/actions'; import { IEmbeddable } from '../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/embeddables'; export const APPLY_SIEM_FILTER_ACTION_ID = 'APPLY_SIEM_FILTER_ACTION_ID'; -export class ApplySiemFilterAction extends Action { +interface ActionContext { + embeddable: IEmbeddable; + filters: Filter[]; +} + +export class ApplySiemFilterAction extends Action { public readonly type = APPLY_SIEM_FILTER_ACTION_ID; private readonly applyFilterQueryFromKueryExpression: (expression: string) => void; @@ -36,34 +38,23 @@ export class ApplySiemFilterAction extends Action { }); } - public async isCompatible( - context: ActionContext - ): Promise { - return ( - context.embeddable.type === MAP_SAVED_OBJECT_TYPE && - context.triggerContext != null && - context.triggerContext.filters !== undefined - ); + public async isCompatible(context: ActionContext): Promise { + return context.embeddable.type === MAP_SAVED_OBJECT_TYPE && context.filters !== undefined; } - public execute({ - embeddable, - triggerContext, - }: ActionContext) { - if (!triggerContext) { + public async execute({ embeddable, filters }: ActionContext) { + if (!filters) { throw new Error('Applying a filter requires a filter as context'); } // Parse queryExpression from queryDSL and apply to SIEM global KQL Bar via redux - const filterObject = getOr(null, 'filters[0].query.match', triggerContext); + const filterObject = getOr(null, '[0].query.match', filters); if (filterObject != null) { const filterQuery = getOr('', 'query.query', embeddable.getInput()); const filterKey = Object.keys(filterObject)[0]; - const filterExpression = Array.isArray(filterObject[filterKey].query) - ? getExpressionFromArray(filterKey, filterObject[filterKey].query) - : `${filterKey}: "${filterObject[filterKey].query}"`; + const filterExpression = getFilterExpression(filterKey, filterObject[filterKey].query); this.applyFilterQueryFromKueryExpression( filterQuery.length > 0 ? `${filterQuery} and ${filterExpression}` : filterExpression @@ -72,7 +63,20 @@ export class ApplySiemFilterAction extends Action { } } -export const getExpressionFromArray = (filterKey: string, filterValues: string[]) => +export const getFilterExpression = ( + filterKey: string, + filterValue: string | string[] | undefined +): string => { + if (Array.isArray(filterValue)) { + return getExpressionFromArray(filterKey, filterValue); + } else if (filterValue != null) { + return `${filterKey}: "${filterValue}"`; + } else { + return `(NOT ${filterKey}:*)`; + } +}; + +export const getExpressionFromArray = (filterKey: string, filterValues: string[]): string => filterValues.length > 0 ? `(${filterValues.map(filterValue => `${filterKey}: "${filterValue}"`).join(' OR ')})` : ''; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts index 7499903ddec88..492a33b549d42 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts @@ -51,7 +51,7 @@ export const getSourceLayer = (indexPatternTitle: string, indexPatternId: string type: 'ES_SEARCH', geoField: 'source.geo.location', filterByMapBounds: false, - tooltipProperties: ['host.name', 'host.ip'], + tooltipProperties: ['host.name', 'source.ip', 'source.domain', 'source.as.organization.name'], useTopHits: false, topHitsTimeField: '@timestamp', topHitsSize: 1, @@ -93,7 +93,12 @@ export const getDestinationLayer = (indexPatternTitle: string, indexPatternId: s type: 'ES_SEARCH', geoField: 'destination.geo.location', filterByMapBounds: true, - tooltipProperties: ['host.name', 'host.ip'], + tooltipProperties: [ + 'host.name', + 'destination.ip', + 'destination.domain', + 'destination.as.organization.name', + ], useTopHits: false, topHitsTimeField: '@timestamp', topHitsSize: 1, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts index 62f99688f9f8b..2701250c609fa 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts @@ -5,7 +5,7 @@ */ import { Filter as ESFilterType } from '@kbn/es-query'; -import { TimeRange } from 'ui/timefilter'; +import { TimeRange } from 'src/plugins/data/public'; import { EmbeddableInput } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; export interface MapEmbeddableInput extends EmbeddableInput { diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 9268badd81234..d0857e6ff8b48 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,21 +20,27 @@ interface LinkToPageProps { export const LinkToPage = pure(({ match }) => ( - - + + + - - - - + + + )); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx index 9b1be534af9f5..ee4ff75595c66 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx @@ -11,7 +11,7 @@ import { RedirectWrapper } from './redirect_wrapper'; import { HostsTableType } from '../../store/hosts/model'; export type HostComponentProps = RouteComponentProps<{ - hostName: string; + detailName: string; tabName: HostsTableType; search: string; }>; @@ -31,13 +31,13 @@ export const RedirectToHostsPage = ({ export const RedirectToHostDetailsPage = ({ match: { - params: { hostName, tabName }, + params: { detailName, tabName }, }, location: { search }, }: HostComponentProps) => { const defaultSelectedTab = HostsTableType.authentications; const selectedTab = tabName ? tabName : defaultSelectedTab; - const to = `/hosts/${hostName}/${selectedTab}${search}`; + const to = `/hosts/${detailName}/${selectedTab}${search}`; return ; }; @@ -45,8 +45,8 @@ export const getHostsUrl = () => '#/link-to/hosts'; export const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/link-to/hosts/${tabName}`; -export const getHostDetailsUrl = (hostName: string) => `#/link-to/hosts/${hostName}`; +export const getHostDetailsUrl = (detailName: string) => `#/link-to/hosts/${detailName}`; -export const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => { - return `#/link-to/hosts/${hostName}/${tabName}`; +export const getTabsOnHostDetailsUrl = (detailName: string, tabName: HostsTableType) => { + return `#/link-to/hosts/${detailName}/${tabName}`; }; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx index dba3f825362ba..50b23486d42a2 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx @@ -10,17 +10,17 @@ import { RouteComponentProps } from 'react-router-dom'; import { RedirectWrapper } from './redirect_wrapper'; export type NetworkComponentProps = RouteComponentProps<{ - ip: string; + detailName: string; search: string; }>; export const RedirectToNetworkPage = ({ match: { - params: { ip }, + params: { detailName }, }, location: { search }, }: NetworkComponentProps) => ( - + ); export const getNetworkUrl = () => '#/link-to/network'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx index 05e8a8779e4fd..a7025c68c429e 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx @@ -18,6 +18,7 @@ import { getJobsToDisplay, getJobsToInstall, searchFilter, + getStablePatternTitles, } from './helpers'; jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ @@ -152,4 +153,18 @@ describe('helpers', () => { ]); }); }); + + describe('getStablePatternTitles', () => { + test('it returns a stable reference two times in a row with standard strings', () => { + const one = getStablePatternTitles(['a', 'b', 'c']); + const two = getStablePatternTitles(['a', 'b', 'c']); + expect(one).toBe(two); + }); + + test('it returns a stable reference two times in a row with strings interchanged', () => { + const one = getStablePatternTitles(['c', 'b', 'a']); + const two = getStablePatternTitles(['a', 'b', 'c']); + expect(one).toBe(two); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx index c17763bc60917..d0fc3ab074ebf 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx @@ -76,6 +76,14 @@ export const searchFilter = (jobs: Job[], filterQuery?: string): Job[] => export const getIndexPatternTitles = (indexPatterns: IndexPatternSavedObject[]): string[] => indexPatterns.reduce((acc: string[], v) => [...acc, v.attributes.title], []); +/** + * Given an array of titles this will always return the same string for usage within + * useEffect and other shallow compare areas. + * This won't return a stable reference for case sensitive strings intentionally for speed. + * @param patterns string[] string array that will return a stable reference regardless of ordering or case sensitivity. + */ +export const getStablePatternTitles = (patterns: string[]) => patterns.sort().join(); + /** * Returns a mapping of indexPatternTitle to indexPatternId * diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx index c7500074d0925..5244b2e040ae6 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx @@ -26,6 +26,7 @@ import { getIndexPatternTitles, getJobsToDisplay, getJobsToInstall, + getStablePatternTitles, } from './helpers'; import { configTemplates } from './config_templates'; import { useStateToaster } from '../toasters'; @@ -189,7 +190,7 @@ export const MlPopover = React.memo(() => { }; setupJobs(); } - }, [jobSummaryData, configuredIndexPatternTitles]); + }, [jobSummaryData, getStablePatternTitles(configuredIndexPatternTitles)]); if (!capabilities.isPlatinumOrTrialLicense) { // If the user does not have platinum show upgrade UI diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts index 088c65b1da08b..39a4c8efc4001 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts @@ -6,13 +6,11 @@ import chrome from 'ui/chrome'; import '../../../mock/match_media'; import { encodeIpv6 } from '../../../lib/helpers'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/host_details'; -import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; -import { TIMELINES_PAGE_NAME } from '../../link_to/redirect_to_timelines'; -import { getBreadcrumbsForRoute, rootBreadcrumbs, setBreadcrumbs } from '.'; +import { getBreadcrumbsForRoute, setBreadcrumbs } from '.'; import { HostsTableType } from '../../../store/hosts/model'; -import { SiemPageName } from '../../../pages/home/home_navigations'; +import { RouteSpyState } from '../../../utils/route/types'; +import { TabNavigationProps } from '../tab_navigation/types'; jest.mock('ui/chrome', () => ({ getBasePath: () => { @@ -26,96 +24,184 @@ jest.mock('ui/chrome', () => ({ }), })); +const getMockObject = ( + pageName: string, + pathName: string, + detailName: string | undefined +): RouteSpyState & TabNavigationProps => ({ + detailName, + hostDetails: { filterQuery: null, queryLocation: null }, + hosts: { filterQuery: null, queryLocation: null }, + navTabs: { + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + network: { filterQuery: null, queryLocation: null }, + pageName, + pathName, + search: '', + tabName: HostsTableType.authentications, + timelineId: '', + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, +}); + describe('Navigation Breadcrumbs', () => { const hostName = 'siem-kibana'; - const hostDetailsParams = { - pageName: SiemPageName.hosts, - hostName, - tabName: HostsTableType.authentications, - }; - const hostBreadcrumbs = [ - ...rootBreadcrumbs.overview, - ...getHostDetailsBreadcrumbs(hostDetailsParams), - ]; + const ipv4 = '192.0.2.255'; - const ipv4Breadcrumbs = [...rootBreadcrumbs.overview, ...getIPDetailsBreadcrumbs(ipv4)]; const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff'; const ipv6Encoded = encodeIpv6(ipv6); - const ipv6Breadcrumbs = [...rootBreadcrumbs.overview, ...getIPDetailsBreadcrumbs(ipv6Encoded)]; - describe('getBreadcrumbsForRoute', () => { - test('should return Host breadcrumbs when supplied link-to host pathname', () => { - const pathname = '/link-to/hosts'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.hosts); - }); + describe('getBreadcrumbsForRoute', () => { test('should return Host breadcrumbs when supplied host pathname', () => { - const pathname = '/hosts'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.hosts); - }); - - test('should return Host breadcrumbs when supplied host pathname with trailing slash', () => { - const pathname = '/hosts/'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.hosts); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('hosts', '/hosts', undefined)); + expect(breadcrumbs).toEqual([ + { + href: '#/link-to/overview', + text: 'SIEM', + }, + { + href: + '#/link-to/hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + text: 'Hosts', + }, + { + href: '', + text: 'Authentications', + }, + ]); }); test('should return Network breadcrumbs when supplied network pathname', () => { - const pathname = '/network'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.network); - }); - - test('should return Timelines breadcrumbs when supplied link-to timelines pathname', () => { - const pathname = `/link-to/${TIMELINES_PAGE_NAME}`; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.timelines); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', undefined)); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Network', + href: + '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + ]); }); test('should return Timelines breadcrumbs when supplied timelines pathname', () => { - const pathname = '/timelines'; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(rootBreadcrumbs.timelines); - }); - - test('should return Host Details breadcrumbs when supplied link-to pathname with hostName', () => { - const pathname = `/link-to/hosts/${hostName}`; - - const breadcrumbs = getBreadcrumbsForRoute(pathname, hostDetailsParams); - expect(breadcrumbs).toEqual(hostBreadcrumbs); + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('timelines', '/timelines', undefined) + ); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { text: 'Timelines', href: '' }, + ]); }); test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { - const pathname = `/hosts/${hostName}`; - - const breadcrumbs = getBreadcrumbsForRoute(pathname, hostDetailsParams); - expect(breadcrumbs).toEqual(hostBreadcrumbs); - }); - - test('should return IP Details breadcrumbs when supplied link-to pathname with ipv4', () => { - const pathname = `link-to/network/ip/${ipv4}`; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(ipv4Breadcrumbs); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('hosts', '/hosts', hostName)); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Hosts', + href: + '#/link-to/hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { + text: 'siem-kibana', + href: + '#/link-to/hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { text: 'Authentications', href: '' }, + ]); }); test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { - const pathname = `/network/ip/${ipv4}`; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(ipv4Breadcrumbs); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', ipv4)); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Network', + href: + '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { text: '192.0.2.255', href: '' }, + ]); }); test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { - const pathname = `/network/ip/${ipv6Encoded}`; - const breadcrumbs = getBreadcrumbsForRoute(pathname); - expect(breadcrumbs).toEqual(ipv6Breadcrumbs); + const breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', ipv6Encoded)); + expect(breadcrumbs).toEqual([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Network', + href: + '#/link-to/network?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { text: '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff', href: '' }, + ]); }); }); describe('setBreadcrumbs()', () => { test('should call chrome breadcrumb service with correct breadcrumbs', () => { - const pathname = `/hosts/${hostName}`; - setBreadcrumbs(pathname, hostDetailsParams); - expect(chrome.breadcrumbs.set).toBeCalledWith(hostBreadcrumbs); + setBreadcrumbs(getMockObject('hosts', '/hosts', hostName)); + expect(chrome.breadcrumbs.set).toBeCalledWith([ + { text: 'SIEM', href: '#/link-to/overview' }, + { + text: 'Hosts', + href: + '#/link-to/hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { + text: 'siem-kibana', + href: + '#/link-to/hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + }, + { text: 'Authentications', href: '' }, + ]); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts index 7037d3cc2e869..3e6eb4e51685b 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -6,23 +6,20 @@ import chrome, { Breadcrumb } from 'ui/chrome'; +import { getOr } from 'lodash/fp'; import { APP_NAME } from '../../../../common/constants'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/host_details'; +import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; -import { getNetworkUrl, getOverviewUrl, getTimelinesUrl } from '../../link_to'; -import * as i18n from '../translations'; -import { getHostsUrl } from '../../link_to/redirect_to_hosts'; -import { HostsTableType } from '../../../store/hosts/model'; import { SiemPageName } from '../../../pages/home/home_navigations'; +import { RouteSpyState } from '../../../utils/route/types'; +import { getOverviewUrl } from '../../link_to'; -export interface NavigationParams { - pageName?: SiemPageName; - hostName?: string; - tabName?: HostsTableType; -} +import { TabNavigationProps } from '../tab_navigation/types'; +import { getSearch } from '../helpers'; +import { SearchNavTab } from '../types'; -export const setBreadcrumbs = (pathname: string, params?: NavigationParams) => { - const breadcrumbs = getBreadcrumbsForRoute(pathname, params); +export const setBreadcrumbs = (object: RouteSpyState & TabNavigationProps) => { + const breadcrumbs = getBreadcrumbsForRoute(object); if (breadcrumbs) { chrome.breadcrumbs.set(breadcrumbs); } @@ -35,48 +32,49 @@ export const siemRootBreadcrumb: Breadcrumb[] = [ }, ]; -export const rootBreadcrumbs: { [name: string]: Breadcrumb[] } = { - overview: siemRootBreadcrumb, - hosts: [ - ...siemRootBreadcrumb, - { - text: i18n.HOSTS, - href: getHostsUrl(), - }, - ], - network: [ - ...siemRootBreadcrumb, - { - text: i18n.NETWORK, - href: getNetworkUrl(), - }, - ], - timelines: [ - ...siemRootBreadcrumb, - { - text: i18n.TIMELINES, - href: getTimelinesUrl(), - }, - ], -}; - export const getBreadcrumbsForRoute = ( - pathname: string, - params?: NavigationParams + object: RouteSpyState & TabNavigationProps ): Breadcrumb[] | null => { - const removeSlash = pathname.replace(/\/$/, ''); - const trailingPath = removeSlash.match(/([^\/]+$)/); - - if (trailingPath !== null) { - if (params != null && params.pageName === SiemPageName.hosts) { - return [...siemRootBreadcrumb, ...getHostDetailsBreadcrumbs(params)]; - } - if (Object.keys(rootBreadcrumbs).includes(trailingPath[0])) { - return rootBreadcrumbs[trailingPath[0]]; - } - if (pathname.match(/network\/ip\/.*?/)) { - return [...siemRootBreadcrumb, ...getIPDetailsBreadcrumbs(trailingPath[0])]; + if (object != null && object.navTabs && object.pageName === SiemPageName.hosts) { + const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, object.pageName, object.navTabs)]; + if (object.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, object.tabName, object.navTabs)]; } + return [ + ...siemRootBreadcrumb, + ...getHostDetailsBreadcrumbs( + object, + urlStateKeys.reduce((acc: string[], item: SearchNavTab) => { + acc = [...acc, getSearch(item, object)]; + return acc; + }, []) + ), + ]; + } + if (object != null && object.navTabs && object.pageName === SiemPageName.network) { + const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; + const urlStateKeys = [getOr(tempNav, object.pageName, object.navTabs)]; + return [ + ...siemRootBreadcrumb, + ...getIPDetailsBreadcrumbs( + object.detailName, + urlStateKeys.reduce((acc: string[], item) => { + acc = [...acc, getSearch(item, object)]; + return acc; + }, []) + ), + ]; } + if (object != null && object.navTabs && object.pageName && object.navTabs[object.pageName]) { + return [ + ...siemRootBreadcrumb, + { + text: object.navTabs[object.pageName].name, + href: '', + }, + ]; + } + return null; }; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts new file mode 100644 index 0000000000000..466a200e662e3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts @@ -0,0 +1,50 @@ +/* + * 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 { Location } from 'history'; + +import { UrlInputsModel } from '../../store/inputs/model'; +import { CONSTANTS } from '../url_state/constants'; +import { KqlQuery, URL_STATE_KEYS, KeyUrlState } from '../url_state/types'; +import { + replaceQueryStringInLocation, + replaceStateKeyInQueryString, + getQueryStringFromLocation, +} from '../url_state/helpers'; + +import { TabNavigationProps } from './tab_navigation/types'; +import { SearchNavTab } from './types'; + +export const getSearch = (tab: SearchNavTab, urlState: TabNavigationProps): string => { + if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { + return URL_STATE_KEYS[tab.urlKey].reduce( + (myLocation: Location, urlKey: KeyUrlState) => { + let urlStateToReplace: UrlInputsModel | KqlQuery | string = urlState[CONSTANTS.timelineId]; + if (urlKey === CONSTANTS.kqlQuery && tab.urlKey === 'host') { + urlStateToReplace = tab.isDetailPage ? urlState.hostDetails : urlState.hosts; + } else if (urlKey === CONSTANTS.kqlQuery && tab.urlKey === 'network') { + urlStateToReplace = urlState.network; + } else if (urlKey === CONSTANTS.timerange) { + urlStateToReplace = urlState[CONSTANTS.timerange]; + } + myLocation = replaceQueryStringInLocation( + myLocation, + replaceStateKeyInQueryString(urlKey, urlStateToReplace)( + getQueryStringFromLocation(myLocation) + ) + ); + return myLocation; + }, + { + pathname: urlState.pathName, + hash: '', + search: '', + state: '', + } + ).search; + } + return ''; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index e15646d80ffb5..25ebb8ad89ecd 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -6,51 +6,27 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import { RouteComponentProps } from 'react-router'; import { CONSTANTS } from '../url_state/constants'; import { SiemNavigationComponent } from './'; import { setBreadcrumbs } from './breadcrumbs'; import { navTabs } from '../../pages/home/home_navigations'; -import { TabNavigationProps } from './type'; +import { TabNavigationProps } from './tab_navigation/types'; +import { HostsTableType } from '../../store/hosts/model'; +import { RouteSpyState } from '../../utils/route/types'; jest.mock('./breadcrumbs', () => ({ setBreadcrumbs: jest.fn(), })); -type Action = 'PUSH' | 'POP' | 'REPLACE'; -type Props = RouteComponentProps & TabNavigationProps; -const pop: Action = 'POP'; describe('SIEM Navigation', () => { - const location = { - pathname: '/hosts', + const mockProps: TabNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, search: '', - state: '', - hash: '', - }; - - const mockProps: Props = { - location, - match: { - isExact: true, - params: {}, - path: '', - url: '', - }, + tabName: HostsTableType.authentications, navTabs, - history: { - length: 2, - location, - action: pop, - push: jest.fn(), - replace: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - block: jest.fn(), - createHref: jest.fn(), - listen: jest.fn(), - }, [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { @@ -87,13 +63,141 @@ describe('SIEM Navigation', () => { }, [CONSTANTS.timelineId]: '', }; - const wrapper = shallow(); + const wrapper = shallow(); test('it calls setBreadcrumbs with correct path on mount', () => { - expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, '/hosts', {}); + expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, { + detailName: undefined, + hostDetails: { filterQuery: null, queryLocation: null }, + hosts: { filterQuery: null, queryLocation: null }, + navTabs: { + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + network: { filterQuery: null, queryLocation: null }, + pageName: 'hosts', + pathName: '/hosts', + search: '', + tabName: 'authentications', + timelineId: '', + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }); }); test('it calls setBreadcrumbs with correct path on update', () => { - wrapper.setProps({ location: { pathname: '/network' } }); + wrapper.setProps({ + pageName: 'network', + pathName: '/network', + tabName: undefined, + }); wrapper.update(); - expect(setBreadcrumbs).toHaveBeenNthCalledWith(2, '/network', {}); + expect(setBreadcrumbs).toHaveBeenNthCalledWith(2, { + detailName: undefined, + hostDetails: { filterQuery: null, queryLocation: null }, + hosts: { filterQuery: null, queryLocation: null }, + navTabs: { + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + network: { filterQuery: null, queryLocation: null }, + pageName: 'network', + pathName: '/network', + search: '', + tabName: undefined, + timelineId: '', + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx index cbfb53f13779f..d53895606f9ee 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEqual } from 'lodash/fp'; import React from 'react'; import { compose } from 'redux'; import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; -import { setBreadcrumbs } from './breadcrumbs'; -import { TabNavigation } from './tab_navigation'; -import { TabNavigationProps, SiemNavigationComponentProps } from './type'; +import { RouteSpyState } from '../../utils/route/types'; +import { useRouteSpy } from '../../utils/route/use_route_spy'; +import { CONSTANTS } from '../url_state/constants'; import { inputsSelectors, hostsSelectors, @@ -21,15 +21,23 @@ import { hostsModel, networkModel, } from '../../store'; -import { CONSTANTS } from '../url_state/constants'; -export class SiemNavigationComponent extends React.Component< - RouteComponentProps & TabNavigationProps -> { - public shouldComponentUpdate(nextProps: Readonly): boolean { +import { setBreadcrumbs } from './breadcrumbs'; +import { TabNavigation } from './tab_navigation'; +import { TabNavigationProps } from './tab_navigation/types'; +import { SiemNavigationComponentProps } from './types'; + +export class SiemNavigationComponent extends React.Component { + public shouldComponentUpdate(nextProps: Readonly): boolean { if ( - this.props.location.pathname === nextProps.location.pathname && - this.props.location.search === nextProps.location.search + this.props.pathName === nextProps.pathName && + this.props.search === nextProps.search && + isEqual(this.props.hosts, nextProps.hosts) && + isEqual(this.props.hostDetails, nextProps.hostDetails) && + isEqual(this.props.network, nextProps.network) && + isEqual(this.props.navTabs, nextProps.navTabs) && + isEqual(this.props.timerange, nextProps.timerange) && + isEqual(this.props.timelineId, nextProps.timelineId) ) { return false; } @@ -38,45 +46,104 @@ export class SiemNavigationComponent extends React.Component< public componentWillMount(): void { const { - location, - match: { params }, + detailName, + hosts, + hostDetails, + navTabs, + network, + pageName, + pathName, + search, + tabName, + timerange, + timelineId, } = this.props; - if (location.pathname) { - setBreadcrumbs(location.pathname, params); + if (pathName) { + setBreadcrumbs({ + detailName, + hosts, + hostDetails, + navTabs, + network, + pageName, + pathName, + search, + tabName, + timerange, + timelineId, + }); } } - public componentWillReceiveProps(nextProps: Readonly): void { - if (this.props.location.pathname !== nextProps.location.pathname) { - setBreadcrumbs(nextProps.location.pathname, nextProps.match.params); + public componentWillReceiveProps(nextProps: Readonly): void { + if ( + this.props.pathName !== nextProps.pathName || + this.props.search !== nextProps.search || + !isEqual(this.props.hosts, nextProps.hosts) || + !isEqual(this.props.hostDetails, nextProps.hostDetails) || + !isEqual(this.props.network, nextProps.network) || + !isEqual(this.props.navTabs, nextProps.navTabs) || + !isEqual(this.props.timerange, nextProps.timerange) || + !isEqual(this.props.timelineId, nextProps.timelineId) + ) { + const { + detailName, + hosts, + hostDetails, + navTabs, + network, + pageName, + pathName, + search, + tabName, + timelineId, + timerange, + } = nextProps; + if (pathName) { + setBreadcrumbs({ + detailName, + hosts, + hostDetails, + navTabs, + network, + pageName, + pathName, + search, + tabName, + timerange, + timelineId, + }); + } } } public render() { const { display, - location, - hosts, hostDetails, - match, + hosts, navTabs, network, + pageName, + pathName, showBorder, - timerange, + tabName, timelineId, + timerange, } = this.props; return ( ); } @@ -132,7 +199,15 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -export const SiemNavigation = compose>( - withRouter, - connect(makeMapStateToProps) -)(SiemNavigationComponent); +export const SiemNavigationRedux = compose< + React.ComponentClass +>(connect(makeMapStateToProps))(SiemNavigationComponent); + +export const SiemNavigation = React.memo(props => { + const [routeProps] = useRouteSpy(); + const stateNavReduxProps: RouteSpyState & SiemNavigationComponentProps = { + ...routeProps, + ...props, + }; + return ; +}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx index 7216d825f9c3d..1493ab8ac9ce8 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx @@ -8,32 +8,26 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { TabNavigation } from './'; -import { TabNavigationProps } from '../type'; +import { TabNavigationProps } from './types'; import { navTabs, SiemPageName } from '../../../pages/home/home_navigations'; import { HostsTableType } from '../../../store/hosts/model'; import { navTabsHostDetails } from '../../../pages/hosts/hosts_navigations'; import { CONSTANTS } from '../../url_state/constants'; +import { RouteSpyState } from '../../../utils/route/types'; describe('Tab Navigation', () => { const pageName = SiemPageName.hosts; const hostName = 'siem-window'; const tabName = HostsTableType.authentications; const pathName = `/${pageName}/${hostName}/${tabName}`; - const mockMatch = { - params: { + + describe('Page Navigation', () => { + const mockProps: TabNavigationProps & RouteSpyState = { pageName, - hostName, + pathName, + detailName: undefined, + search: '', tabName, - }, - }; - describe('Page Navigation', () => { - const mockProps: TabNavigationProps = { - location: { - pathname: pathName, - search: '', - state: '', - hash: '', - }, navTabs, [CONSTANTS.timerange]: { global: { @@ -77,7 +71,6 @@ describe('Tab Navigation', () => { test('it mounts with correct tab highlighted', () => { const wrapper = shallow(); const hostsTab = wrapper.find('[data-test-subj="navigation-hosts"]'); - expect(hostsTab.prop('isSelected')).toBeTruthy(); }); test('it changes active tab when nav changes by props', () => { @@ -85,12 +78,9 @@ describe('Tab Navigation', () => { const networkTab = () => wrapper.find('[data-test-subj="navigation-network"]'); expect(networkTab().prop('isSelected')).toBeFalsy(); wrapper.setProps({ - location: { - pathname: '/network', - search: '', - state: '', - hash: '', - }, + pageName: 'network', + pathName: '/network', + tabName: undefined, }); wrapper.update(); expect(networkTab().prop('isSelected')).toBeTruthy(); @@ -105,15 +95,13 @@ describe('Tab Navigation', () => { }); describe('Table Navigation', () => { - const mockProps: TabNavigationProps = { - location: { - pathname: pathName, - search: '', - state: '', - hash: '', - }, + const mockProps: TabNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, + search: '', + tabName: HostsTableType.authentications, navTabs: navTabsHostDetails(hostName), - match: mockMatch, [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { @@ -162,18 +150,15 @@ describe('Tab Navigation', () => { expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); }); test('it changes active tab when nav changes by props', () => { - const newMatch = { - params: { - pageName: SiemPageName.hosts, - hostName, - tabName: HostsTableType.events, - }, - }; const wrapper = shallow(); const tableNavigationTab = () => wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`); expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); - wrapper.setProps({ location: `/${SiemPageName.hosts}`, match: newMatch }); + wrapper.setProps({ + pageName: SiemPageName.hosts, + pathName: `/${SiemPageName.hosts}`, + tabName: HostsTableType.events, + }); wrapper.update(); expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index 5c831bf51e23d..98357901e4273 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -5,23 +5,15 @@ */ import { EuiTab, EuiTabs, EuiLink } from '@elastic/eui'; import { get, getOr } from 'lodash/fp'; -import { Location } from 'history'; + import * as React from 'react'; import styled from 'styled-components'; import classnames from 'classnames'; import { trackUiAction as track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/track_usage'; import { HostsTableType } from '../../../store/hosts/model'; -import { UrlInputsModel } from '../../../store/inputs/model'; -import { CONSTANTS } from '../../url_state/constants'; -import { KqlQuery, URL_STATE_KEYS, KeyUrlState } from '../../url_state/types'; -import { NavTab, NavMatchParams, TabNavigationProps } from '../type'; - -import { - replaceQueryStringInLocation, - replaceStateKeyInQueryString, - getQueryStringFromLocation, -} from '../../url_state/helpers'; +import { getSearch } from '../helpers'; +import { TabNavigationProps } from './types'; const TabContainer = styled.div` .euiLink { @@ -42,15 +34,11 @@ interface TabNavigationState { export class TabNavigation extends React.PureComponent { constructor(props: TabNavigationProps) { super(props); - const pathname = props.location.pathname; - const match = props.match; - const selectedTabId = this.mapLocationToTab(pathname, match); + const selectedTabId = this.mapLocationToTab(props.pageName, props.tabName); this.state = { selectedTabId }; } public componentWillReceiveProps(nextProps: TabNavigationProps): void { - const pathname = nextProps.location.pathname; - const match = nextProps.match; - const selectedTabId = this.mapLocationToTab(pathname, match); + const selectedTabId = this.mapLocationToTab(nextProps.pageName, nextProps.tabName); if (this.state.selectedTabId !== selectedTabId) { this.setState(prevState => ({ @@ -68,13 +56,13 @@ export class TabNavigation extends React.PureComponent { + public mapLocationToTab = (pageName: string, tabName?: HostsTableType): string => { const { navTabs } = this.props; - const tabName: HostsTableType | undefined = get('params.tabName', match); - const myNavTab = Object.keys(navTabs) - .map(tab => get(tab, navTabs)) - .filter((item: NavTab) => (tabName || pathname).includes(item.id))[0]; - return getOr('', 'id', myNavTab); + return getOr( + '', + 'id', + Object.values(navTabs).find(item => tabName === item.id || pageName === item.id) + ); }; private renderTabs = (): JSX.Element[] => { @@ -88,7 +76,7 @@ export class TabNavigation extends React.PureComponent { - return URL_STATE_KEYS[tab.urlKey].reduce( - (myLocation: Location, urlKey: KeyUrlState) => { - let urlStateToReplace: UrlInputsModel | KqlQuery | string = this.props[ - CONSTANTS.timelineId - ]; - if (urlKey === CONSTANTS.kqlQuery && tab.urlKey === 'host') { - urlStateToReplace = tab.isDetailPage ? this.props.hostDetails : this.props.hosts; - } else if (urlKey === CONSTANTS.kqlQuery && tab.urlKey === 'network') { - urlStateToReplace = this.props.network; - } else if (urlKey === CONSTANTS.timerange) { - urlStateToReplace = this.props[CONSTANTS.timerange]; - } - myLocation = replaceQueryStringInLocation( - myLocation, - replaceStateKeyInQueryString(urlKey, urlStateToReplace)( - getQueryStringFromLocation(myLocation) - ) - ); - return myLocation; - }, - { - ...this.props.location, - search: '', - } - ).search; - }; } diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts new file mode 100644 index 0000000000000..38970e31332cd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts @@ -0,0 +1,23 @@ +/* + * 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 { UrlInputsModel } from '../../../store/inputs/model'; +import { CONSTANTS } from '../../url_state/constants'; +import { KqlQuery } from '../../url_state/types'; +import { HostsTableType } from '../../../store/hosts/model'; + +import { SiemNavigationComponentProps } from '../types'; + +export interface TabNavigationProps extends SiemNavigationComponentProps { + pathName: string; + pageName: string; + tabName: HostsTableType | undefined; + hosts: KqlQuery; + hostDetails: KqlQuery; + network: KqlQuery; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timelineId]: string; +} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/type.ts b/x-pack/legacy/plugins/siem/public/components/navigation/type.ts deleted file mode 100644 index ff96f28ed815e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/type.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Location } from 'history'; - -import { UrlInputsModel } from '../../store/inputs/model'; -import { CONSTANTS } from '../url_state/constants'; -import { KqlQuery, UrlStateType } from '../url_state/types'; -import { NavigationParams } from './breadcrumbs'; - -export interface NavTab { - id: string; - name: string; - href: string; - disabled: boolean; - urlKey: UrlStateType; - isDetailPage?: boolean; -} - -export interface NavMatchParams { - params: NavigationParams; -} - -export interface SiemNavigationComponentProps { - display?: 'default' | 'condensed'; - navTabs: Record; - showBorder?: boolean; -} - -export interface TabNavigationProps extends SiemNavigationComponentProps { - location: Location; - hosts: KqlQuery; - hostDetails: KqlQuery; - network: KqlQuery; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timelineId]: string; - match?: NavMatchParams; -} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/types.ts b/x-pack/legacy/plugins/siem/public/components/navigation/types.ts new file mode 100644 index 0000000000000..2918a19df52fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/navigation/types.ts @@ -0,0 +1,24 @@ +/* + * 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 { UrlStateType } from '../url_state/constants'; + +export interface SiemNavigationComponentProps { + display?: 'default' | 'condensed'; + navTabs: Record; + showBorder?: boolean; +} + +export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; + +export interface NavTab { + id: string; + name: string; + href: string; + disabled: boolean; + urlKey: UrlStateType; + isDetailPage?: boolean; +} diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx index f2f37c78807a0..c08b877076cbe 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx @@ -90,7 +90,7 @@ describe('AddToKql Component', () => { activePage: 0, limit: 10, }, - hosts: { + allHosts: { activePage: 0, limit: 10, direction: 'desc', diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx index a4399a16dbf05..700340b8c9dd2 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx @@ -21,6 +21,7 @@ import { createStore, hostsModel, State } from '../../../../store'; import { HostsTable } from './index'; import { mockData } from './mock'; +import { HostsTableType } from '../../../../store/hosts/model'; describe('Hosts Table', () => { const loadPage = jest.fn(); @@ -100,7 +101,7 @@ describe('Hosts Table', () => { ); }); test('Initial value of the store', () => { - expect(store.getState().hosts.page.queries.hosts).toEqual({ + expect(store.getState().hosts.page.queries[HostsTableType.hosts]).toEqual({ activePage: 0, direction: 'desc', sortField: 'lastSeen', @@ -128,7 +129,7 @@ describe('Hosts Table', () => { wrapper.update(); - expect(store.getState().hosts.page.queries.hosts).toEqual({ + expect(store.getState().hosts.page.queries[HostsTableType.hosts]).toEqual({ activePage: 0, direction: 'asc', sortField: 'hostName', diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx index abd3bf0a8a25b..598b9b038d386 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx @@ -50,6 +50,7 @@ export const getNetworkTopNFlowColumns = ( const ipAttr = `${flowTarget}.ip`; const ip: string | null = get(ipAttr, node); const geoAttr = `${flowTarget}.location.geo.country_iso_code[0]`; + const geoAttrName = `${flowTarget}.geo.country_iso_code`; const geo: string | null = get(geoAttr, node); const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-ip-${ip}`); @@ -79,10 +80,30 @@ export const getNetworkTopNFlowColumns = ( /> {geo && ( - <> - {' '} - {geo} - + + snapshot.isDragging ? ( + + + + ) : ( + <> + {' '} + {geo} + + ) + } + /> )} ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx index 7026114795c0f..51ab07a4ad1d6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -20,10 +20,15 @@ import { getRowRenderer } from './get_row_renderer'; describe('get_column_renderer', () => { let nonSuricata: Ecs; let suricata: Ecs; - + let zeek: Ecs; + let system: Ecs; + let auditd: Ecs; beforeEach(() => { nonSuricata = cloneDeep(mockTimelineData[0].ecs); suricata = cloneDeep(mockTimelineData[2].ecs); + zeek = cloneDeep(mockTimelineData[13].ecs); + system = cloneDeep(mockTimelineData[28].ecs); + auditd = cloneDeep(mockTimelineData[19].ecs); }); test('renders correctly against snapshot', () => { @@ -69,4 +74,76 @@ describe('get_column_renderer', () => { 'some child 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' ); }); + + test('should render a suricata row data if event.category is network_traffic', () => { + suricata.event = { ...suricata.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(suricata, rowRenderers); + const row = rowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: suricata, + children: {'some child '}, + }); + const wrapper = mount( + + {row} + + ); + expect(wrapper.text()).toContain( + 'some child 4ETEXPLOITNETGEARWNR2000v5 hidden_lang_avi Stack Overflow (CVE-2016-10174)Source192.168.0.3:53Destination192.168.0.3:6343' + ); + }); + + test('should render a zeek row data if event.category is network_traffic', () => { + zeek.event = { ...zeek.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(zeek, rowRenderers); + const row = rowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: zeek, + children: {'some child '}, + }); + const wrapper = mount( + + {row} + + ); + expect(wrapper.text()).toContain( + 'some child C8DRTq362Fios6hw16connectionREJSrConnection attempt rejectedtcpSource185.176.26.101:44059Destination207.154.238.205:11568' + ); + }); + + test('should render a system row data if event.category is network_traffic', () => { + system.event = { ...system.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(system, rowRenderers); + const row = rowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: system, + children: {'some child '}, + }); + const wrapper = mount( + + {row} + + ); + expect(wrapper.text()).toContain( + 'some child Braden@zeek-londonattempted a login via6278with resultfailureSource128.199.212.120' + ); + }); + + test('should render a auditd row data if event.category is network_traffic', () => { + auditd.event = { ...auditd.event, ...{ category: ['network_traffic'] } }; + const rowRenderer = getRowRenderer(auditd, rowRenderers); + const row = rowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: auditd, + children: {'some child '}, + }); + const wrapper = mount( + + {row} + + ); + expect(wrapper.text()).toContain( + 'some child Sessionalice@zeek-sanfranin/executedgpgconf--list-dirs agent-socket' + ); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/index.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/index.ts index d465ed128ed49..8e95fc3ad238a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/index.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/index.ts @@ -16,12 +16,19 @@ import { unknownColumnRenderer } from './unknown_column_renderer'; import { zeekRowRenderer } from './zeek/zeek_row_renderer'; import { systemRowRenderers } from './system/generic_row_renderer'; +// The row renderers are order dependent and will return the first renderer +// which returns true from its isInstance call. The bottom renderers which +// are netflowRenderer and plainRowRenderer are the most accepting where +// netflowRowRenderer returns true on any netflow related data set including +// Suricata and Zeek which is why Suricata and Zeek are above it. The +// plainRowRenderer always returns true to everything which is why it always +// should be last. export const rowRenderers: RowRenderer[] = [ ...auditdRowRenderers, - netflowRowRenderer, - suricataRowRenderer, ...systemRowRenderers, + suricataRowRenderer, zeekRowRenderer, + netflowRowRenderer, plainRowRenderer, // falls-back to the plain row renderer ]; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap index e3195baf20fd3..47b07bbd09f5a 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/url_state/__snapshots__/index.test.tsx.snap @@ -333,28 +333,11 @@ exports[`UrlStateContainer mounts and renders 1`] = ` "state": "", }, "push": [MockFunction], - "replace": [MockFunction] { - "calls": Array [ - Array [ - Object { - "hash": "", - "pathname": "/network", - "search": "?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))", - "state": "", - }, - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, + "replace": [MockFunction], } } > - - - + - - - - + } + /> + + diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts index 66f79d53c77f0..e0ecfc1640bbe 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts @@ -16,3 +16,5 @@ export enum CONSTANTS { timelineId = 'timelineId', unknown = 'unknown', } + +export type UrlStateType = 'host' | 'network' | 'overview' | 'timeline'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts index 7a0f1402d765a..69e3d10fff8e9 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts @@ -3,45 +3,69 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { isKqlForRoute } from './helpers'; +import { navTabs, SiemPageName } from '../../pages/home/home_navigations'; +import { isKqlForRoute, getTitle } from './helpers'; import { CONSTANTS } from './constants'; -describe('isKqlForRoute', () => { - test('host page and host page kuery', () => { - const result = isKqlForRoute('/hosts', CONSTANTS.hostsPage); - expect(result).toBeTruthy(); - }); - test('host page and host details kuery', () => { - const result = isKqlForRoute('/hosts', CONSTANTS.hostsDetails); - expect(result).toBeFalsy(); - }); - test('works when there is a trailing slash', () => { - const result = isKqlForRoute('/hosts/', CONSTANTS.hostsPage); - expect(result).toBeTruthy(); - }); - test('host details and host details kuery', () => { - const result = isKqlForRoute('/hosts/siem-kibana', CONSTANTS.hostsDetails); - expect(result).toBeTruthy(); - }); - test('host details and host page kuery', () => { - const result = isKqlForRoute('/hosts/siem-kibana', CONSTANTS.hostsPage); - expect(result).toBeFalsy(); - }); - test('network page and network page kuery', () => { - const result = isKqlForRoute('/network', CONSTANTS.networkPage); - expect(result).toBeTruthy(); - }); - test('network page and network details kuery', () => { - const result = isKqlForRoute('/network', CONSTANTS.networkDetails); - expect(result).toBeFalsy(); - }); - test('network details and network details kuery', () => { - const result = isKqlForRoute('/network/ip/10.100.7.198', CONSTANTS.networkDetails); - expect(result).toBeTruthy(); +describe('Helpers Url_State', () => { + describe('isKqlForRoute', () => { + test('host page and host page kuery', () => { + const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsPage); + expect(result).toBeTruthy(); + }); + test('host page and host details kuery', () => { + const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsDetails); + expect(result).toBeFalsy(); + }); + test('host details and host details kuery', () => { + const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsDetails); + expect(result).toBeTruthy(); + }); + test('host details and host page kuery', () => { + const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsPage); + expect(result).toBeFalsy(); + }); + test('network page and network page kuery', () => { + const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkPage); + expect(result).toBeTruthy(); + }); + test('network page and network details kuery', () => { + const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkDetails); + expect(result).toBeFalsy(); + }); + test('network details and network details kuery', () => { + const result = isKqlForRoute(SiemPageName.network, '10.100.7.198', CONSTANTS.networkDetails); + expect(result).toBeTruthy(); + }); + test('network details and network page kuery', () => { + const result = isKqlForRoute(SiemPageName.network, '123.234.34', CONSTANTS.networkPage); + expect(result).toBeFalsy(); + }); }); - test('network details and network page kuery', () => { - const result = isKqlForRoute('/network/ip/123.234.34', CONSTANTS.networkPage); - expect(result).toBeFalsy(); + describe('getTitle', () => { + test('host page name', () => { + const result = getTitle('hosts', undefined, navTabs); + expect(result).toEqual('Hosts'); + }); + test('network page name', () => { + const result = getTitle('network', undefined, navTabs); + expect(result).toEqual('Network'); + }); + test('overview page name', () => { + const result = getTitle('overview', undefined, navTabs); + expect(result).toEqual('Overview'); + }); + test('timelines page name', () => { + const result = getTitle('timelines', undefined, navTabs); + expect(result).toEqual('Timelines'); + }); + test('details page name', () => { + const result = getTitle('hosts', 'details', navTabs); + expect(result).toEqual('details'); + }); + test('Not existing', () => { + const result = getTitle('IamHereButNotReally', undefined, navTabs); + expect(result).toEqual(''); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index cda63bd3a76bd..64a53a8402a57 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -7,8 +7,11 @@ import { decode, encode, RisonValue } from 'rison-node'; import { Location } from 'history'; import { QueryString } from 'ui/utils/query_string'; -import { CONSTANTS } from './constants'; -import { LocationTypes, UrlStateType } from './types'; + +import { SiemPageName } from '../../pages/home/home_navigations'; +import { NavTab } from '../navigation/types'; +import { CONSTANTS, UrlStateType } from './constants'; +import { LocationTypes } from './types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const decodeRisonUrlState = (value: string | undefined): RisonValue | any | undefined => { @@ -71,50 +74,56 @@ export const replaceQueryStringInLocation = (location: Location, queryString: st } }; -export const getUrlType = (pathname: string): UrlStateType => { - const removeSlash = pathname.replace(/\/$/, ''); - const trailingPath = removeSlash.match(/([^\/]+$)/); - if (trailingPath !== null) { - if (trailingPath[0] === 'hosts' || pathname.match(/^\/hosts\/.+$/) != null) { - return 'host'; - } else if (trailingPath[0] === 'network' || pathname.match(/^\/network\/.+$/) != null) { - return 'network'; - } else if (trailingPath[0] === 'overview') { - return 'overview'; - } else if (trailingPath[0] === 'timelines') { - return 'timeline'; - } +export const getUrlType = (pageName: string): UrlStateType => { + if (pageName === SiemPageName.hosts) { + return 'host'; + } else if (pageName === SiemPageName.network) { + return 'network'; + } else if (pageName === SiemPageName.overview) { + return 'overview'; + } else if (pageName === SiemPageName.timelines) { + return 'timeline'; } return 'overview'; }; -export const getCurrentLocation = (pathname: string): LocationTypes => { - const removeSlash = pathname.replace(/\/$/, ''); - const trailingPath = removeSlash.match(/([^\/]+$)/); - if (trailingPath !== null) { - if (trailingPath[0] === 'hosts') { - return CONSTANTS.hostsPage; - } else if (pathname.match(/^\/hosts\/.+$/) != null) { +export const getTitle = ( + pageName: string, + detailName: string | undefined, + navTabs: Record +): string => { + if (detailName != null) return detailName; + return navTabs[pageName] != null ? navTabs[pageName].name : ''; +}; + +export const getCurrentLocation = ( + pageName: string, + detailName: string | undefined +): LocationTypes => { + if (pageName === SiemPageName.hosts) { + if (detailName != null) { return CONSTANTS.hostsDetails; - } else if (trailingPath[0] === 'network') { - return CONSTANTS.networkPage; - } else if (pathname.match(/^\/network\/.+$/) != null) { + } + return CONSTANTS.hostsPage; + } else if (pageName === SiemPageName.network) { + if (detailName != null) { return CONSTANTS.networkDetails; - } else if (trailingPath[0] === 'overview') { - return CONSTANTS.overviewPage; - } else if (trailingPath[0] === 'timelines') { - return CONSTANTS.timelinePage; } + return CONSTANTS.networkPage; + } else if (pageName === SiemPageName.overview) { + return CONSTANTS.overviewPage; + } else if (pageName === SiemPageName.timelines) { + return CONSTANTS.timelinePage; } return CONSTANTS.unknown; - // throw new Error(`'Unknown pathName in else if statement': ${pathname}`); }; export const isKqlForRoute = ( - pathname: string, + pageName: string, + detailName: string | undefined, queryLocation: LocationTypes | null = null ): boolean => { - const currentLocation = getCurrentLocation(pathname); + const currentLocation = getCurrentLocation(pageName, detailName); if ( (currentLocation === CONSTANTS.hostsPage && queryLocation === CONSTANTS.hostsPage) || (currentLocation === CONSTANTS.networkPage && queryLocation === CONSTANTS.networkPage) || diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx index 6957ed515b7c5..dd8e8909a0921 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx @@ -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 { Location } from 'history'; + import { mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -11,20 +11,16 @@ import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import { StaticIndexPattern } from 'ui/index_patterns'; -import { - apolloClientObservable, - globalNode, - HookWrapper, - mockGlobalState, - TestProviders, -} from '../../mock'; +import { apolloClientObservable, HookWrapper, mockGlobalState, TestProviders } from '../../mock'; import { createStore, State } from '../../store'; import { UseUrlState } from './'; import { defaultProps, getMockPropsObj, mockHistory, testCases } from './test_dependencies'; import { UrlStateContainerPropTypes } from './types'; -import { useUrlStateHooks, initializeLocation } from './use_url_state'; +import { useUrlStateHooks } from './use_url_state'; import { CONSTANTS } from './constants'; +import { RouteSpyState } from '../../utils/route/types'; +import { navTabs, SiemPageName } from '../../pages/home/home_navigations'; let mockProps: UrlStateContainerPropTypes; @@ -39,6 +35,19 @@ const indexPattern: StaticIndexPattern = { }, ], }; + +// const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; +const mockRouteSpy: RouteSpyState = { + pageName: SiemPageName.network, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/network', +}; +jest.mock('../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => [mockRouteSpy], +})); + describe('UrlStateContainer', () => { const state: State = mockGlobalState; @@ -55,7 +64,7 @@ describe('UrlStateContainer', () => { - + @@ -68,53 +77,64 @@ describe('UrlStateContainer', () => { describe('handleInitialize', () => { describe('URL state updates redux', () => { describe('relative timerange actions are called with correct data on component mount', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).relativeTimeSearch - .undefinedQuery; - mount( useUrlStateHooks(args)} />); - - // @ts-ignore property mock does not exists - expect(defaultProps.setRelativeTimerange.mock.calls[1][0]).toEqual({ - from: 1558591200000, - fromStr: 'now-1d/d', - kind: 'relative', - to: 1558677599999, - toStr: 'now-1d/d', - id: 'global', - }); - // @ts-ignore property mock does not exists - expect(defaultProps.setRelativeTimerange.mock.calls[0][0]).toEqual({ - from: 1558732849370, - fromStr: 'now-15m', - kind: 'relative', - to: 1558733749370, - toStr: 'now', - id: 'timeline', - }); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).relativeTimeSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + // @ts-ignore property mock does not exists + expect(defaultProps.setRelativeTimerange.mock.calls[1][0]).toEqual({ + from: 1558591200000, + fromStr: 'now-1d/d', + kind: 'relative', + to: 1558677599999, + toStr: 'now-1d/d', + id: 'global', + }); + // @ts-ignore property mock does not exists + expect(defaultProps.setRelativeTimerange.mock.calls[0][0]).toEqual({ + from: 1558732849370, + fromStr: 'now-15m', + kind: 'relative', + to: 1558733749370, + toStr: 'now', + id: 'timeline', + }); + } + ); }); describe('absolute timerange actions are called with correct data on component mount', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).absoluteTimeSearch - .undefinedQuery; - mount( useUrlStateHooks(args)} />); - - // @ts-ignore property mock does not exists - expect(defaultProps.setAbsoluteTimerange.mock.calls[1][0]).toEqual({ - from: 1556736012685, - kind: 'absolute', - to: 1556822416082, - id: 'global', - }); - // @ts-ignore property mock does not exists - expect(defaultProps.setAbsoluteTimerange.mock.calls[0][0]).toEqual({ - from: 1556736012685, - kind: 'absolute', - to: 1556822416082, - id: 'timeline', - }); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) + .absoluteTimeSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + // @ts-ignore property mock does not exists + expect(defaultProps.setAbsoluteTimerange.mock.calls[1][0]).toEqual({ + from: 1556736012685, + kind: 'absolute', + to: 1556822416082, + id: 'global', + }); + // @ts-ignore property mock does not exists + expect(defaultProps.setAbsoluteTimerange.mock.calls[0][0]).toEqual({ + from: 1556736012685, + kind: 'absolute', + to: 1556822416082, + id: 'timeline', + }); + } + ); }); describe('kqlQuery action is called with correct data on component mount', () => { @@ -128,9 +148,9 @@ describe('UrlStateContainer', () => { }; test.each(testCases.slice(0, 4))( ' %o', - (page, namespaceLower, namespaceUpper, examplePath, type) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).relativeTimeSearch - .undefinedQuery; + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) + .relativeTimeSearch.undefinedQuery; mount( useUrlStateHooks(args)} />); const functionName = namespaceUpper === 'Network' ? defaultProps.setNetworkKql : defaultProps.setHostsKql; @@ -144,111 +164,54 @@ describe('UrlStateContainer', () => { }); describe('kqlQuery action is not called called when the queryLocation does not match the router location', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }) - .oppositeQueryLocationSearch.undefinedQuery; - mount( useUrlStateHooks(args)} />); - const functionName = - namespaceUpper === 'Network' ? defaultProps.setNetworkKql : defaultProps.setHostsKql; - // @ts-ignore property mock does not exists - expect(functionName.mock.calls.length).toEqual(0); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).oppositeQueryLocationSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + const functionName = + namespaceUpper === 'Network' ? defaultProps.setNetworkKql : defaultProps.setHostsKql; + // @ts-ignore property mock does not exists + expect(functionName.mock.calls.length).toEqual(0); + } + ); }); }); describe('Redux updates URL state', () => { describe('kqlQuery url state is set from redux data on component mount', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).noSearch.definedQuery; - mount( useUrlStateHooks(args)} />); - - // @ts-ignore property mock does not exists - expect( - mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] - ).toEqual({ - hash: '', - pathname: examplePath, - search: [CONSTANTS.overviewPage, CONSTANTS.timelinePage].includes(page) - ? '?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))' - : `?_g=()&kqlQuery=(filterQuery:(expression:'host.name:%22siem-es%22',kind:kuery),queryLocation:${page})&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, - state: '', - }); - }); - }); - }); - }); - - describe('initializeLocation', () => { - test('basic functionality with no pathname', () => { - Object.defineProperty(globalNode.window, 'location', { - value: { - href: 'http://localhost:5601/app/siem#/overview', - hash: '#/overview', - }, - writable: true, - }); - const location: Location = { - hash: '', - pathname: '/', - search: '', - state: null, - }; - expect(initializeLocation(location).search).toEqual(''); - }); - test('basic functionality with no search', () => { - Object.defineProperty(globalNode.window, 'location', { - value: { - href: 'http://localhost:5601/app/siem#/hosts?_g=()', - }, - writable: true, - }); - const location: Location = { - hash: '', - pathname: '/hosts', - search: '?_g=()', - state: null, - }; - expect(initializeLocation(location).search).toEqual('?_g=()'); - }); - - test('basic functionality with search', () => { - Object.defineProperty(globalNode.window, 'location', { - value: { - href: - "http://localhost:5601/app/siem#/hosts?_g=()&kqlQuery=(filterQuery:(expression:'%20host.name:%20%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%20and%20process.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))", - }, - writable: true, - }); - const location: Location = { - hash: '', - pathname: '/hosts', - search: - "?_g=()&kqlQuery=(filterQuery:(expression:'%2Bhost.name:%2B%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%2Band%2Bprocess.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))", - state: null, - }; - expect(initializeLocation(location).search).toEqual( - "?_g=()&kqlQuery=(filterQuery:(expression:'%20host.name:%20%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%20and%20process.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))" - ); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).noSearch.definedQuery; + mount( useUrlStateHooks(args)} />); - test('If hash and pathname do not match href from the hash, do not do anything', () => { - Object.defineProperty(globalNode.window, 'location', { - value: { - href: - "http://localhost:5601/app/siem#/hosts?_g=()&kqlQuery=(filterQuery:(expression:'%20host.name:%20%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%20and%20process.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))", - }, - writable: true, + // @ts-ignore property mock does not exists + expect( + mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] + ).toEqual({ + hash: '', + pathname: examplePath, + search: [CONSTANTS.overviewPage, CONSTANTS.timelinePage].includes(page) + ? '?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))' + : `?_g=()&kqlQuery=(filterQuery:(expression:'host.name:%22siem-es%22',kind:kuery),queryLocation:${page})&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + state: '', + }); + } + ); }); - const location: Location = { - hash: '', - pathname: '/network', - search: - "?_g=()&kqlQuery=(filterQuery:(expression:'%2Bhost.name:%2B%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%2Band%2Bprocess.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))", - state: null, - }; - expect(initializeLocation(location).search).toEqual( - "?_g=()&kqlQuery=(filterQuery:(expression:'%2Bhost.name:%2B%22beats-ci-immutable-ubuntu-1604-1560801145745062645%22%2Band%2Bprocess.name:*',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1560714985274,fromStr:now-24h,kind:relative,to:1560801385274,toStr:now)))" - ); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx index 700dc9f9ff440..e53cb55c9b792 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { compose, Dispatch } from 'redux'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router-dom'; import { isEqual } from 'lodash/fp'; import { @@ -20,6 +19,8 @@ import { timelineSelectors, } from '../../store'; import { hostsActions, inputsActions, networkActions, timelineActions } from '../../store/actions'; +import { RouteSpyState } from '../../utils/route/types'; +import { useRouteSpy } from '../../utils/route/use_route_spy'; import { CONSTANTS } from './constants'; import { UrlStateContainerPropTypes, UrlStateProps, KqlQuery, LocationTypes } from './types'; @@ -28,13 +29,12 @@ import { dispatchUpdateTimeline } from '../open_timeline/helpers'; import { getCurrentLocation } from './helpers'; export const UrlStateContainer = React.memo( - props => { + (props: UrlStateContainerPropTypes) => { useUrlStateHooks(props); return null; }, (prevProps, nextProps) => - prevProps.location.pathname === nextProps.location.pathname && - isEqual(prevProps.urlState, nextProps.urlState) + prevProps.pathName === nextProps.pathName && isEqual(prevProps.urlState, nextProps.urlState) ); UrlStateContainer.displayName = 'UrlStateContainer'; @@ -44,12 +44,12 @@ const makeMapStateToProps = () => { const getHostsFilterQueryAsKuery = hostsSelectors.hostsFilterQueryAsKuery(); const getNetworkFilterQueryAsKuery = networkSelectors.networkFilterQueryAsKuery(); const getTimelines = timelineSelectors.getTimelines(); - const mapStateToProps = (state: State, { location }: UrlStateContainerPropTypes) => { + const mapStateToProps = (state: State, { pageName, detailName }: UrlStateContainerPropTypes) => { const inputState = getInputsSelector(state); const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - const page: LocationTypes | null = getCurrentLocation(location.pathname); + const page: LocationTypes | null = getCurrentLocation(pageName, detailName); const kqlQueryInitialState: KqlQuery = { filterQuery: null, queryLocation: page, @@ -121,10 +121,15 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ dispatch, }); -export const UseUrlState = compose>( - withRouter, +export const UrlStateRedux = compose>( connect( makeMapStateToProps, mapDispatchToProps ) )(UrlStateContainer); + +export const UseUrlState = React.memo(props => { + const [routeProps] = useRouteSpy(); + const urlStateReduxProps: RouteSpyState & UrlStateProps = { ...routeProps, ...props }; + return ; +}); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx index f194e4cc6f425..6598bd491f73b 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx @@ -9,6 +9,7 @@ import { difference } from 'lodash/fp'; import * as React from 'react'; import { HookWrapper } from '../../mock/hook_wrapper'; +import { SiemPageName } from '../../pages/home/home_navigations'; import { CONSTANTS } from './constants'; import { getFilterQuery, getMockPropsObj, mockHistory, testCases } from './test_dependencies'; @@ -36,6 +37,8 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', + pageName: SiemPageName.network, + detailName: undefined, }).noSearch.definedQuery; const wrapper = mount( useUrlStateHooks(args)} /> @@ -85,6 +88,8 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', + pageName: SiemPageName.network, + detailName: undefined, }).noSearch.undefinedQuery; const wrapper = mount( useUrlStateHooks(args)} /> @@ -117,6 +122,8 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', + pageName: SiemPageName.network, + detailName: undefined, }).noSearch.undefinedQuery; const wrapper = mount( useUrlStateHooks(args)} /> @@ -145,39 +152,46 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => describe('handleInitialize', () => { describe('Redux updates URL state', () => { describe('Timerange url state is set when not defined on component mount', () => { - test.each(testCases)('%o', (page, namespaceLower, namespaceUpper, examplePath) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower }).noSearch - .undefinedQuery; - mount( useUrlStateHooks(args)} />); - - expect(mockHistory.replace.mock.calls[0][0]).toEqual({ - hash: '', - pathname: examplePath, - search: '?_g=()', - state: '', - }); - - expect( - mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] - ).toEqual({ - hash: '', - pathname: examplePath, - search: - '?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', - state: '', - }); - }); + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) + .noSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + expect(mockHistory.replace.mock.calls[0][0]).toEqual({ + hash: '', + pathname: examplePath, + search: '?_g=()', + state: '', + }); + + expect( + mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] + ).toEqual({ + hash: '', + pathname: examplePath, + search: + '?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))', + state: '', + }); + } + ); test('url state is set from redux data when location updates and initialization', () => { mockProps = getMockPropsObj({ page: CONSTANTS.hostsPage, examplePath: '/hosts', namespaceLower: 'hosts', + pageName: SiemPageName.hosts, + detailName: undefined, }).noSearch.undefinedQuery; const updatedProps = getMockPropsObj({ page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', + pageName: SiemPageName.network, + detailName: undefined, }).noSearch.definedQuery; const wrapper = mount( useUrlStateHooks(args)} /> diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts index a520d54a6d493..1915b2c524525 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts @@ -10,6 +10,8 @@ import { UrlStateContainerPropTypes, LocationTypes, KqlQuery } from './types'; import { CONSTANTS } from './constants'; import { InputsModelId } from '../../store/inputs/constants'; import { DispatchUpdateTimeline } from '../open_timeline/types'; +import { navTabs, SiemPageName } from '../../pages/home/home_navigations'; +import { HostsTableType } from '../../store/hosts/model'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; @@ -44,13 +46,12 @@ export const mockHistory = { }; export const defaultProps: UrlStateContainerPropTypes = { - match: { - isExact: true, - params: '', - path: '', - url: '', - }, - isInitializing: true, + pageName: SiemPageName.network, + detailName: undefined, + tabName: HostsTableType.authentications, + search: '', + pathName: '/network', + navTabs, indexPattern: { fields: [ { @@ -127,13 +128,14 @@ export const defaultProps: UrlStateContainerPropTypes = { ...mockHistory, location: defaultLocation, }, - location: defaultLocation, }; export const getMockProps = ( location = defaultLocation, kqlQueryKey = CONSTANTS.networkPage, - kqlQueryValue: KqlQuery | null + kqlQueryValue: KqlQuery | null, + pageName: string, + detailName: string | undefined ): UrlStateContainerPropTypes => ({ ...defaultProps, urlState: { @@ -144,16 +146,27 @@ export const getMockProps = ( ...mockHistory, location, }, - location, + detailName, + pageName, + pathName: location.pathname, + search: location.search, }); interface GetMockPropsObj { examplePath: string; namespaceLower: string; page: LocationTypes; + pageName: string; + detailName: string | undefined; } -export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPropsObj) => ({ +export const getMockPropsObj = ({ + page, + examplePath, + namespaceLower, + pageName, + detailName, +}: GetMockPropsObj) => ({ noSearch: { undefinedQuery: getMockProps( { @@ -163,7 +176,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - null + null, + pageName, + detailName ), definedQuery: getMockProps( { @@ -173,7 +188,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - getFilterQuery(page) + getFilterQuery(page), + pageName, + detailName ), }, relativeTimeSearch: { @@ -185,7 +202,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - null + null, + pageName, + detailName ), definedQuery: getMockProps( { @@ -195,7 +214,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - getFilterQuery(page) + getFilterQuery(page), + pageName, + detailName ), }, absoluteTimeSearch: { @@ -208,7 +229,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - null + null, + pageName, + detailName ), definedQuery: getMockProps( { @@ -219,7 +242,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - getFilterQuery(page) + getFilterQuery(page), + pageName, + detailName ), }, oppositeQueryLocationSearch: { @@ -233,7 +258,9 @@ export const getMockPropsObj = ({ page, examplePath, namespaceLower }: GetMockPr state: '', }, page, - null + null, + pageName, + detailName ), }, }); @@ -245,40 +272,54 @@ export const testCases = [ /* page */ CONSTANTS.networkPage, /* namespaceLower */ 'network', /* namespaceUpper */ 'Network', - /* examplePath */ '/network', + /* pathName */ '/network', /* type */ networkModel.NetworkType.page, + /* pageName */ SiemPageName.network, + /* detailName */ undefined, ], [ /* page */ CONSTANTS.hostsPage, /* namespaceLower */ 'hosts', /* namespaceUpper */ 'Hosts', - /* examplePath */ '/hosts', + /* pathName */ '/hosts', /* type */ hostsModel.HostsType.page, + /* pageName */ SiemPageName.hosts, + /* detailName */ undefined, ], [ /* page */ CONSTANTS.hostsDetails, /* namespaceLower */ 'hosts', /* namespaceUpper */ 'Hosts', - /* examplePath */ '/hosts/siem-es', + /* pathName */ '/hosts/siem-es', /* type */ hostsModel.HostsType.details, + /* pageName */ SiemPageName.hosts, + /* detailName */ 'host-test', ], [ /* page */ CONSTANTS.networkDetails, /* namespaceLower */ 'network', /* namespaceUpper */ 'Network', - /* examplePath */ '/network/ip/100.90.80', + /* pathName */ '/network/ip/100.90.80', /* type */ networkModel.NetworkType.details, + /* pageName */ SiemPageName.network, + /* detailName */ '100.90.80', ], [ /* page */ CONSTANTS.overviewPage, /* namespaceLower */ 'overview', /* namespaceUpper */ 'Overview', - /* examplePath */ '/overview', + /* pathName */ '/overview', + /* type */ null, + /* pageName */ SiemPageName.overview, + /* detailName */ undefined, ], [ /* page */ CONSTANTS.timelinePage, /* namespaceLower */ 'timeline', /* namespaceUpper */ 'Timeline', - /* examplePath */ '/timeline', + /* pathName */ '/timeline', + /* type */ null, + /* pageName */ SiemPageName.timelines, + /* detailName */ undefined, ], ]; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index ea3ea40481179..8ff54e0f9d433 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -4,20 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Location } from 'history'; -import { RouteComponentProps } from 'react-router'; import { ActionCreator } from 'typescript-fsa'; import { StaticIndexPattern } from 'ui/index_patterns'; - import { Dispatch } from 'redux'; + import { hostsModel, KueryFilterQuery, networkModel, SerializedFilterQuery } from '../../store'; import { UrlInputsModel } from '../../store/inputs/model'; import { InputsModelId } from '../../store/inputs/constants'; - -import { CONSTANTS } from './constants'; +import { RouteSpyState } from '../../utils/route/types'; import { DispatchUpdateTimeline } from '../open_timeline/types'; +import { NavTab } from '../navigation/types'; -export type UrlStateType = 'host' | 'network' | 'overview' | 'timeline'; +import { CONSTANTS, UrlStateType } from './constants'; export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ CONSTANTS.kqlQuery, @@ -53,8 +51,8 @@ export interface UrlState { } export type KeyUrlState = keyof UrlState; -export interface UrlStateProps { - isInitializing: boolean; +export interface UrlStateProps { + navTabs: Record; indexPattern?: StaticIndexPattern; mapToUrlState?: (value: string) => UrlState; onChange?: (urlState: UrlState, previousUrlState: UrlState) => void; @@ -100,12 +98,12 @@ export interface UrlStateDispatchToPropsType { }>; } -export type UrlStateContainerPropTypes = RouteComponentProps & +export type UrlStateContainerPropTypes = RouteSpyState & UrlStateStateToPropsType & UrlStateDispatchToPropsType & UrlStateProps; export interface PreviousLocationUrlState { - location: Location; + pathName: string | undefined; urlState: UrlState; } diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx index 441f828f03417..f444b0105100c 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx +++ b/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx @@ -6,7 +6,7 @@ import { Location } from 'history'; import { get, isEqual, difference, isEmpty } from 'lodash/fp'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { convertKueryToElasticSearchQuery } from '../../lib/keury'; import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants'; @@ -16,8 +16,12 @@ import { RelativeTimeRange, UrlInputsModel, } from '../../store/inputs/model'; +import { useApolloClient } from '../../utils/apollo_context'; +import { queryTimelineById } from '../open_timeline/helpers'; +import { HostsType } from '../../store/hosts/model'; +import { NetworkType } from '../../store/network/model'; -import { CONSTANTS } from './constants'; +import { CONSTANTS, UrlStateType } from './constants'; import { replaceQueryStringInLocation, getQueryStringFromLocation, @@ -27,6 +31,7 @@ import { isKqlForRoute, getCurrentLocation, getUrlType, + getTitle, } from './helpers'; import { normalizeTimeRange } from './normalize_time_range'; import { @@ -36,12 +41,7 @@ import { KeyUrlState, KqlQuery, ALL_URL_STATE_KEYS, - UrlStateType, } from './types'; -import { useApolloClient } from '../../utils/apollo_context'; -import { queryTimelineById } from '../open_timeline/helpers'; -import { HostsType } from '../../store/hosts/model'; -import { NetworkType } from '../../store/network/model'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); @@ -54,37 +54,52 @@ function usePrevious(value: PreviousLocationUrlState) { export const useUrlStateHooks = ({ addGlobalLinkTo, addTimelineLinkTo, + detailName, dispatch, - location, indexPattern, - isInitializing, history, + navTabs, + pageName, + pathName, removeGlobalLinkTo, removeTimelineLinkTo, + search, setAbsoluteTimerange, setHostsKql, setNetworkKql, setRelativeTimerange, + tabName, updateTimeline, updateTimelineIsLoading, urlState, }: UrlStateContainerPropTypes) => { + const [isInitializing, setIsInitializing] = useState(true); const apolloClient = useApolloClient(); - const prevProps = usePrevious({ location, urlState }); + const prevProps = usePrevious({ pathName, urlState }); const replaceStateInLocation = ( urlStateToReplace: UrlInputsModel | KqlQuery | string, urlStateKey: string, - latestLocation: Location = location + latestLocation: Location = { + hash: '', + pathname: pathName, + search, + state: '', + } ) => { const newLocation = replaceQueryStringInLocation( - location, + { + hash: '', + pathname: pathName, + search, + state: '', + }, replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)( getQueryStringFromLocation(latestLocation) ) ); - if (!isEqual(newLocation.search, latestLocation.search)) { + if (history && !isEqual(newLocation.search, latestLocation.search)) { history.replace(newLocation); } return newLocation; @@ -101,7 +116,7 @@ export const useUrlStateHooks = ({ const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString); if ( urlKey === CONSTANTS.kqlQuery && - !isKqlForRoute(location.pathname, kqlQueryStateData.queryLocation) && + !isKqlForRoute(pageName, detailName, kqlQueryStateData.queryLocation) && urlState[urlKey].queryLocation === kqlQueryStateData.queryLocation ) { myLocation = replaceStateInLocation( @@ -201,7 +216,7 @@ export const useUrlStateHooks = ({ } if (urlKey === CONSTANTS.kqlQuery && indexPattern != null) { const kqlQueryStateData: KqlQuery = decodeRisonUrlState(newUrlStateString); - if (isKqlForRoute(location.pathname, kqlQueryStateData.queryLocation)) { + if (isKqlForRoute(pageName, detailName, kqlQueryStateData.queryLocation)) { const filterQuery = { kuery: kqlQueryStateData.filterQuery, serializedQuery: convertKueryToElasticSearchQuery( @@ -209,7 +224,7 @@ export const useUrlStateHooks = ({ indexPattern ), }; - const page = getCurrentLocation(location.pathname); + const page = getCurrentLocation(pageName, detailName); if ([CONSTANTS.hostsPage, CONSTANTS.hostsDetails].includes(page)) { dispatch( setHostsKql({ @@ -243,38 +258,30 @@ export const useUrlStateHooks = ({ }; useEffect(() => { - const type: UrlStateType = getUrlType(location.pathname); - if (isInitializing) { - handleInitialize(initializeLocation(location), type); + const type: UrlStateType = getUrlType(pageName); + const location: Location = { + hash: '', + pathname: pathName, + search, + state: '', + }; + + if (isInitializing && pageName != null && pageName !== '') { + handleInitialize(location, type); + setIsInitializing(false); } else if (!isEqual(urlState, prevProps.urlState)) { let newLocation: Location = location; URL_STATE_KEYS[type].forEach((urlKey: KeyUrlState) => { newLocation = replaceStateInLocation(urlState[urlKey], urlKey, newLocation); }); - } else if (location.pathname !== prevProps.location.pathname) { + } else if (pathName !== prevProps.pathName) { handleInitialize(location, type); } }); - return null; -}; + useEffect(() => { + document.title = `${getTitle(pageName, detailName, navTabs)} - Kibana`; + }, [pageName]); -/* - * Why are we doing that, it is because angular-ui router is encoding the `+` back to `2%B` after - * that react router is getting the data with the `+` and convert to `2%B` - * so we need to get back the value from the window location at initialization to avoid - * to bring back the `+` in the kql - */ -export const initializeLocation = (location: Location): Location => { - if (location.pathname === '/') { - location.pathname = window.location.hash.substring(1); - } - const substringIndex = - window.location.href.indexOf(`#${location.pathname}`) >= 0 - ? window.location.href.indexOf(`#${location.pathname}`) + location.pathname.length + 1 - : -1; - if (substringIndex >= 0 && location.pathname !== '/') { - location.search = window.location.href.substring(substringIndex); - } - return location; + return null; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx index 8280f7cb32c08..aa90cb8025dd0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx @@ -22,7 +22,7 @@ interface GlobalQuery extends SetQuery { inputId: InputsModelId; } -interface GlobalTimeArgs { +export interface GlobalTimeArgs { from: number; to: number; setQuery: ({ id, inspect, loading, refetch }: SetQuery) => void; @@ -55,7 +55,7 @@ export const GlobalTimeComponent = React.memo( return () => { deleteAllQuery({ id: 'global' }); }; - }); + }, []); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index 7a19c65ec9f34..83fa30c97145f 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -38,7 +38,7 @@ export const mockGlobalState: State = { page: { queries: { authentications: { activePage: 0, limit: 10 }, - hosts: { + allHosts: { activePage: 0, limit: 10, direction: Direction.desc, @@ -54,7 +54,7 @@ export const mockGlobalState: State = { details: { queries: { authentications: { activePage: 0, limit: 10 }, - hosts: { + allHosts: { activePage: 0, limit: 10, direction: Direction.desc, diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index edbcad74a9cda..329547fe202e0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import * as i18n from '../../components/navigation/translations'; -import { NavTab } from '../../components/navigation/type'; +import * as i18n from './translations'; import { getOverviewUrl, getNetworkUrl, getTimelinesUrl, getHostsUrl, } from '../../components/link_to'; +import { NavTab } from '../../components/navigation/types'; export enum SiemPageName { overview = 'overview', diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index 6bfbdc48e63f5..f5c22e6acbf64 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -12,7 +12,6 @@ import { pure } from 'recompose'; import styled from 'styled-components'; import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; import { AutoSizer } from '../../components/auto_sizer'; import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper'; import { Flyout, flyoutHeaderHeight } from '../../components/flyout'; @@ -25,7 +24,6 @@ import { NotFoundPage } from '../404'; import { HostsContainer } from '../hosts'; import { NetworkContainer } from '../network'; import { Overview } from '../overview'; -import { PageRoute } from '../../components/page_route/pageroute'; import { Timelines } from '../timelines'; import { WithSource } from '../../containers/source'; import { MlPopover } from '../../components/ml_popover/ml_popover'; @@ -33,7 +31,7 @@ import { MlHostConditionalContainer } from '../../components/ml/conditional_link import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container'; import { navTabs } from './home_navigations'; import { UseUrlState } from '../../components/url_state'; -import { useGlobalLoading } from '../../utils/use_global_loading'; +import { SpyRoute } from '../../utils/route/spy_routes'; const WrappedByAutoSizer = styled.div` height: 100%; @@ -77,119 +75,100 @@ const calculateFlyoutHeight = ({ windowHeight: number; }): number => Math.max(0, windowHeight - globalHeaderSize); -export const HomePage = pure(() => { - const isGlobalInitializing = useGlobalLoading(); - return ( - - {({ measureRef, windowMeasurement: { height: windowHeight = 0 } }) => ( - - - - - {({ browserFields, indexPattern }) => ( - - - - ( + + {({ measureRef, windowMeasurement: { height: windowHeight = 0 } }) => ( + + + + + {({ browserFields, indexPattern }) => ( + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + } /> + ( + + )} /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - ( - - )} - /> - - - ( - - )} - /> - - - - - - - - )} - - - - )} - - ); -}); + ( + + )} + /> + } /> + + + + + + + + )} + + + + + )} + +)); HomePage.displayName = 'HomePage'; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/home/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx new file mode 100644 index 0000000000000..ad2c38cb0af12 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/body.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; + +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../../components/ml/types'; +import { getHostDetailsEventsKqlQueryExpression } from '../helpers'; + +import { HostDetailsBodyComponentProps } from './type'; +import { getFilterQuery, type, makeMapStateToProps } from './utils'; + +const HostDetailsBodyComponent = React.memo( + ({ + children, + filterQueryExpression, + from, + isInitializing, + detailName, + setAbsoluteRangeDatePicker, + setQuery, + to, + }) => { + return ( + + {({ indicesExist, indexPattern }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + <> + {children({ + endDate: to, + filterQuery: getFilterQuery(detailName, filterQueryExpression, indexPattern), + kqlQueryExpression: getHostDetailsEventsKqlQueryExpression({ + filterQueryExpression, + hostName: detailName, + }), + skip: isInitializing, + setQuery, + startDate: from, + type, + indexPattern, + narrowDateRange: (score: Anomaly, interval: string) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + })} + + ) : null + } + + ); + } +); + +HostDetailsBodyComponent.displayName = 'HostDetailsBodyComponent'; + +export const HostDetailsBody = connect( + makeMapStateToProps, + { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + } +)(HostDetailsBodyComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx new file mode 100644 index 0000000000000..ebcfeb30c6e0c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx @@ -0,0 +1,175 @@ +/* + * 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 { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import React from 'react'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; + +import { FiltersGlobal } from '../../../components/filters_global'; +import { HeaderPage } from '../../../components/header_page'; +import { LastEventTime } from '../../../components/last_event_time'; + +import { HostOverviewByNameQuery } from '../../../containers/hosts/overview'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; +import { LastEventIndexKey } from '../../../graphql/types'; + +import { HostsEmptyPage } from '../hosts_empty_page'; +import { HostsKql } from '../kql'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details'; +import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria'; +import { navTabsHostDetails } from '../hosts_navigations'; +import { SiemNavigation } from '../../../components/navigation'; +import { HostsQueryProps } from '../hosts'; +import { SpyRoute } from '../../../utils/route/spy_routes'; +import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; +import { manageQuery } from '../../../components/page/manage_query'; +import { HostOverview } from '../../../components/page/hosts/host_overview'; +import { KpiHostsComponent } from '../../../components/page/hosts'; + +import { HostDetailsComponentProps } from './type'; +import { getFilterQuery, type, makeMapStateToProps } from './utils'; + +export { HostDetailsBody } from './body'; + +const HostOverviewManage = manageQuery(HostOverview); +const KpiHostDetailsManage = manageQuery(KpiHostsComponent); + +const HostDetailsComponent = React.memo( + ({ + isInitializing, + filterQueryExpression, + from, + detailName, + setQuery, + setAbsoluteRangeDatePicker, + to, + }) => { + return ( + <> + + {({ indicesExist, indexPattern }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + + + + } + title={detailName} + /> + + {({ hostOverview, loading, id, inspect, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + + + + + + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + { + /** + * Using setTimeout here because of this issue: + * https://github.com/elastic/elastic-charts/issues/360 + * Need to remove the setTimeout here after this issue is fixed. + * */ + setTimeout(() => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, 500); + }} + /> + )} + + + + + + + ) : ( + <> + + + + + ) + } + + + + ); + } +); + +HostDetailsComponent.displayName = 'HostDetailsComponent'; + +export const HostDetails = compose>( + connect( + makeMapStateToProps, + { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + } + ) +)(HostDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/type.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/type.ts new file mode 100644 index 0000000000000..a057e791ab1d4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/type.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionCreator } from 'typescript-fsa'; + +import { InputsModelId } from '../../../store/inputs/constants'; +import { CommonChildren, AnonamaliesChildren, HostsQueryProps } from '../hosts'; +import { HostComponentProps } from '../../../components/link_to/redirect_to_hosts'; + +interface HostDetailsComponentReduxProps { + filterQueryExpression: string; +} + +interface HostDetailsComponentDispatchProps { + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + detailName: string; +} + +export interface HostDetailsBodyProps extends HostsQueryProps { + children: CommonChildren | AnonamaliesChildren; +} + +export type HostDetailsComponentProps = HostDetailsComponentReduxProps & + HostDetailsComponentDispatchProps & + HostComponentProps & + HostsQueryProps; + +export type HostDetailsBodyComponentProps = HostDetailsComponentReduxProps & + HostDetailsComponentDispatchProps & + HostDetailsBodyProps; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts new file mode 100644 index 0000000000000..cd4239f00cac7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts @@ -0,0 +1,81 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import { Breadcrumb } from 'ui/chrome'; +import { StaticIndexPattern } from 'ui/index_patterns'; + +import { ESTermQuery } from '../../../../common/typed_json'; + +import { hostsModel, hostsSelectors } from '../../../store/hosts'; +import { HostsTableType } from '../../../store/hosts/model'; +import { State } from '../../../store'; +import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; + +import * as i18n from '../translations'; +import { convertKueryToElasticSearchQuery, escapeQueryValue } from '../../../lib/keury'; +import { RouteSpyState } from '../../../utils/route/types'; + +export const type = hostsModel.HostsType.details; + +export const makeMapStateToProps = () => { + const getHostsFilterQuery = hostsSelectors.hostsFilterQueryExpression(); + return (state: State) => ({ + filterQueryExpression: getHostsFilterQuery(state, type) || '', + }); +}; + +const TabNameMappedToI18nKey = { + [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, + [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, + [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, + [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, +}; + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: `${getHostsUrl()}${search && search[0] ? search[0] : ''}`, + }, + ]; + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: `${getHostDetailsUrl(params.detailName)}${search && search[1] ? search[1] : ''}`, + }, + ]; + } + if (params.tabName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[params.tabName], + href: '', + }, + ]; + } + return breadcrumb; +}; + +export const getFilterQuery = ( + hostName: string | null, + filterQueryExpression: string, + indexPattern: StaticIndexPattern +): ESTermQuery | string => + isEmpty(filterQueryExpression) + ? hostName + ? { term: { 'host.name': hostName } } + : '' + : convertKueryToElasticSearchQuery( + `${filterQueryExpression} ${ + hostName ? `and host.name: "${escapeQueryValue(hostName)}"` : '' + }`, + indexPattern + ); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx deleted file mode 100644 index 972aef1327750..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx +++ /dev/null @@ -1,319 +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 { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import { isEmpty, get } from 'lodash/fp'; -import React from 'react'; -import { connect } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; -import { Breadcrumb } from 'ui/chrome'; -import { StaticIndexPattern } from 'ui/index_patterns'; - -import { ActionCreator } from 'typescript-fsa'; -import { ESTermQuery } from '../../../common/typed_json'; -import { FiltersGlobal } from '../../components/filters_global'; -import { HeaderPage } from '../../components/header_page'; -import { LastEventTime } from '../../components/last_event_time'; -import { - getHostsUrl, - HostComponentProps, - getHostDetailsUrl, -} from '../../components/link_to/redirect_to_hosts'; -import { KpiHostsComponent } from '../../components/page/hosts'; - -import { HostOverview } from '../../components/page/hosts/host_overview'; -import { manageQuery } from '../../components/page/manage_query'; -import { GlobalTime } from '../../containers/global_time'; -import { HostOverviewByNameQuery } from '../../containers/hosts/overview'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; -import { LastEventIndexKey } from '../../graphql/types'; -import { convertKueryToElasticSearchQuery, escapeQueryValue } from '../../lib/keury'; -import { hostsModel, hostsSelectors, State } from '../../store'; - -import { HostsEmptyPage } from './hosts_empty_page'; -import { HostsKql } from './kql'; -import * as i18n from './translations'; -import { AnomalyTableProvider } from '../../components/ml/anomaly/anomaly_table_provider'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { InputsModelId } from '../../store/inputs/constants'; -import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; -import { KpiHostDetailsQuery } from '../../containers/kpi_host_details'; -import { hostToCriteria } from '../../components/ml/criteria/host_to_criteria'; -import { navTabsHostDetails } from './hosts_navigations'; -import { SiemNavigation } from '../../components/navigation'; -import { Anomaly } from '../../components/ml/types'; -import { NavigationParams } from '../../components/navigation/breadcrumbs'; -import { HostsTableType } from '../../store/hosts/model'; -import { HostsQueryProps } from './hosts'; -import { getHostDetailsEventsKqlQueryExpression } from './helpers'; - -const type = hostsModel.HostsType.details; - -const HostOverviewManage = manageQuery(HostOverview); -const KpiHostDetailsManage = manageQuery(KpiHostsComponent); - -interface HostDetailsComponentReduxProps { - filterQueryExpression: string; - setAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; -} - -type HostDetailsComponentProps = HostDetailsComponentReduxProps & - HostComponentProps & - HostsQueryProps; - -const HostDetailsComponent = React.memo( - ({ - match: { - params: { hostName, tabName }, - }, - filterQueryExpression, - setAbsoluteRangeDatePicker, - }) => { - return ( - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - } - title={hostName} - /> - - - {({ to, from, setQuery, isInitializing }) => ( - <> - - {({ hostOverview, loading, id, inspect, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - - - - - - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - { - /** - * Using setTimeout here because of this issue: - * https://github.com/elastic/elastic-charts/issues/360 - * Need to remove the setTimeout here after this issue is fixed. - * */ - setTimeout(() => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, 500); - }} - /> - )} - - - - - - - )} - - - ) : ( - <> - - - - - ) - } - - ); - } -); - -HostDetailsComponent.displayName = 'HostDetailsComponent'; - -const HostDetailsBodyComponent = React.memo( - ({ - match: { - params: { hostName, tabName }, - }, - filterQueryExpression, - setAbsoluteRangeDatePicker, - children, - }) => { - return ( - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ to, from, setQuery, isInitializing }) => ( - <> - {children({ - endDate: to, - filterQuery: getFilterQuery(hostName, filterQueryExpression, indexPattern), - kqlQueryExpression: getHostDetailsEventsKqlQueryExpression({ - filterQueryExpression, - hostName, - }), - skip: isInitializing, - setQuery, - startDate: from, - type, - indexPattern, - narrowDateRange: (score: Anomaly, interval: string) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - })} - - )} - - ) : null - } - - ); - } -); - -HostDetailsBodyComponent.displayName = 'HostDetailsBodyComponent'; - -const makeMapStateToProps = () => { - const getHostsFilterQuery = hostsSelectors.hostsFilterQueryExpression(); - return (state: State) => ({ - filterQueryExpression: getHostsFilterQuery(state, type) || '', - }); -}; - -export const HostDetails = connect( - makeMapStateToProps, - { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - } -)(HostDetailsComponent); - -export const HostDetailsBody = connect( - makeMapStateToProps, - { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - } -)(HostDetailsBodyComponent); - -const TabNameMappedToI18nKey = { - [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, - [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, - [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, -}; - -export const getBreadcrumbs = (params: NavigationParams): Breadcrumb[] => { - const hostName = get('hostName', params); - const tabName = get('tabName', params); - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getHostsUrl(), - }, - ]; - if (hostName) { - breadcrumb = [ - ...breadcrumb, - { - text: hostName, - href: getHostDetailsUrl(hostName), - }, - ]; - } - if (tabName) { - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - } - return breadcrumb; -}; - -const getFilterQuery = ( - hostName: string | null, - filterQueryExpression: string, - indexPattern: StaticIndexPattern -): ESTermQuery | string => - isEmpty(filterQueryExpression) - ? hostName - ? { term: { 'host.name': hostName } } - : '' - : convertKueryToElasticSearchQuery( - `${filterQueryExpression} ${ - hostName ? `and host.name: "${escapeQueryValue(hostName)}"` : '' - }`, - indexPattern - ); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx index fbb522772d4a4..98698d50da096 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -7,10 +7,11 @@ import { mount } from 'enzyme'; import * as React from 'react'; import { Router } from 'react-router-dom'; +import { ActionCreator } from 'typescript-fsa'; import '../../mock/match_media'; import '../../mock/ui_settings'; -import { Hosts, AnonamaliesChildren, HostsComponentProps } from './hosts'; +import { Hosts, HostsComponentProps } from './hosts'; import { mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; @@ -19,6 +20,8 @@ import { cloneDeep } from 'lodash/fp'; import { SiemNavigation } from '../../components/navigation'; import { wait } from '../../lib/helpers'; +import { InputsModelId } from '../../store/inputs/constants'; + jest.mock('../../lib/settings/use_kibana_ui_setting'); jest.mock('ui/documentation_links', () => ({ @@ -62,22 +65,26 @@ const mockHistory = { listen: jest.fn(), }; -const mockMatch = { - isExact: false, - url: '/', - path: '/', -}; -const mockChildren: AnonamaliesChildren = () =>
; - // Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769 /* eslint-disable no-console */ const originalError = console.error; +const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); +const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); + describe('Hosts - rendering', () => { - const hostProps = { - match: mockMatch, - children: mockChildren, - } as HostsComponentProps; + const hostProps: HostsComponentProps = { + from, + to, + setQuery: jest.fn(), + isInitializing: false, + setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ + from: number; + id: InputsModelId; + to: number; + }>, + filterQuery: '', + }; beforeAll(() => { console.error = jest.fn(); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx index 63a822b90d0ee..6ada127c9f650 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx @@ -6,18 +6,16 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; +import { compose } from 'redux'; import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; -import { pure } from 'recompose'; import { ActionCreator } from 'typescript-fsa'; -import { RouteComponentProps } from 'react-router-dom'; -import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; import { KpiHostsComponent } from '../../components/page/hosts'; import { manageQuery } from '../../components/page/manage_query'; -import { GlobalTime } from '../../containers/global_time'; +import { GlobalTimeArgs } from '../../containers/global_time'; import { KpiHostsQuery } from '../../containers/kpi_hosts'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { LastEventIndexKey } from '../../graphql/types'; @@ -28,6 +26,8 @@ import { HostsKql } from './kql'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { InputsModelId } from '../../store/inputs/constants'; import { SiemNavigation } from '../../components/navigation'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { FiltersGlobal } from '../../components/filters_global'; import * as i18n from './translations'; import { @@ -40,7 +40,9 @@ const KpiHostsComponentManage = manageQuery(KpiHostsComponent); interface HostsComponentReduxProps { filterQuery: string; - kqlQueryExpression: string; +} + +interface HostsComponentDispatchProps { setAbsoluteRangeDatePicker: ActionCreator<{ id: InputsModelId; from: number; @@ -48,32 +50,31 @@ interface HostsComponentReduxProps { }>; } -type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; +export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; export type AnonamaliesChildren = (args: AnomaliesQueryTabBodyProps) => JSX.Element; -export interface HostsQueryProps { - children: CommonChildren | AnonamaliesChildren; -} - -export type HostsComponentProps = RouteComponentProps & HostsComponentReduxProps & HostsQueryProps; +export type HostsQueryProps = GlobalTimeArgs; -const HostsComponent = pure(({ filterQuery, setAbsoluteRangeDatePicker }) => { - return ( - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - +export type HostsComponentProps = HostsComponentReduxProps & + HostsComponentDispatchProps & + HostsQueryProps; - } - title={i18n.PAGE_TITLE} - /> +const HostsComponent = React.memo( + ({ isInitializing, filterQuery, from, setAbsoluteRangeDatePicker, setQuery, to }) => { + return ( + <> + + {({ indicesExist, indexPattern }) => + indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + - - {({ to, from, setQuery, isInitializing }) => ( + } + title={i18n.PAGE_TITLE} + /> <> (({ filterQuery, setAbsoluteRang - )} - - - ) : ( - <> - - - - ) - } - - ); -}); + + ) : ( + <> + + + + ) + } + + + + ); + } +); HostsComponent.displayName = 'HostsComponent'; const makeMapStateToProps = () => { const getHostsFilterQueryAsJson = hostsSelectors.hostsFilterQueryAsJson(); - const mapStateToProps = (state: State) => ({ + const mapStateToProps = (state: State): HostsComponentReduxProps => ({ filterQuery: getHostsFilterQueryAsJson(state, hostsModel.HostsType.page) || '', }); return mapStateToProps; }; -export const Hosts = connect( - makeMapStateToProps, - { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, - } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const Hosts = compose>( + connect( + makeMapStateToProps, + { + setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, + } + ) )(HostsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx index 4b8fa3b734587..898bdec5b281c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_body.tsx @@ -14,12 +14,17 @@ import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../cont import { hostsModel, hostsSelectors, State } from '../../store'; -import { HostsComponentProps } from './hosts'; +import { HostsComponentProps, CommonChildren, AnonamaliesChildren } from './hosts'; import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { Anomaly } from '../../components/ml/types'; -const HostsBodyComponent = pure( +interface HostsBodyComponentProps extends HostsComponentProps { + kqlQueryExpression: string; + children: CommonChildren | AnonamaliesChildren; +} + +const HostsBodyComponent = pure( ({ filterQuery, kqlQueryExpression, setAbsoluteRangeDatePicker, children }) => { return ( diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx index 52416bf6997a8..1f496e4d85b6e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_navigations.tsx @@ -9,7 +9,6 @@ import { getOr } from 'lodash/fp'; import React from 'react'; import * as i18n from './translations'; -import { NavTab } from '../../components/navigation/type'; import { HostsTable, UncommonProcessTable } from '../../components/page/hosts'; import { HostsQuery } from '../../containers/hosts'; @@ -24,6 +23,7 @@ import { AuthenticationsQuery } from '../../containers/authentications'; import { ESTermQuery } from '../../../common/typed_json'; import { HostsTableType } from '../../store/hosts/model'; import { StatefulEventsViewer } from '../../components/events_viewer'; +import { NavTab } from '../../components/navigation/types'; const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/hosts/${tabName}`; const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => { diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx index 415848337d9e9..36d9826c5d6a6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx @@ -5,15 +5,9 @@ */ import React from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import { pure } from 'recompose'; -import { i18n } from '@kbn/i18n'; -import { PageRoute } from '../../components/page_route/pageroute'; +import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; -import { HostComponentProps } from '../../components/link_to/redirect_to_hosts'; - -import { HostDetails, HostDetailsBody } from './host_details'; -import { Hosts } from './hosts'; +import { HostDetailsBody, HostDetails } from './details'; import { HostsQueryTabBody, AuthenticationsQueryTabBody, @@ -23,6 +17,8 @@ import { } from './hosts_navigations'; import { HostsBody } from './hosts_body'; import { HostsTableType } from '../../store/hosts/model'; +import { GlobalTime } from '../../containers/global_time'; +import { Hosts } from './hosts'; const hostsPagePath = `/:pageName(hosts)`; @@ -35,114 +31,199 @@ const getHostsTabPath = (pagePath: string) => `${HostsTableType.events})`; const getHostDetailsTabPath = (pagePath: string) => - `${pagePath}/:hostName/:tabName(` + + `${pagePath}/:detailName/:tabName(` + `${HostsTableType.authentications}|` + `${HostsTableType.uncommonProcesses}|` + `${HostsTableType.anomalies}|` + `${HostsTableType.events})`; -export const HostsContainer = pure(({ match }) => ( - <> - - ( - <> - +type Props = Partial> & { url: string }; + +export const HostsContainer = React.memo(({ url }) => ( + + {({ to, from, setQuery, isInitializing }) => ( + + ( } - /> - - )} - /> - ( - <> - - } - /> - } - /> - } - /> - } - /> - } - /> - - )} - /> - ( - <> - - } - /> - } - /> - } - /> - } - /> - } + render={() => ( + <> + + + + )} /> - - )} - /> - - - - + )} + /> + ( + <> + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + )} + /> + ( + <> + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + )} + /> + + + + )} + )); HostsContainer.displayName = 'HostsContainer'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap index 633b68e9359be..71d799bbf7063 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap @@ -2,6 +2,7 @@ exports[`Ip Details it matches the snapshot 1`] = ` (({ match }) => ( - <> - - ( - - )} - /> - ( - - )} - /> - - - + +type Props = Partial> & { url: string }; + +export const NetworkContainer = React.memo(() => ( + + } /> + } + /> + + )); NetworkContainer.displayName = 'NetworkContainer'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx index 757829ffec82e..5a31bc8e7ae5d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx @@ -70,7 +70,8 @@ const getMockProps = (ip: string) => ({ state: '', hash: '', }, - match: { params: { ip, search: '' }, isExact: true, path: '', url: '' }, + detailName: ip, + match: { params: { detailName: ip, search: '' }, isExact: true, path: '', url: '' }, setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ id: InputsModelId; from: number; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx index dafa6f57b6b7a..6e723c1c83a78 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx @@ -16,7 +16,7 @@ import { ActionCreator } from 'typescript-fsa'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; -import { getNetworkUrl, NetworkComponentProps } from '../../components/link_to/redirect_to_network'; +import { getNetworkUrl } from '../../components/link_to/redirect_to_network'; import { manageQuery } from '../../components/page/manage_query'; import { DomainsTable } from '../../components/page/network/domains_table'; import { FlowTargetSelectConnected } from '../../components/page/network/flow_target_select_connected'; @@ -42,6 +42,7 @@ import { InputsModelId } from '../../store/inputs/constants'; import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; import { AnomaliesNetworkTable } from '../../components/ml/tables/anomalies_network_table'; import { networkToCriteria } from '../../components/ml/criteria/network_to_criteria'; +import { SpyRoute } from '../../utils/route/spy_routes'; const DomainsTableManage = manageQuery(DomainsTable); const TlsTableManage = manageQuery(TlsTable); @@ -58,212 +59,216 @@ interface IPDetailsComponentReduxProps { }>; } -export type IPDetailsComponentProps = IPDetailsComponentReduxProps & NetworkComponentProps; +export type IPDetailsComponentProps = IPDetailsComponentReduxProps & { detailName: string }; export const IPDetailsComponent = pure( - ({ - match: { - params: { ip }, - }, - filterQuery, - flowTarget, - setAbsoluteRangeDatePicker, - }) => ( - - {({ indicesExist, indexPattern }) => - indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - + ({ detailName, filterQuery, flowTarget, setAbsoluteRangeDatePicker }) => ( + <> + + {({ indicesExist, indexPattern }) => { + const ip = decodeIpv6(detailName); + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + - - } - title={decodeIpv6(ip)} - > - - + } + title={ip} + > + + - - {({ to, from, setQuery, isInitializing }) => ( - <> - - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - + + {({ to, from, setQuery, isInitializing }) => ( + <> + + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + - + - - {({ - id, - inspect, - domains, - totalCount, - pageInfo, - loading, - loadPage, - refetch, - }) => ( - - )} - + + {({ + id, + inspect, + domains, + totalCount, + pageInfo, + loading, + loadPage, + refetch, + }) => ( + + )} + - + - - {({ id, inspect, users, totalCount, pageInfo, loading, loadPage, refetch }) => ( - - )} - + + {({ + id, + inspect, + users, + totalCount, + pageInfo, + loading, + loadPage, + refetch, + }) => ( + + )} + - + - - {({ id, inspect, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( - - )} - + + {({ id, inspect, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( + + )} + - + - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - - )} - - - ) : ( - <> - + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + + )} + + + ) : ( + <> + - - - ) - } - + + + ); + }} + + + ) ); @@ -285,11 +290,11 @@ export const IPDetails = connect( } )(IPDetailsComponent); -export const getBreadcrumbs = (ip: string): Breadcrumb[] => { +export const getBreadcrumbs = (ip: string | undefined, search: string[]): Breadcrumb[] => { const breadcrumbs = [ { text: i18n.PAGE_TITLE, - href: getNetworkUrl(), + href: `${getNetworkUrl()}${search && search[0] ? search[0] : ''}`, }, ]; if (ip) { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx index 28f395e5cf303..061b1145cf5cd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/network.tsx @@ -12,6 +12,7 @@ import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; import { ActionCreator } from 'typescript-fsa'; +import { RouteComponentProps } from 'react-router-dom'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; @@ -35,6 +36,7 @@ import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from import { InputsModelId } from '../../store/inputs/constants'; import { EmbeddedMap } from '../../components/embeddables/embedded_map'; import { NetworkFilter } from '../../containers/network'; +import { SpyRoute } from '../../utils/route/spy_routes'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); const NetworkDnsTableManage = manageQuery(NetworkDnsTable); @@ -49,7 +51,7 @@ interface NetworkComponentReduxProps { }>; } -type NetworkComponentProps = NetworkComponentReduxProps; +type NetworkComponentProps = NetworkComponentReduxProps & Partial>; const mediaMatch = window.matchMedia( 'screen and (min-width: ' + euiLightVars.euiBreakpoints.xl + ')' ); @@ -73,8 +75,8 @@ export const getFlexDirection = () => { }; const NetworkComponent = React.memo( - ({ filterQuery, queryExpression, setAbsoluteRangeDatePicker }) => { - return ( + ({ filterQuery, queryExpression, setAbsoluteRangeDatePicker }) => ( + <> {({ indicesExist, indexPattern }) => indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( @@ -283,8 +285,9 @@ const NetworkComponent = React.memo( ) } - ); - } + + + ) ); NetworkComponent.displayName = 'NetworkComponent'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx index 82f4d3e32b117..e0af54acde310 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { pure } from 'recompose'; +import React, { memo } from 'react'; import { OverviewComponent } from './overview'; -export const Overview = pure(() => ); +export const Overview = memo(() => ); Overview.displayName = 'Overview'; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx index 160379567133f..833030e0dc8a1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx @@ -5,15 +5,16 @@ */ import { mount } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { MemoryRouter } from 'react-router-dom'; import { Overview } from './index'; import '../../mock/ui_settings'; import { mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { cloneDeep } from 'lodash/fp'; jest.mock('ui/documentation_links', () => ({ documentationLinks: { @@ -45,7 +46,9 @@ describe('Overview', () => { const wrapper = mount( - + + + ); @@ -60,7 +63,9 @@ describe('Overview', () => { const wrapper = mount( - + + + ); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx index f13b48532dc14..d8965f4d49491 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx @@ -19,6 +19,7 @@ import { GlobalTime } from '../../containers/global_time'; import { Summary } from './summary'; import { EmptyPage } from '../../components/empty_page'; import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; +import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; @@ -65,6 +66,7 @@ export const OverviewComponent = pure(() => { ) } + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx index 942c5730e2222..adc5471cc37a7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx @@ -10,6 +10,7 @@ import styled from 'styled-components'; import { HeaderPage } from '../../components/header_page'; import { StatefulOpenTimeline } from '../../components/open_timeline'; +import { SpyRoute } from '../../utils/route/spy_routes'; import * as i18n from './translations'; @@ -41,6 +42,7 @@ export class TimelinesPage extends React.PureComponent { title={i18n.ALL_TIMELINES_PANEL_TITLE} /> + ); } diff --git a/x-pack/legacy/plugins/siem/public/routes.tsx b/x-pack/legacy/plugins/siem/public/routes.tsx index 37e56f4f067f7..9a132eb8d4fac 100644 --- a/x-pack/legacy/plugins/siem/public/routes.tsx +++ b/x-pack/legacy/plugins/siem/public/routes.tsx @@ -10,16 +10,19 @@ import { Route, Router, Switch } from 'react-router-dom'; import { NotFoundPage } from './pages/404'; import { HomePage } from './pages/home'; +import { ManageRoutesSpy } from './utils/route/manage_spy_routes'; interface RouterProps { history: History; } export const PageRouter: FC = memo(({ history }) => ( - - - - - - + + + + + + + + )); diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts b/x-pack/legacy/plugins/siem/public/store/hosts/model.ts index 29469c129c23f..69efa404d2eee 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/model.ts @@ -14,7 +14,7 @@ export enum HostsType { export enum HostsTableType { authentications = 'authentications', - hosts = 'hosts', + hosts = 'allHosts', events = 'events', uncommonProcesses = 'uncommonProcesses', anomalies = 'anomalies', diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts index 80d7876902203..a597386942cb1 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts @@ -106,8 +106,8 @@ export const hostsReducer = reducerWithInitialState(initialHostsState) ...state[hostsType], queries: { ...state[hostsType].queries, - hosts: { - ...state[hostsType].queries.hosts, + [HostsTableType.hosts]: { + ...state[hostsType].queries[HostsTableType.hosts], direction: sort.direction, sortField: sort.field, }, diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts b/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts index dcd6cd8e67006..a4cf0715ef6da 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts @@ -10,7 +10,7 @@ import { createSelector } from 'reselect'; import { isFromKueryExpressionValid } from '../../lib/keury'; import { State } from '../reducer'; -import { GenericHostsModel, HostsType } from './model'; +import { GenericHostsModel, HostsType, HostsTableType } from './model'; const selectHosts = (state: State, hostsType: HostsType): GenericHostsModel => get(hostsType, state.hosts); @@ -24,7 +24,7 @@ export const authenticationsSelector = () => export const hostsSelector = () => createSelector( selectHosts, - hosts => hosts.queries.hosts + hosts => hosts.queries[HostsTableType.hosts] ); export const eventsSelector = () => diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts index ce84da6ddf129..2507c2cf81c51 100644 --- a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts +++ b/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts @@ -530,6 +530,21 @@ describe('default_date_settings', () => { }); describe('#parseDateWithDefault', () => { + beforeEach(() => { + // Disable momentJS deprecation warning and it looks like it is not typed either so + // we have to disable the type as well and cannot extend it easily. + ((moment as unknown) as { + suppressDeprecationWarnings: boolean; + }).suppressDeprecationWarnings = true; + }); + + afterEach(() => { + // Re-enable momentJS deprecation warning and it looks like it is not typed either so + // we have to disable the type as well and cannot extend it easily. + ((moment as unknown) as { + suppressDeprecationWarnings: boolean; + }).suppressDeprecationWarnings = false; + }); test('should return the first value if it is ok', () => { const value = parseDateWithDefault( '1930-05-31T13:03:54.234Z', diff --git a/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts b/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts new file mode 100644 index 0000000000000..188ae9c6c1866 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts @@ -0,0 +1,23 @@ +/* + * 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 { noop } from 'lodash/fp'; +import { createContext, Dispatch } from 'react'; + +import { RouteSpyState, RouteSpyAction } from './types'; + +export const initRouteSpy: RouteSpyState = { + pageName: '', + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/', +}; + +export const RouterSpyStateContext = createContext<[RouteSpyState, Dispatch]>([ + initRouteSpy, + () => noop, +]); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx b/x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx new file mode 100644 index 0000000000000..bcc256d50d960 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx @@ -0,0 +1,202 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { HostsTableType } from '../../store/hosts/model'; +import { RouteSpyState } from './types'; +import { ManageRoutesSpy } from './manage_spy_routes'; +import { SpyRouteComponent } from './spy_routes'; +import { useRouteSpy } from './use_route_spy'; + +type Action = 'PUSH' | 'POP' | 'REPLACE'; +const pop: Action = 'POP'; + +const defaultLocation = { + hash: '', + pathname: '/hosts', + search: '', + state: '', +}; + +export const mockHistory = { + action: pop, + block: jest.fn(), + createHref: jest.fn(), + go: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + length: 2, + listen: jest.fn(), + location: defaultLocation, + push: jest.fn(), + replace: jest.fn(), +}; + +const dispatchMock = jest.fn(); +const mockRoutes: RouteSpyState = { + pageName: '', + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/', + history: mockHistory, +}; + +const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; +jest.mock('./use_route_spy', () => ({ + useRouteSpy: jest.fn(), +})); + +describe('Spy Routes', () => { + describe('At Initialization of the app', () => { + beforeEach(() => { + dispatchMock.mockReset(); + dispatchMock.mockClear(); + }); + test('Make sure we update search state first', () => { + const pathname = '/'; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + mount( + + + + ); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + type: 'updateSearch', + search: '?importantQueryString="really"', + }, + ]); + }); + + test('Make sure we update search state first and then update the route but keeping the initial search', () => { + const pathname = '/hosts/allHosts'; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + mount( + + + + ); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + type: 'updateSearch', + search: '?importantQueryString="really"', + }, + ]); + + expect(dispatchMock.mock.calls[1]).toEqual([ + { + route: { + detailName: undefined, + history: mockHistory, + pageName: 'hosts', + pathName: pathname, + tabName: HostsTableType.hosts, + }, + type: 'updateRouteWithOutSearch', + }, + ]); + }); + }); + + describe('When app is running', () => { + beforeEach(() => { + dispatchMock.mockReset(); + dispatchMock.mockClear(); + }); + test('Update route should be updated when there is changed detected', () => { + const pathname = '/hosts/allHosts'; + const newPathname = `hosts/${HostsTableType.authentications}`; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + const wrapper = mount( + + ); + + dispatchMock.mockReset(); + dispatchMock.mockClear(); + + wrapper.setProps({ + location: { + hash: '', + pathname: newPathname, + search: '?updated="true"', + state: '', + }, + match: { + isExact: false, + path: newPathname, + url: newPathname, + params: { + pageName: 'hosts', + detailName: undefined, + tabName: HostsTableType.authentications, + search: '', + }, + }, + }); + wrapper.update(); + expect(dispatchMock.mock.calls[0]).toEqual([ + { + route: { + detailName: undefined, + history: mockHistory, + pageName: 'hosts', + pathName: newPathname, + tabName: HostsTableType.authentications, + search: '?updated="true"', + }, + type: 'updateRoute', + }, + ]); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx new file mode 100644 index 0000000000000..87b40c565c758 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useReducer } from 'react'; + +import { ManageRoutesSpyProps, RouteSpyState, RouteSpyAction } from './types'; +import { RouterSpyStateContext, initRouteSpy } from './helpers'; + +export const ManageRoutesSpy = memo(({ children }: ManageRoutesSpyProps) => { + const reducerSpyRoute = (state: RouteSpyState, action: RouteSpyAction) => { + switch (action.type) { + case 'updateRoute': + return action.route; + case 'updateRouteWithOutSearch': + return { ...state, ...action.route }; + case 'updateSearch': + return { ...state, search: action.search }; + default: + return state; + } + }; + + return ( + + {children} + + ); +}); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx new file mode 100644 index 0000000000000..3a02d81272344 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as H from 'history'; +import { isEqual } from 'lodash/fp'; +import { memo, useEffect, useState } from 'react'; +import { withRouter } from 'react-router-dom'; + +import { SpyRouteProps } from './types'; +import { useRouteSpy } from './use_route_spy'; + +export const SpyRouteComponent = memo( + ({ + location: { pathname, search }, + history, + match: { + params: { pageName, detailName, tabName }, + }, + }) => { + const [isInitializing, setIsInitializing] = useState(true); + const [route, dispatch] = useRouteSpy(); + + useEffect(() => { + if (isInitializing && search !== '') { + dispatch({ + type: 'updateSearch', + search, + }); + setIsInitializing(false); + } + }, [search]); + useEffect(() => { + if (pageName && !isEqual(route.pathName, pathname)) { + if (isInitializing && detailName == null) { + dispatch({ + type: 'updateRouteWithOutSearch', + route: { + pageName, + detailName, + tabName, + pathName: pathname, + history, + }, + }); + setIsInitializing(false); + } else { + dispatch({ + type: 'updateRoute', + route: { + pageName, + detailName, + tabName, + search, + pathName: pathname, + history, + }, + }); + } + } + }, [pathname, search, pageName, detailName, tabName]); + return null; + } +); + +export const SpyRoute = withRouter(SpyRouteComponent); diff --git a/x-pack/legacy/plugins/siem/public/utils/route/types.ts b/x-pack/legacy/plugins/siem/public/utils/route/types.ts new file mode 100644 index 0000000000000..62f6b67df245f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as H from 'history'; +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; + +import { HostsTableType } from '../../store/hosts/model'; + +export interface RouteSpyState { + pageName: string; + detailName: string | undefined; + tabName: HostsTableType | undefined; + search: string; + pathName: string; + history?: H.History; +} + +export type RouteSpyAction = + | { + type: 'updateSearch'; + search: string; + } + | { + type: 'updateRouteWithOutSearch'; + route: Pick; + } + | { + type: 'updateRoute'; + route: RouteSpyState; + }; + +export interface ManageRoutesSpyProps { + children: React.ReactNode; +} + +export type SpyRouteProps = RouteComponentProps<{ + pageName: string | undefined; + detailName: string | undefined; + tabName: HostsTableType | undefined; + search: string; +}>; diff --git a/x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx b/x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx new file mode 100644 index 0000000000000..ce988df1c9d2f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx @@ -0,0 +1,10 @@ +/* + * 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 { useContext } from 'react'; +import { RouterSpyStateContext } from './helpers'; + +export const useRouteSpy = () => useContext(RouterSpyStateContext); diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap index 15d2aeef67e99..d8fbe90000f9a 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/space_identifier.test.tsx.snap @@ -7,7 +7,9 @@ exports[`renders without crashing 1`] = ` fullWidth={true} hasEmptyLabelSpace={false} helpText={ -

+

{ }) ) => { return ( -

+

{ const tasks = [[1, 2, 3], [4, 5]]; let index = 0; const fetchAvailableTasks = async () => tasks[index++] || []; - const run = sinon.spy(() => true); + const run = sinon.spy(async () => true); const converter = _.identity; await fillPool(run, fetchAvailableTasks, converter); @@ -25,7 +25,7 @@ describe('fillPool', () => { const tasks = [[1, 2, 3], [4, 5]]; let index = 0; const fetchAvailableTasks = async () => tasks[index++] || []; - const run = sinon.spy(() => false); + const run = sinon.spy(async () => false); const converter = _.identity; await fillPool(run, fetchAvailableTasks, converter); @@ -37,7 +37,7 @@ describe('fillPool', () => { const tasks = [[1, 2, 3], [4, 5]]; let index = 0; const fetchAvailableTasks = async () => tasks[index++] || []; - const run = sinon.spy(() => false); + const run = sinon.spy(async () => false); const converter = (x: number) => x.toString(); await fillPool(run, fetchAvailableTasks, converter); @@ -47,7 +47,7 @@ describe('fillPool', () => { describe('error handling', () => { test('throws exception from fetchAvailableTasks', async () => { - const run = sinon.spy(() => false); + const run = sinon.spy(async () => false); const converter = (x: number) => x.toString(); try { @@ -80,7 +80,7 @@ describe('fillPool', () => { const tasks = [[1, 2, 3], [4, 5]]; let index = 0; const fetchAvailableTasks = async () => tasks[index++] || []; - const run = sinon.spy(() => false); + const run = sinon.spy(async () => false); const converter = (x: number) => { throw new Error(`can not convert ${x}`); }; diff --git a/x-pack/legacy/plugins/task_manager/task_pool.test.ts b/x-pack/legacy/plugins/task_manager/task_pool.test.ts index b883831f2c6f9..76087d9c1add6 100644 --- a/x-pack/legacy/plugins/task_manager/task_pool.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_pool.test.ts @@ -71,11 +71,13 @@ describe('TaskPool', () => { const firstRun = sinon.spy(async () => { await sleep(0); firstWork.resolve(); + return { state: {} }; }); const secondWork = resolvable(); const secondRun = sinon.spy(async () => { await sleep(0); secondWork.resolve(); + return { state: {} }; }); const result = await pool.run([ @@ -192,7 +194,10 @@ describe('TaskPool', () => { }); function mockRun() { - return sinon.spy(async () => sleep(0)); + return sinon.spy(async () => { + await sleep(0); + return { state: {} }; + }); } function mockTask() { diff --git a/x-pack/legacy/plugins/task_manager/task_store.test.ts b/x-pack/legacy/plugins/task_manager/task_store.test.ts index ccc7322682f4c..f33beca0948d9 100644 --- a/x-pack/legacy/plugins/task_manager/task_store.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_store.test.ts @@ -159,7 +159,7 @@ describe('TaskStore', () => { describe('fetch', () => { async function testFetch(opts?: FetchOpts, hits: any[] = []) { - const callCluster = sinon.spy(async () => ({ hits: { hits } })); + const callCluster = sinon.spy(async (name: string, params?: any) => ({ hits: { hits } })); const store = new TaskStore({ index: 'tasky', serializer, @@ -328,7 +328,7 @@ describe('TaskStore', () => { describe('fetchAvailableTasks', () => { async function testFetchAvailableTasks({ opts = {}, hits = [] }: any = {}) { - const callCluster = sinon.spy(async () => ({ hits: { hits } })); + const callCluster = sinon.spy(async (name: string, params?: any) => ({ hits: { hits } })); const store = new TaskStore({ callCluster, logger: mockLogger(), @@ -350,7 +350,7 @@ describe('TaskStore', () => { } test('it returns normally with no tasks when the index does not exist.', async () => { - const callCluster = sinon.spy(async () => ({ hits: { hits: [] } })); + const callCluster = sinon.spy(async (name: string, params?: any) => ({ hits: { hits: [] } })); const store = new TaskStore({ index: 'tasky', serializer, diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap index d41b4c806f5a9..f07d0171b22ad 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/integration_group.test.tsx.snap @@ -62,7 +62,7 @@ exports[`IntegrationGroup will not display infra links when infra is unavailable - - - - - - - - - - - + + + + + For Uptime specific information + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_framework_adapter.ts index 596389f34fb28..dee8a957e54da 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -9,7 +9,6 @@ import { unmountComponentAtNode } from 'react-dom'; import chrome from 'ui/chrome'; import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; import { PLUGIN, INTEGRATED_SOLUTIONS } from '../../../../common/constants'; -import { UMBreadcrumb } from '../../../breadcrumbs'; import { BootstrapUptimeApp, UMFrameworkAdapter } from '../../lib'; import { CreateGraphQLClient } from './framework_adapter_types'; import { renderUptimeKibanaGlobalHelp } from './kibana_global_help'; @@ -44,12 +43,6 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter { $scope.$$postDigest(() => { const elem = document.getElementById('uptimeReactRoot'); - // configure breadcrumbs - let kibanaBreadcrumbs: UMBreadcrumb[] = []; - chrome.breadcrumbs.get$().subscribe((breadcrumbs: UMBreadcrumb[]) => { - kibanaBreadcrumbs = breadcrumbs; - }); - // set up route with current base path const basePath = chrome.getBasePath(); const routerBasename = basePath.endsWith('/') @@ -110,7 +103,6 @@ export class UMKibanaFrameworkAdapter implements UMFrameworkAdapter { isApmAvailable, isInfraAvailable, isLogsAvailable, - kibanaBreadcrumbs, logMonitorPageLoad: getTelemetryMonitorPageLogger(this.xsrfHeader, basePath), logOverviewPageLoad: getTelemetryOverviewPageLogger(this.xsrfHeader, basePath), renderGlobalHelpControls, diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_global_help.tsx b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_global_help.tsx index 59b3001d9e690..7896764d35963 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_global_help.tsx +++ b/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/kibana_global_help.tsx @@ -4,42 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiLink, EuiSpacer, EuiHorizontalRule, EuiButton, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; export const renderUptimeKibanaGlobalHelp = (docsSiteUrl: string, docLinkVersion: string) => ( - - - - - - - - - - - - + + + + For Uptime specific information + + + + + + + + + ); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/get_title.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/get_title.ts new file mode 100644 index 0000000000000..4aa8860de96fa --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/get_title.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. + */ + +import { i18n } from '@kbn/i18n'; + +export const getTitle = (name?: string) => { + const appName = i18n.translate('xpack.uptime.title', { + defaultMessage: 'Uptime', + }); + return `${appName} ${name ? '| ' + name : ''} - Kibana`; +}; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_apm_href.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_apm_href.test.ts.snap index c258c2ec44973..53d336b52bd24 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_apm_href.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_apm_href.test.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getApmHref creates href with base path when present 1`] = `"/foo/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"`; +exports[`getApmHref creates href with base path when present 1`] = `"foo/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"`; exports[`getApmHref does not add a base path or extra slash when base path is empty string 1`] = `"/app/apm#/services?kuery=url.domain:%20%22www.elastic.co%22&rangeFrom=now-15m&rangeTo=now"`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_infra_href.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_infra_href.test.ts.snap index 256d19f636bdd..5094aab1226a1 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_infra_href.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_infra_href.test.ts.snap @@ -1,18 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getInfraHref getInfraContainerHref creates a link for valid parameters 1`] = `"/foo/app/infra#/link-to/container-detail/test-container-id"`; +exports[`getInfraHref getInfraContainerHref creates a link for valid parameters 1`] = `"foo/app/infra#/link-to/container-detail/test-container-id"`; exports[`getInfraHref getInfraContainerHref does not specify a base path when none is available 1`] = `"/app/infra#/link-to/container-detail/test-container-id"`; -exports[`getInfraHref getInfraContainerHref returns the first item when multiple container ids are supplied 1`] = `"/bar/app/infra#/link-to/container-detail/test-container-id"`; +exports[`getInfraHref getInfraContainerHref returns the first item when multiple container ids are supplied 1`] = `"bar/app/infra#/link-to/container-detail/test-container-id"`; -exports[`getInfraHref getInfraIpHref creates a link for valid parameters 1`] = `"/bar/app/infra#/infrastructure/inventory?waffleFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"`; +exports[`getInfraHref getInfraIpHref creates a link for valid parameters 1`] = `"bar/app/infra#/infrastructure/inventory?waffleFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"`; exports[`getInfraHref getInfraIpHref does not specify a base path when none is available 1`] = `"/app/infra#/infrastructure/inventory?waffleFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"`; -exports[`getInfraHref getInfraIpHref returns a url for ors between multiple ips 1`] = `"/foo/app/infra#/infrastructure/inventory?waffleFilter=(expression:'host.ip%20%3A%20152.151.23.192%20or%20host.ip%20%3A%20151.101.202.217',kind:kuery)"`; +exports[`getInfraHref getInfraIpHref returns a url for ors between multiple ips 1`] = `"foo/app/infra#/infrastructure/inventory?waffleFilter=(expression:'host.ip%20%3A%20152.151.23.192%20or%20host.ip%20%3A%20151.101.202.217',kind:kuery)"`; -exports[`getInfraHref getInfraKubernetesHref creates a link for valid parameters 1`] = `"/foo/app/infra#/link-to/pod-detail/test-pod-uid"`; +exports[`getInfraHref getInfraKubernetesHref creates a link for valid parameters 1`] = `"foo/app/infra#/link-to/pod-detail/test-pod-uid"`; exports[`getInfraHref getInfraKubernetesHref does not specify a base path when none is available 1`] = `"/app/infra#/link-to/pod-detail/test-pod-uid"`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_logging_href.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_logging_href.test.ts.snap index 70eda1e5442cd..67402d16d9a27 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_logging_href.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/__snapshots__/get_logging_href.test.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`getLoggingHref creates a container href with base path when present 1`] = `"/bar/app/infra#/logs?logFilter=(expression:'container.id%20:%20test-container-id',kind:kuery)"`; +exports[`getLoggingHref creates a container href with base path when present 1`] = `"bar/app/infra#/logs?logFilter=(expression:'container.id%20:%20test-container-id',kind:kuery)"`; exports[`getLoggingHref creates a container href without a base path if it's an empty string 1`] = `"/app/infra#/logs?logFilter=(expression:'container.id%20:%20test-container-id',kind:kuery)"`; -exports[`getLoggingHref creates a pod href with base path when present 1`] = `"/bar/app/infra#/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"`; +exports[`getLoggingHref creates a pod href with base path when present 1`] = `"bar/app/infra#/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"`; exports[`getLoggingHref creates a pod href without a base path when it's an empty string 1`] = `"/app/infra#/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"`; -exports[`getLoggingHref creates an ip href with base path when present 1`] = `"/bar/app/infra#/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"`; +exports[`getLoggingHref creates an ip href with base path when present 1`] = `"bar/app/infra#/logs?logFilter=(expression:'pod.uid%20:%20test-pod-id',kind:kuery)"`; exports[`getLoggingHref creates an ip href without a base path when it's an empty string 1`] = `"/app/infra#/logs?logFilter=(expression:'host.ip%20%3A%20151.101.202.217',kind:kuery)"`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts index 176674a6896a1..bbf5886cfde73 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts @@ -5,4 +5,4 @@ */ export const addBasePath = (basePath: string, url: string) => - `${basePath.length > 0 ? `/${basePath}` : ''}${url}`; + `${basePath.length > 0 ? `${basePath}` : ''}${url}`; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index bc294dd54d2ed..e8ed1cc00fde0 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -25,6 +25,7 @@ import { useUrlParams } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { BaseLocationOptions } from '../components/functional/ping_list'; import { useTrackPageview } from '../../../infra/public'; +import { getTitle } from '../lib/helper/get_title'; interface MonitorPageProps { logMonitorPageLoad: () => void; @@ -65,6 +66,7 @@ export const MonitorPage = ({ }).then((result: any) => { const { name, url, id } = result.data.monitorPageTitle; const heading: string = name || url || id; + document.title = getTitle(name); setBreadcrumbs(getMonitorPageBreadcrumb(heading, stringifyUrlParams(params))); if (setHeadingText) { setHeadingText(heading); diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index 582eda2acf4ad..1d2534ad8a453 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -14,12 +14,12 @@ import { ApolloProvider } from 'react-apollo'; import { BrowserRouter as Router, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { capabilities } from 'ui/capabilities'; import { I18nContext } from 'ui/i18n'; -import { UMBreadcrumb } from './breadcrumbs'; import { UMGraphQLClient, UMUpdateBreadcrumbs, UMUpdateBadge } from './lib/lib'; import { MonitorPage, OverviewPage } from './pages'; import { UptimeRefreshContext, UptimeSettingsContext, UMSettingsContextValues } from './contexts'; import { UptimeDatePicker } from './components/functional/uptime_date_picker'; import { useUrlParams } from './hooks'; +import { getTitle } from './lib/helper/get_title'; export interface UptimeAppColors { danger: string; @@ -36,7 +36,6 @@ export interface UptimeAppProps { isApmAvailable: boolean; isInfraAvailable: boolean; isLogsAvailable: boolean; - kibanaBreadcrumbs: UMBreadcrumb[]; logMonitorPageLoad: () => void; logOverviewPageLoad: () => void; routerBasename: string; @@ -79,7 +78,6 @@ const Application = (props: UptimeAppProps) => { warning: euiLightVars.euiColorWarning, }; } - const [lastRefresh, setLastRefresh] = useState(Date.now()); const [headingText, setHeadingText] = useState(undefined); @@ -100,6 +98,10 @@ const Application = (props: UptimeAppProps) => { ); }, []); + useEffect(() => { + document.title = getTitle(); + }, []); + const refreshApp = () => { setLastRefresh(Date.now()); }; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts index f274d974305f3..09010e9c76096 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/pings/__tests__/elasticsearch_pings_adapter.test.ts @@ -114,7 +114,7 @@ describe('ElasticsearchPingsAdapter class', () => { head: async (request: any, params: any) => null, }; const pingAdapter = new ElasticsearchPingsAdapter(pingDatabase); - const result = await pingAdapter.getPingHistogram(serverRequest, '1234', '5678', null); + const result = await pingAdapter.getPingHistogram(serverRequest, 'now-15m', 'now', null); expect(pingDatabase.search).toHaveBeenCalledTimes(1); expect(result).toEqual([]); }); @@ -131,7 +131,7 @@ describe('ElasticsearchPingsAdapter class', () => { head: async (request: any, params: any) => null, }; const pingAdapter = new ElasticsearchPingsAdapter(pingDatabase); - const result = await pingAdapter.getPingHistogram(serverRequest, '1234', '5678', null); + const result = await pingAdapter.getPingHistogram(serverRequest, 'now-15m', 'now', null); expect(pingDatabase.search).toHaveBeenCalledTimes(1); expect(result).toMatchSnapshot(); @@ -192,8 +192,8 @@ describe('ElasticsearchPingsAdapter class', () => { const pingAdapter = new ElasticsearchPingsAdapter(pingDatabase); const result = await pingAdapter.getPingHistogram( serverRequest, - '1234', - '5678', + 'now-15m', + 'now', JSON.stringify(searchFilter) ); @@ -248,8 +248,8 @@ describe('ElasticsearchPingsAdapter class', () => { const pingAdapter = new ElasticsearchPingsAdapter(pingDatabase); const result = await pingAdapter.getPingHistogram( serverRequest, - '1234', - '5678', + 'now-15m', + 'now', searchFilter ); @@ -270,8 +270,8 @@ describe('ElasticsearchPingsAdapter class', () => { const pingAdapter = new ElasticsearchPingsAdapter(pingDatabase); const result = await pingAdapter.getPingHistogram( serverRequest, - '1234', - '5678', + 'now-15m', + 'now', searchFilter ); @@ -294,8 +294,8 @@ describe('ElasticsearchPingsAdapter class', () => { const pingAdapter = new ElasticsearchPingsAdapter(pingDatabase); const result = await pingAdapter.getPingHistogram( serverRequest, - '1234', - '5678', + 'now-15m', + 'now', searchFilter ); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts index a023358a7f0a6..e33ae02036224 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts @@ -92,6 +92,8 @@ export type TestSubjects = | 'watchActionAccordion' | 'watchActionAccordion.mockComboBox' | 'watchActionsPanel' + | 'watchThresholdButton' + | 'watchThresholdInput' | 'watchConditionTitle' | 'watchTimeFieldSelect' | 'watchVisualizationChart' diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 4094c26d9cd32..f281408c9d439 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -190,6 +190,45 @@ describe.skip(' create route', () => { expect(exists('watchVisualizationChart')).toBe(true); expect(exists('watchActionsPanel')).toBe(true); }); + + describe('watch conditions', () => { + beforeEach(async () => { + const { form, find, component } = testBed; + + // Name, index and time fields are required before the watch condition expression renders + form.setInputValue('nameInput', 'my_test_watch'); + find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox + form.setInputValue('watchTimeFieldSelect', '@timestamp'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + }); + + test('should require a threshold value', async () => { + const { form, find, component } = testBed; + + find('watchThresholdButton').simulate('click'); + + // Provide invalid value + form.setInputValue('watchThresholdInput', ''); + + expect(form.getErrorsMessages()).toContain('A value is required.'); + + // Provide valid value + form.setInputValue('watchThresholdInput', '0'); + + // @ts-ignore (remove when react 16.9.0 is released) + await act(async () => { + await nextTick(); + component.update(); + }); + + expect(form.getErrorsMessages().length).toEqual(0); + }); + }); }); describe('actions', () => { diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js b/x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js index 5f46f1400c57a..af995d6594a38 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js +++ b/x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js @@ -206,7 +206,7 @@ export class ThresholdWatch extends BaseWatch { Array.from(Array(comparators[this.thresholdComparator].requiredValues)).forEach((value, i) => { const key = `threshold${i}`; errors[key] = []; - if (!this.threshold[i]) { + if (this.threshold[i] == null || this.threshold[i] === '') { errors[key].push(i18n.translate( 'xpack.watcher.thresholdWatchExpression.thresholdLevel.valueIsRequiredValidationMessage', { diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx index 4d092895b4149..1041213d355ba 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx +++ b/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx @@ -698,6 +698,7 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { id="watchThresholdPopover" button={ { errors={errors} > { setupXPackMain(server); const { types: savedObjectTypes } = server.savedObjects; - registerOssFeatures(server.plugins.xpack_main.registerFeature, savedObjectTypes, server.config().get('timelion.ui.enabled')); + const config = server.config(); + const isTimelionUiEnabled = config.get('timelion.enabled') && config.get('timelion.ui.enabled'); + registerOssFeatures(server.plugins.xpack_main.registerFeature, savedObjectTypes, isTimelionUiEnabled); // register routes xpackInfoRoute(server); diff --git a/x-pack/package.json b/x-pack/package.json index 06404068b1d65..a4e4437a9e3d0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -47,7 +47,7 @@ "@types/base64-js": "^1.2.5", "@types/boom": "^7.2.0", "@types/cheerio": "^0.22.10", - "@types/chroma-js": "^1.4.1", + "@types/chroma-js": "^1.4.2", "@types/color": "^3.0.0", "@types/d3-array": "^1.2.1", "@types/d3-scale": "^2.0.0", @@ -60,7 +60,7 @@ "@types/glob": "^7.1.1", "@types/graphql": "^0.13.1", "@types/hapi__wreck": "^15.0.1", - "@types/history": "^4.6.2", + "@types/history": "^4.7.3", "@types/jest": "^24.0.9", "@types/joi": "^13.4.2", "@types/js-yaml": "^3.11.1", @@ -72,12 +72,12 @@ "@types/memoize-one": "^4.1.0", "@types/mime": "^2.0.1", "@types/mkdirp": "^0.5.2", - "@types/mocha": "^5.2.6", + "@types/mocha": "^5.2.7", "@types/nock": "^10.0.3", "@types/node": "^10.12.27", "@types/node-fetch": "^2.5.0", "@types/nodemailer": "^6.2.1", - "@types/object-hash": "^1.2.0", + "@types/object-hash": "^1.3.0", "@types/papaparse": "^4.5.11", "@types/pngjs": "^3.3.1", "@types/prop-types": "^15.5.3", @@ -94,7 +94,7 @@ "@types/reduce-reducers": "^0.3.0", "@types/redux-actions": "^2.2.1", "@types/rimraf": "^2.0.2", - "@types/sinon": "^7.0.0", + "@types/sinon": "^7.0.13", "@types/storybook__addon-actions": "^3.4.3", "@types/storybook__addon-info": "^4.1.2", "@types/storybook__addon-knobs": "^5.0.3", @@ -121,7 +121,7 @@ "commander": "3.0.0", "copy-webpack-plugin": "^5.0.4", "cypress": "^3.4.1", - "del": "^4.0.0", + "del": "^4.1.1", "dotenv": "2.0.0", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", @@ -166,7 +166,7 @@ "sass-loader": "^7.3.1", "sass-resources-loader": "^2.0.1", "simple-git": "1.116.0", - "sinon": "^7.2.2", + "sinon": "^7.4.2", "string-replace-loader": "^2.2.0", "supertest": "^3.1.0", "supertest-as-promised": "^4.0.2", diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 6a88c5cddc441..c60605cf71715 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -129,7 +129,7 @@ export async function setupAuthentication({ }); } - authLogger.info('Could not handle authentication attempt'); + authLogger.debug('Could not handle authentication attempt'); return response.unauthorized({ headers: authenticationResult.authResponseHeaders, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b8938d2cae174..7885fbaad4660 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -506,24 +506,10 @@ "common.ui.savedObjects.confirmModal.overwriteTitle": "{name} を上書きしますか?", "common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel": "{name} を保存", "common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage": "「{title}」というタイトルの {name} が既に存在します。保存を続けますか?", - "kibana-react.savedObjects.finder.filterButtonLabel": "タイプ", - "kibana-react.savedObjects.finder.searchPlaceholder": "検索…", - "kibana-react.savedObjects.finder.sortAsc": "昇順", - "kibana-react.savedObjects.finder.sortAuto": "ベストマッチ", - "kibana-react.savedObjects.finder.sortButtonLabel": "並べ替え", - "kibana-react.savedObjects.finder.sortDesc": "降順", "common.ui.savedObjects.howToSaveAsNewDescription": "Kibana の以前のバージョンでは、{savedObjectName} の名前を変更すると新しい名前でコピーが作成されました。今後この操作を行うには、「新規 {savedObjectName} として保存」を使用します。", "common.ui.savedObjects.overwriteRejectedDescription": "上書き確認が拒否されました", "common.ui.savedObjects.saveAsNewLabel": "新規 {savedObjectName} として保存", "common.ui.savedObjects.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", - "kibana-react.savedObjects.saveModal.cancelButtonLabel": "キャンセル", - "kibana-react.savedObjects.saveModal.confirmSaveButtonLabel": "保存の確認", - "kibana-react.savedObjects.saveModal.duplicateTitleDescription": "重複タイトルで {objectType} を保存するには {confirmSaveLabel} をクリックしてください。", - "kibana-react.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText": "保存の確認", - "kibana-react.savedObjects.saveModal.duplicateTitleLabel": "「{title}」というタイトルの {objectType} が既に存在します。", - "kibana-react.savedObjects.saveModal.saveAsNewLabel": "新規 {objectType} として保存", - "kibana-react.savedObjects.saveModal.saveTitle": "{objectType} を保存", - "kibana-react.savedObjects.saveModal.titleLabel": "タイトル", "common.ui.scriptingLanguages.errorFetchingToastDescription": "Elasticsearch から利用可能なスクリプト言語の取得中にエラーが発生しました", "common.ui.share.contextMenu.embedCodeLabel": "埋め込みコード", "common.ui.share.contextMenu.embedCodePanelTitle": "埋め込みコード", @@ -595,6 +581,20 @@ "common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "バウンドを取得できませんでした", "common.ui.welcomeErrorMessage": "Kibana が正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。", "common.ui.welcomeMessage": "Kibana を読み込み中", + "kibana-react.savedObjects.finder.filterButtonLabel": "タイプ", + "kibana-react.savedObjects.finder.searchPlaceholder": "検索…", + "kibana-react.savedObjects.finder.sortAsc": "昇順", + "kibana-react.savedObjects.finder.sortAuto": "ベストマッチ", + "kibana-react.savedObjects.finder.sortButtonLabel": "並べ替え", + "kibana-react.savedObjects.finder.sortDesc": "降順", + "kibana-react.savedObjects.saveModal.cancelButtonLabel": "キャンセル", + "kibana-react.savedObjects.saveModal.confirmSaveButtonLabel": "保存の確認", + "kibana-react.savedObjects.saveModal.duplicateTitleDescription": "重複タイトルで {objectType} を保存するには {confirmSaveLabel} をクリックしてください。", + "kibana-react.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText": "保存の確認", + "kibana-react.savedObjects.saveModal.duplicateTitleLabel": "「{title}」というタイトルの {objectType} が既に存在します。", + "kibana-react.savedObjects.saveModal.saveAsNewLabel": "新規 {objectType} として保存", + "kibana-react.savedObjects.saveModal.saveTitle": "{objectType} を保存", + "kibana-react.savedObjects.saveModal.titleLabel": "タイトル", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonAreaLabel": "全画面モードを終了", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "全画面を終了", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "ESC キーで全画面モードを修了します。", @@ -3259,7 +3259,6 @@ "tsvb.getInterval.secondsLabel": "秒", "tsvb.getInterval.weeksLabel": "週間", "tsvb.getInterval.yearsLabel": "年", - "tsvb.horizontalLegend.toggleChartAriaLabel": "チャートの凡例を切り替える", "tsvb.iconSelect.asteriskLabel": "アスタリスク", "tsvb.iconSelect.bellLabel": "ベル", "tsvb.iconSelect.boltLabel": "ボルト", @@ -3564,7 +3563,6 @@ "tsvb.validateInterval.notifier.maxBucketsExceededErrorMessage": "バケットの最高数を超えました。{buckets} が {maxBuckets} を超えています。パネルオプションでより広い間隔を試してみてください。", "tsvb.vars.variableNameAriaLabel": "変数名", "tsvb.vars.variableNamePlaceholder": "変数名", - "tsvb.verticalLegend.toggleChartAriaLabel": "チャートの凡例を切り替える", "tsvb.visEditorVisualization.applyChangesLabel": "変更を適用", "tsvb.visEditorVisualization.autoApplyLabel": "自動適用", "tsvb.visEditorVisualization.changesHaveNotBeenAppliedMessage": "ビジュアライゼーションへの変更が適用されました。", @@ -4058,7 +4056,7 @@ "xpack.beatsManagement.tagTable.typeColumnName": "タイプ", "xpack.beatsManagement.walkthrough.initial.betaBadgeText": "ベータ", "xpack.canvas.badge.readOnly.text": "読み込み専用", - "xpack.canvas.badge.readOnly.tooltip": "Canvas ワークパッドを保存できません", + "xpack.canvas.badge.readOnly.tooltip": "{canvas} ワークパッドを保存できません", "xpack.canvas.elements.areaChartDisplayName": "面グラフ", "xpack.canvas.elements.areaChartHelpText": "塗りつぶされた折れ線グラフ", "xpack.canvas.elements.bubbleChartDisplayName": "バブルチャート", @@ -4219,7 +4217,6 @@ "xpack.canvas.functions.timefilter.invalidStringErrorMessage": "無効な日付/時刻文字列: 「{str}」", "xpack.canvas.functions.timefilterControl.args.columnHelpText": "フィルターを添付する列またはフィールドです", "xpack.canvas.functions.timefilterControl.args.compactHelpText": "時間フィルターを、ポップオーバーを実行するボタンとして表示します", - "xpack.canvas.sampleDataLinkLabel": "Canvas", "xpack.code.featureRegistry.codeFeatureName": "コード", "xpack.crossClusterReplication.addAutoFollowPatternButtonLabel": "自動フォローパターンを作成", "xpack.crossClusterReplication.addBreadcrumbTitle": "追加", @@ -4227,8 +4224,6 @@ "xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle": "クラスター横断レプリケーションアプリ", "xpack.crossClusterReplication.app.deniedPermissionDescription": "クラスター横断レプリケーションを使用するには、{clusterPrivilegesCount, plural, one {次のクラスター特権} other {次のクラスター特権}}が必要です: {clusterPrivileges}。", "xpack.crossClusterReplication.app.deniedPermissionTitle": "クラスター特権が足りません", - "xpack.crossClusterReplication.app.licenseErrorLinkText": "ライセンスの管理。", - "xpack.crossClusterReplication.app.licenseErrorTitle": "ライセンスエラー", "xpack.crossClusterReplication.app.permissionCheckErrorTitle": "パーミッションの確認中にエラーが発生", "xpack.crossClusterReplication.app.permissionCheckTitle": "パーミッションを確認中…", "xpack.crossClusterReplication.appTitle": "クラスター横断レプリケーション", @@ -4642,16 +4637,9 @@ "xpack.graph.topNavMenu.newWorkspaceAriaLabel": "新規ワークスペース", "xpack.graph.topNavMenu.newWorkspaceLabel": "新規", "xpack.graph.topNavMenu.newWorkspaceTooltip": "新規ワークスペースを作成します", - "xpack.graph.topNavMenu.save.confirmButtonAriaLabel": "ワークスペースを保存", - "xpack.graph.topNavMenu.save.confirmButtonLabel": "保存", "xpack.graph.topNavMenu.save.descriptionInputLabel": "説明", - "xpack.graph.topNavMenu.save.descriptionInputPlaceholder": "メモ…", - "xpack.graph.topNavMenu.save.nameInputLabel": "名前", - "xpack.graph.topNavMenu.save.nameInputPlaceholder": "グラフワークスペース名", "xpack.graph.topNavMenu.save.saveConfigurationOnlyText": "このワークスペースのデータは消去され、構成のみが保存されます", - "xpack.graph.topNavMenu.save.saveConfigurationOnlyWarning": "このワークスペースのデータは消去され、構成のみが保存されます", "xpack.graph.topNavMenu.save.saveGraphContentCheckboxLabel": "Graph コンテンツを保存", - "xpack.graph.topNavMenu.save.saveWorkspaceTitle": "ワークスペースの保存", "xpack.graph.topNavMenu.saveWorkspace.disabledTooltip": "現在の保存ポリシーでは、保存されたワークスペースへの変更が許可されていません", "xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel": "ワークスペースを保存", "xpack.graph.topNavMenu.saveWorkspace.enabledLabel": "保存", @@ -7183,7 +7171,6 @@ "xpack.ml.timeSeriesExplorer.loadingLabel": "読み込み中", "xpack.ml.timeSeriesExplorer.noResultsFoundLabel": "結果が見つかりませんでした", "xpack.ml.timeSeriesExplorer.noSingleMetricJobsFoundLabel": "シングルメトリックジョブが見つかりませんでした", - "xpack.ml.timeSeriesExplorer.refreshButtonAriaLabel": "更新", "xpack.ml.timeSeriesExplorer.requestedDetectorIndexNotValidWarningMessage": "リクエストされた検知器インデックス {detectorIndex} はジョブ {jobId} に有効ではありません", "xpack.ml.timeSeriesExplorer.runControls.durationLabel": "期間", "xpack.ml.timeSeriesExplorer.runControls.forecastMaximumLengthHelpText": "予想の長さで、最長 {maximumForecastDurationDays} 日です。秒には s、分には m、時間には h、日には d、週には w を使います。", @@ -10386,4 +10373,4 @@ "xpack.watcher.watchActions.logging.logTextIsRequiredValidationMessage": "ログテキストが必要です。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 950fe053ca5e6..1285cb288f3f4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -506,24 +506,10 @@ "common.ui.savedObjects.confirmModal.overwriteTitle": "覆盖“{name}”?", "common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel": "保存“{name}”", "common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage": "具有标题 “{title}” 的 “{name}” 已存在。是否确定要保存?", - "kibana-react.savedObjects.finder.filterButtonLabel": "类型", - "kibana-react.savedObjects.finder.searchPlaceholder": "搜索……", - "kibana-react.savedObjects.finder.sortAsc": "升序", - "kibana-react.savedObjects.finder.sortAuto": "最佳匹配", - "kibana-react.savedObjects.finder.sortButtonLabel": "排序", - "kibana-react.savedObjects.finder.sortDesc": "降序", "common.ui.savedObjects.howToSaveAsNewDescription": "在 Kibana 的以前版本中,更改 {savedObjectName} 的名称将创建具有新名称的副本。使用“另存为新的 {savedObjectName}” 复选框可立即达到此目的。", "common.ui.savedObjects.overwriteRejectedDescription": "已拒绝覆盖确认", "common.ui.savedObjects.saveAsNewLabel": "另存为新的 {savedObjectName}", "common.ui.savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", - "kibana-react.savedObjects.saveModal.cancelButtonLabel": "取消", - "kibana-react.savedObjects.saveModal.confirmSaveButtonLabel": "确认保存", - "kibana-react.savedObjects.saveModal.duplicateTitleDescription": "单击 “{confirmSaveLabel}” 以保存标题重复的{objectType}", - "kibana-react.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText": "确认保存", - "kibana-react.savedObjects.saveModal.duplicateTitleLabel": "具有标题 “{title}” 的 {objectType} 已存在。", - "kibana-react.savedObjects.saveModal.saveAsNewLabel": "另存为新的{objectType}", - "kibana-react.savedObjects.saveModal.saveTitle": "保存{objectType}", - "kibana-react.savedObjects.saveModal.titleLabel": "标题", "common.ui.scriptingLanguages.errorFetchingToastDescription": "从 Elasticsearch 获取可用的脚本语言时出错", "common.ui.share.contextMenu.embedCodeLabel": "嵌入代码", "common.ui.share.contextMenu.embedCodePanelTitle": "嵌入代码", @@ -595,6 +581,20 @@ "common.ui.visualize.queryGeohashBounds.unableToGetBoundErrorTitle": "无法获取边界", "common.ui.welcomeErrorMessage": "Kibana 未正确加载。检查服务器输出以了解详情。", "common.ui.welcomeMessage": "正在加载 Kibana", + "kibana-react.savedObjects.finder.filterButtonLabel": "类型", + "kibana-react.savedObjects.finder.searchPlaceholder": "搜索……", + "kibana-react.savedObjects.finder.sortAsc": "升序", + "kibana-react.savedObjects.finder.sortAuto": "最佳匹配", + "kibana-react.savedObjects.finder.sortButtonLabel": "排序", + "kibana-react.savedObjects.finder.sortDesc": "降序", + "kibana-react.savedObjects.saveModal.cancelButtonLabel": "取消", + "kibana-react.savedObjects.saveModal.confirmSaveButtonLabel": "确认保存", + "kibana-react.savedObjects.saveModal.duplicateTitleDescription": "单击 “{confirmSaveLabel}” 以保存标题重复的{objectType}", + "kibana-react.savedObjects.saveModal.duplicateTitleDescription.confirmSaveText": "确认保存", + "kibana-react.savedObjects.saveModal.duplicateTitleLabel": "具有标题 “{title}” 的 {objectType} 已存在。", + "kibana-react.savedObjects.saveModal.saveAsNewLabel": "另存为新的{objectType}", + "kibana-react.savedObjects.saveModal.saveTitle": "保存{objectType}", + "kibana-react.savedObjects.saveModal.titleLabel": "标题", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonAreaLabel": "退出全屏模式", "kibana-react.exitFullScreenButton.exitFullScreenModeButtonLabel": "退出全屏", "kibana-react.exitFullScreenButton.fullScreenModeDescription": "在全屏模式下,按 ESC 键可退出。", @@ -3259,7 +3259,6 @@ "tsvb.getInterval.secondsLabel": "秒", "tsvb.getInterval.weeksLabel": "周", "tsvb.getInterval.yearsLabel": "年", - "tsvb.horizontalLegend.toggleChartAriaLabel": "切换图例", "tsvb.iconSelect.asteriskLabel": "星号", "tsvb.iconSelect.bellLabel": "钟铃", "tsvb.iconSelect.boltLabel": "闪电", @@ -3564,7 +3563,6 @@ "tsvb.validateInterval.notifier.maxBucketsExceededErrorMessage": "超过最大桶数:{buckets} 大于 {maxBuckets},请在面板选项中尝试更大的时间间隔。", "tsvb.vars.variableNameAriaLabel": "变量名称", "tsvb.vars.variableNamePlaceholder": "变量名称", - "tsvb.verticalLegend.toggleChartAriaLabel": "切换图例", "tsvb.visEditorVisualization.applyChangesLabel": "应用更改", "tsvb.visEditorVisualization.autoApplyLabel": "自动应用", "tsvb.visEditorVisualization.changesHaveNotBeenAppliedMessage": "未应用对此可视化的更改。", @@ -4058,7 +4056,7 @@ "xpack.beatsManagement.tagTable.typeColumnName": "类型", "xpack.beatsManagement.walkthrough.initial.betaBadgeText": "公测版", "xpack.canvas.badge.readOnly.text": "只读", - "xpack.canvas.badge.readOnly.tooltip": "无法保存 Canvas Workpad", + "xpack.canvas.badge.readOnly.tooltip": "无法保存 {canvas} Workpad", "xpack.canvas.elements.areaChartDisplayName": "面积图", "xpack.canvas.elements.areaChartHelpText": "已填充主体的折线图", "xpack.canvas.elements.bubbleChartDisplayName": "气泡图", @@ -4219,7 +4217,6 @@ "xpack.canvas.functions.timefilter.invalidStringErrorMessage": "无效的日期/时间字符串:“{str}”", "xpack.canvas.functions.timefilterControl.args.columnHelpText": "附加筛选的列字段", "xpack.canvas.functions.timefilterControl.args.compactHelpText": "将时间筛选显示为触发弹出框的按钮", - "xpack.canvas.sampleDataLinkLabel": "Canvas", "xpack.code.adminPage.langserverTab.installedText": "已安装", "xpack.code.adminPage.langserverTab.languageServersDescription": "{serverCount} {serverCount, plural, one {服务器} other {服务器}}", "xpack.code.adminPage.langserverTab.notInstalledText": "未安装", @@ -4370,8 +4367,6 @@ "xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle": "跨集群复制应用", "xpack.crossClusterReplication.app.deniedPermissionDescription": "要使用跨集群复制,您必须具有{clusterPrivilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{clusterPrivileges}。", "xpack.crossClusterReplication.app.deniedPermissionTitle": "您缺少集群权限", - "xpack.crossClusterReplication.app.licenseErrorLinkText": "管理您的许可。", - "xpack.crossClusterReplication.app.licenseErrorTitle": "许可错误", "xpack.crossClusterReplication.app.permissionCheckErrorTitle": "检查权限时出错", "xpack.crossClusterReplication.app.permissionCheckTitle": "正在检查权限......", "xpack.crossClusterReplication.appTitle": "跨集群复制", @@ -4785,16 +4780,9 @@ "xpack.graph.topNavMenu.newWorkspaceAriaLabel": "新建工作空间", "xpack.graph.topNavMenu.newWorkspaceLabel": "新建", "xpack.graph.topNavMenu.newWorkspaceTooltip": "新建工作空间", - "xpack.graph.topNavMenu.save.confirmButtonAriaLabel": "保存工作空间", - "xpack.graph.topNavMenu.save.confirmButtonLabel": "保存", "xpack.graph.topNavMenu.save.descriptionInputLabel": "描述", - "xpack.graph.topNavMenu.save.descriptionInputPlaceholder": "任何备注......", - "xpack.graph.topNavMenu.save.nameInputLabel": "名称", - "xpack.graph.topNavMenu.save.nameInputPlaceholder": "Graph 工作空间名称", "xpack.graph.topNavMenu.save.saveConfigurationOnlyText": "将清除此工作空间的数据,仅保存配置", - "xpack.graph.topNavMenu.save.saveConfigurationOnlyWarning": "将清除此工作空间的数据,仅保存配置", "xpack.graph.topNavMenu.save.saveGraphContentCheckboxLabel": "保存 Graph 内容", - "xpack.graph.topNavMenu.save.saveWorkspaceTitle": "保存工作空间", "xpack.graph.topNavMenu.saveWorkspace.disabledTooltip": "当前保存策略不允许对已保存的工作空间做任何更改", "xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel": "保存工作空间", "xpack.graph.topNavMenu.saveWorkspace.enabledLabel": "保存", @@ -7325,7 +7313,6 @@ "xpack.ml.timeSeriesExplorer.loadingLabel": "正在加载", "xpack.ml.timeSeriesExplorer.noResultsFoundLabel": "找不到结果", "xpack.ml.timeSeriesExplorer.noSingleMetricJobsFoundLabel": "未找到单指标作业", - "xpack.ml.timeSeriesExplorer.refreshButtonAriaLabel": "刷新", "xpack.ml.timeSeriesExplorer.requestedDetectorIndexNotValidWarningMessage": "请求的检测工具索引 {detectorIndex} 对于作业 {jobId} 无效", "xpack.ml.timeSeriesExplorer.runControls.durationLabel": "持续时间", "xpack.ml.timeSeriesExplorer.runControls.forecastMaximumLengthHelpText": "预测时长,最多 {maximumForecastDurationDays} 天。使用 s 表示秒,m 表示分钟,h 表示小时,d 表示天,w 表示周。", @@ -10528,4 +10515,4 @@ "xpack.watcher.watchActions.logging.logTextIsRequiredValidationMessage": "“日志文本”必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} +} \ No newline at end of file diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 4bd207e51a941..1bfc50e4c83db 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -54,6 +54,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) serverArgs: [ ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), '--xpack.actions.enabled=true', + `--xpack.actions.whitelistedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + ])}`, '--xpack.alerting.enabled=true', ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 495691cb386c6..89fc986fd0255 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -162,6 +162,27 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(result.status).to.eql('ok'); }); + it('should handle target webhooks that are not whitelisted', async () => { + const { body: result } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'test') + .send({ + description: 'A generic Webhook action', + actionTypeId: '.webhook', + secrets: { + user: 'username', + password: 'mypassphrase', + }, + config: { + url: 'http://a.none.whitelisted.webhook/endpoint', + }, + }) + .expect(400); + + expect(result.error).to.eql('Bad Request'); + expect(result.message).to.match(/not in the Kibana whitelist/); + }); + it('should handle unreachable webhook targets', async () => { const webhookActionId = await createWebhookAction('http://some.non.existent.com/endpoint'); const { body: result } = await supertest @@ -177,7 +198,6 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(result.status).to.eql('error'); expect(result.message).to.match(/Unreachable Remote Webhook/); }); - it('should handle failing webhook targets', async () => { const webhookActionId = await createWebhookAction(webhookSimulatorURL); const { body: result } = await supertest diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 0780efc0fc977..7f67f2f5b60e7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -21,5 +21,6 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); + loadTestFile(require.resolve('./builtin_action_types/webhook')); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts index d3d5ca592ce63..4c2dc3cbdf11f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts @@ -19,7 +19,7 @@ export default function alertingApiIntegrationTests({ const esArchiver = getService('esArchiver'); describe('alerting api integration security and spaces enabled', function() { - this.tags('ciGroup3'); + this.tags('ciGroup8'); before(async () => { for (const space of Spaces) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts index 8c59e39818619..dfbb2cca81a49 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/index.ts @@ -17,7 +17,7 @@ export default function alertingApiIntegrationTests({ const esArchiver = getService('esArchiver'); describe('alerting api integration spaces only', function() { - this.tags('ciGroup3'); + this.tags('ciGroup8'); before(async () => { for (const space of Object.values(Spaces)) { diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 807a808e7f9d3..99694c250ef3f 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -6,7 +6,7 @@ export default function ({ loadTestFile }) { describe('apis', function () { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./es')); loadTestFile(require.resolve('./security')); diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index 2174b578abff5..4d034622427fc 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -6,7 +6,7 @@ export default function ({ loadTestFile }) { describe('security', function () { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); diff --git a/x-pack/test/api_integration/apis/spaces/index.ts b/x-pack/test/api_integration/apis/spaces/index.ts index f3f96b891db07..adcf70d032e0f 100644 --- a/x-pack/test/api_integration/apis/spaces/index.ts +++ b/x-pack/test/api_integration/apis/spaces/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { describe('spaces', function() { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./saved_objects')); loadTestFile(require.resolve('./space_attributes')); diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts index 977b6fca549c3..945af09183f03 100644 --- a/x-pack/test/functional/apps/apm/index.ts +++ b/x-pack/test/functional/apps/apm/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { describe('APM', function() { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/functional/apps/code/code_intelligence.ts b/x-pack/test/functional/apps/code/code_intelligence.ts index 0dbd6ee8c38a6..0213cc27fafe8 100644 --- a/x-pack/test/functional/apps/code/code_intelligence.ts +++ b/x-pack/test/functional/apps/code/code_intelligence.ts @@ -37,7 +37,7 @@ export default function codeIntelligenceFunctionalTests({ expect(spans.length).to.greaterThan(1); const userModelSpan = spans[1]; expect(await userModelSpan.getVisibleText()).to.equal('UserModel'); - await browser.moveMouseTo(userModelSpan); + await userModelSpan.moveMouseTo(); // Expect the go to definition button show up eventually. expect(await exists('codeGoToDefinitionButton')).to.be(true); @@ -180,7 +180,7 @@ export default function codeIntelligenceFunctionalTests({ expect(spans.length).to.greaterThan(1); const userModelSpan = spans[0]; expect(await userModelSpan.getVisibleText()).to.equal('UserModel'); - await browser.moveMouseTo(userModelSpan); + await userModelSpan.moveMouseTo(); // Expect the go to definition button show up eventually. expect(await exists('codeFindReferenceButton')).to.be(true); @@ -231,7 +231,7 @@ export default function codeIntelligenceFunctionalTests({ expect(spans.length).to.greaterThan(1); const asyncSpan = spans[1]; expect(await asyncSpan.getVisibleText()).to.equal('async'); - await browser.moveMouseTo(asyncSpan); + await asyncSpan.moveMouseTo(); // Expect the go to definition button show up eventually. expect(await exists('codeGoToDefinitionButton')).to.be(true); diff --git a/x-pack/test/functional/apps/code/history.ts b/x-pack/test/functional/apps/code/history.ts index 8dfde5a0aaad2..5b2e3cdde336a 100644 --- a/x-pack/test/functional/apps/code/history.ts +++ b/x-pack/test/functional/apps/code/history.ts @@ -25,7 +25,8 @@ export default function manageRepositoriesFunctionalTests({ const existsInvisible = async (selector: string) => await testSubjects.exists(selector, { allowHidden: true }); - describe('History', function() { + // FLAKY: https://github.com/elastic/kibana/issues/37859 + describe.skip('History', function() { this.tags('smoke'); const repositoryListSelector = 'codeRepositoryList codeRepositoryItem'; diff --git a/x-pack/test/functional/apps/dashboard_mode/index.js b/x-pack/test/functional/apps/dashboard_mode/index.js index 5612fced7a25d..2d263834fc311 100644 --- a/x-pack/test/functional/apps/dashboard_mode/index.js +++ b/x-pack/test/functional/apps/dashboard_mode/index.js @@ -6,7 +6,7 @@ export default function ({ loadTestFile }) { describe('dashboard mode', function () { - this.tags('ciGroup2'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./dashboard_view_mode')); }); diff --git a/x-pack/test/functional/apps/index_lifecycle_management/index.ts b/x-pack/test/functional/apps/index_lifecycle_management/index.ts index d85b1af2b2612..9078a9d681e7e 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/index.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Lifecycle Management app', function() { - this.tags('ciGroup2'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index b534f6b69fe5b..b706dc8cce546 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('InfraOps app', function() { - this.tags('ciGroup2'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/functional/apps/license_management/index.ts b/x-pack/test/functional/apps/license_management/index.ts index a41e4f5f4abd1..7524d00a4b8dd 100644 --- a/x-pack/test/functional/apps/license_management/index.ts +++ b/x-pack/test/functional/apps/license_management/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('License app', function() { - this.tags('ciGroup2'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/machine_learning/create_single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/create_single_metric_job.ts index 3973f2aa38257..a8993f7c92c1e 100644 --- a/x-pack/test/functional/apps/machine_learning/create_single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/create_single_metric_job.ts @@ -13,9 +13,15 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); const jobId = `fq_single_1_${Date.now()}`; + const jobDescription = + 'Create single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)'; + const jobGroups = ['automated', 'farequote', 'single-metric']; + const aggAndFieldIdentifier = 'Mean(responsetime)'; + const bucketSpan = '30m'; + const memoryLimit = '15MB'; describe('single metric job creation', function() { - this.tags('smoke'); + this.tags(['smoke', 'mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); }); @@ -58,14 +64,12 @@ export default function({ getService }: FtrProviderContext) { }); it('selects field and aggregation', async () => { - const identifier = 'Mean(responsetime)'; await ml.jobWizardCommon.assertAggAndFieldInputExists(); - await ml.jobWizardCommon.selectAggAndField(identifier); - await ml.jobWizardCommon.assertAggAndFieldSelection(identifier); + await ml.jobWizardCommon.selectAggAndField(aggAndFieldIdentifier); + await ml.jobWizardCommon.assertAggAndFieldSelection(aggAndFieldIdentifier); }); it('inputs the bucket span', async () => { - const bucketSpan = '30m'; await ml.jobWizardCommon.assertBucketSpanInputExists(); await ml.jobWizardCommon.setBucketSpan(bucketSpan); await ml.jobWizardCommon.assertBucketSpanValue(bucketSpan); @@ -83,15 +87,12 @@ export default function({ getService }: FtrProviderContext) { }); it('inputs the job description', async () => { - const jobDescription = - 'Create single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)'; await ml.jobWizardCommon.assertJobDescriptionInputExists(); await ml.jobWizardCommon.setJobDescription(jobDescription); await ml.jobWizardCommon.assertJobDescriptionValue(jobDescription); }); it('inputs job groups', async () => { - const jobGroups = ['automated', 'farequote', 'single-metric']; await ml.jobWizardCommon.assertJobGroupInputExists(); for (const jobGroup of jobGroups) { await ml.jobWizardCommon.addJobGroup(jobGroup); @@ -114,7 +115,6 @@ export default function({ getService }: FtrProviderContext) { }); it('inputs the model memory limit', async () => { - const memoryLimit = '15MB'; await ml.jobWizardCommon.assertModelMemoryLimitInputExists(); await ml.jobWizardCommon.setModelMemoryLimit(memoryLimit); await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit); @@ -138,9 +138,75 @@ export default function({ getService }: FtrProviderContext) { it('displays the created job in the job list', async () => { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobManagement.filterJobsTable(jobId); - const jobRow = await ml.jobManagement.getJobRowByJobId(jobId); - expect(jobRow).to.not.be(null); + + await ml.jobTable.waitForJobsToLoad(); + await ml.jobTable.filterWithSearchString(jobId); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === jobId)).to.have.length(1); + }); + + it('displays details for the created job in the job list', async () => { + const rows = await ml.jobTable.parseJobTable(); + const job = rows.filter(row => row.id === jobId)[0]; + expect(job).to.eql({ + id: jobId, + description: jobDescription, + jobGroups, + recordCount: '2,399', + memoryStatus: 'ok', + jobState: 'closed', + datafeedState: 'stopped', + latestTimestamp: '2016-02-11 23:56:59', + }); + + const countDetails = await ml.jobTable.parseJobCounts(jobId); + const counts = countDetails.counts; + + // last_data_time holds a runtime timestamp and is hard to predict + // the property is only validated to be present and then removed + // so it doesn't make the counts object validation fail + expect(counts).to.have.property('last_data_time'); + delete counts.last_data_time; + + expect(counts).to.eql({ + job_id: jobId, + processed_record_count: '2,399', + processed_field_count: '4,798', + input_bytes: '180.6 KB', + input_field_count: '4,798', + invalid_date_count: '0', + missing_field_count: '0', + out_of_order_timestamp_count: '0', + empty_bucket_count: '0', + sparse_bucket_count: '0', + bucket_count: '239', + earliest_record_timestamp: '2016-02-07 00:02:50', + latest_record_timestamp: '2016-02-11 23:56:59', + input_record_count: '2,399', + latest_bucket_timestamp: '2016-02-11 23:30:00', + }); + + const modelSizeStats = countDetails.modelSizeStats; + + // log_time holds a runtime timestamp and is hard to predict + // the property is only validated to be present and then removed + // so it doesn't make the modelSizeStats object validation fail + expect(modelSizeStats).to.have.property('log_time'); + delete modelSizeStats.log_time; + + expect(modelSizeStats).to.eql({ + job_id: jobId, + result_type: 'model_size_stats', + model_bytes: '47.6 KB', + model_bytes_exceeded: '0', + model_bytes_memory_limit: '15728640', + total_by_field_count: '3', + total_over_field_count: '0', + total_partition_field_count: '2', + bucket_allocation_failures_count: '0', + memory_status: 'ok', + timestamp: '2016-02-11 23:00:00', + }); }); }); } diff --git a/x-pack/test/functional/apps/machine_learning/pages.ts b/x-pack/test/functional/apps/machine_learning/pages.ts index fb085f0661f8c..b3537abb4044b 100644 --- a/x-pack/test/functional/apps/machine_learning/pages.ts +++ b/x-pack/test/functional/apps/machine_learning/pages.ts @@ -11,7 +11,7 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('page navigation', function() { - this.tags('smoke'); + this.tags(['smoke', 'mlqa']); before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/functional/apps/maps/add_layer_panel.js b/x-pack/test/functional/apps/maps/add_layer_panel.js index 60a719f046674..a17772987ab8e 100644 --- a/x-pack/test/functional/apps/maps/add_layer_panel.js +++ b/x-pack/test/functional/apps/maps/add_layer_panel.js @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { await PageObjects.maps.openNewMap(); await PageObjects.maps.clickAddLayer(); - await PageObjects.maps.selectVectorSource(); + await PageObjects.maps.selectEMSBoundariesSource(); await PageObjects.maps.selectVectorLayer(LAYER_NAME); }); diff --git a/x-pack/test/functional/apps/maps/full_screen_mode.js b/x-pack/test/functional/apps/maps/full_screen_mode.js index 36db159704d21..7d89ff1454598 100644 --- a/x-pack/test/functional/apps/maps/full_screen_mode.js +++ b/x-pack/test/functional/apps/maps/full_screen_mode.js @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['maps', 'common']); - const browser = getService('browser'); const retry = getService('retry'); describe('full screen mode', () => { @@ -40,7 +39,7 @@ export default function ({ getService, getPageObjects }) { it('exits when the text button is clicked on', async () => { const logoButton = await PageObjects.maps.getExitFullScreenLogoButton(); - await browser.moveMouseTo(logoButton); + await logoButton.moveMouseTo(); await PageObjects.maps.clickExitFullScreenTextButton(); await retry.try(async () => { diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 76cccc9f5d21a..9880fe41076d0 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -28,7 +28,7 @@ export default function ({ loadTestFile, getService }) { }); describe('', function () { - this.tags('ciGroup2'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./documents_source')); loadTestFile(require.resolve('./saved_object_management')); loadTestFile(require.resolve('./sample_data')); @@ -38,7 +38,7 @@ export default function ({ loadTestFile, getService }) { }); describe('', function () { - this.tags('ciGroup5'); + this.tags('ciGroup10'); loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./joins')); loadTestFile(require.resolve('./add_layer_panel')); diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index c7976741f835e..49519b530337e 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -49,7 +49,7 @@ export const MAPBOX_STYLES = { 'coalesce', [ 'feature-state', - '__kbn__scaled(__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name)' + '__kbn__dynamic____kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name__fillColor' ], -1 ], @@ -123,7 +123,7 @@ export const MAPBOX_STYLES = { 'coalesce', [ 'feature-state', - '__kbn__scaled(__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name)' + '__kbn__dynamic____kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name__fillColor' ], -1 ], diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts index f62400ad62ee7..c4e905b3babd0 100644 --- a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts +++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts @@ -16,8 +16,7 @@ export default function spaceSelectorFunctonalTests({ const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['security', 'settings', 'copySavedObjectsToSpace']); - // FLAKY: https://github.com/elastic/kibana/issues/44575 - describe.skip('Copy Saved Objects to Space', function() { + describe('Copy Saved Objects to Space', function() { before(async () => { await esArchiver.load('spaces/copy_saved_objects'); diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index b5d3c73c96855..c1bc8f856c467 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -18,7 +18,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC' }); }); after(async () => await esArchiver.unload(ARCHIVE)); - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./overview')); diff --git a/x-pack/test/functional/es_archives/ml/farequote/data.json.gz b/x-pack/test/functional/es_archives/ml/farequote/data.json.gz index 47e0d55672bfd..6d84ef28be658 100644 Binary files a/x-pack/test/functional/es_archives/ml/farequote/data.json.gz and b/x-pack/test/functional/es_archives/ml/farequote/data.json.gz differ diff --git a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts index 3908b2ddecf1d..69e79d63d5fd5 100644 --- a/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts +++ b/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts @@ -31,6 +31,9 @@ export function CopySavedObjectsToSpacePageProvider({ getService }: FtrProviderC 'table.euiTable tbody tr.euiTableRow td.euiTableRowCell:last-child .euiButtonIcon' ); + // Wait for context menu to render + await find.existsByCssSelector('.euiContextMenuPanel'); + const actions = await find.allByCssSelector('.euiContextMenuItem'); for (const action of actions) { diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index 553ad6edc72d7..f46abfd28f1fb 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -470,14 +470,14 @@ export function GisPageProvider({ getService, getPageObjects }) { await this.waitForLayersToLoad(); } - async selectVectorSource() { - log.debug(`Select vector source`); - await testSubjects.click('vectorShapes'); + async selectEMSBoundariesSource() { + log.debug(`Select EMS boundaries source`); + await testSubjects.click('emsBoundaries'); } async selectGeoJsonUploadSource() { - log.debug(`Select upload geojson vector file`); - await testSubjects.click('uploadGeoJsonVectorFile'); + log.debug(`Select upload geojson source`); + await testSubjects.click('uploadedGeoJson'); } async uploadJsonFileForIndexing(path) { @@ -601,8 +601,8 @@ export function GisPageProvider({ getService, getPageObjects }) { async lockTooltipAtPosition(xOffset, yOffset) { await retry.try(async () => { const mapContainerElement = await testSubjects.find('mapContainer'); - await browser.moveMouseTo(mapContainerElement, xOffset, yOffset); - await browser.clickMouseButton(mapContainerElement, xOffset, yOffset); + await mapContainerElement.moveMouseTo({ xOffset, yOffset }); + await mapContainerElement.clickMouseButton({ xOffset, yOffset }); // Close button is only displayed with tooltip is locked const hasCloseButton = await testSubjects.exists('mapTooltipCloseButton'); if (!hasCloseButton) { diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index d3af519a66b57..a0ea73e40f6b0 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -164,7 +164,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { async clickSaveEditRole() { const saveButton = await retry.try(() => testSubjects.find('roleFormSaveButton')); - await browser.moveMouseTo(saveButton); + await saveButton.moveMouseTo(); await saveButton.click(); await PageObjects.header.waitUntilLoadingHasFinished(); } diff --git a/x-pack/test/functional/services/machine_learning/index.ts b/x-pack/test/functional/services/machine_learning/index.ts index d2e387d5ea1ba..f6fab4af1a119 100644 --- a/x-pack/test/functional/services/machine_learning/index.ts +++ b/x-pack/test/functional/services/machine_learning/index.ts @@ -10,6 +10,7 @@ export { MachineLearningDataFramesProvider } from './data_frames'; export { MachineLearningDataVisualizerProvider } from './data_visualizer'; export { MachineLearningJobManagementProvider } from './job_management'; export { MachineLearningJobSourceSelectionProvider } from './job_source_selection'; +export { MachineLearningJobTableProvider } from './job_table'; export { MachineLearningJobTypeSelectionProvider } from './job_type_selection'; export { MachineLearningJobWizardCommonProvider } from './job_wizard_common'; export { MachineLearningNavigationProvider } from './navigation'; diff --git a/x-pack/test/functional/services/machine_learning/job_management.ts b/x-pack/test/functional/services/machine_learning/job_management.ts index dcec09def1221..29380c85ad7a4 100644 --- a/x-pack/test/functional/services/machine_learning/job_management.ts +++ b/x-pack/test/functional/services/machine_learning/job_management.ts @@ -5,30 +5,11 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; export function MachineLearningJobManagementProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); - const retry = getService('retry'); return { - async getJobsTable(): Promise { - const tableContainer = await testSubjects.find('mlJobListTable'); - return await tableContainer.findByTagName('table'); - }, - - async isJobsTableLoadingIndicatorDisplayed(): Promise { - const mlJobListTable = await testSubjects.find('mlJobListTable'); - const innerText = await mlJobListTable.getVisibleText(); - return innerText.includes('Loading jobs...'); - }, - - async isNoItemsFoundMessageDisplayed(): Promise { - const mlJobListTable = await testSubjects.find('mlJobListTable'); - const innerText = await mlJobListTable.getVisibleText(); - return innerText.includes('No jobs found'); - }, - async navigateToNewJobSourceSelection() { await testSubjects.clickWhenNotDisabled('mlCreateNewJobButton'); await testSubjects.existOrFail('mlPageSourceSelection'); @@ -45,30 +26,5 @@ export function MachineLearningJobManagementProvider({ getService }: FtrProvider async assertJobStatsBarExists() { await testSubjects.existOrFail('mlJobStatsBar'); }, - - async waitForJobsTableToLoad() { - await retry.waitFor( - 'jobs table to exist', - async () => await testSubjects.exists('mlJobListTable') - ); - - await retry.waitFor( - 'jobs table loading indicator to be invisible', - async () => (await this.isJobsTableLoadingIndicatorDisplayed()) === false - ); - }, - - async filterJobsTable(jobId: string) { - await this.waitForJobsTableToLoad(); - const searchBar = await testSubjects.find('mlJobListSearchBar'); - const searchBarInput = await searchBar.findByTagName('input'); - await searchBarInput.clearValueWithKeyboard(); - await searchBarInput.type(jobId); - }, - - async getJobRowByJobId(jobId: string): Promise { - const table = await this.getJobsTable(); - return await table.findByCssSelector(`[data-row-id=${jobId}]`); - }, }; } diff --git a/x-pack/test/functional/services/machine_learning/job_table.ts b/x-pack/test/functional/services/machine_learning/job_table.ts new file mode 100644 index 0000000000000..8e9c74f7c22c7 --- /dev/null +++ b/x-pack/test/functional/services/machine_learning/job_table.ts @@ -0,0 +1,178 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningJobTableProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + return new (class MlJobTable { + public async parseJobTable() { + const table = await testSubjects.find('mlJobListTable'); + const $ = await table.parseDomContent(); + const rows = []; + + for (const tr of $.findTestSubjects('row').toArray()) { + const $tr = $(tr); + + const $description = $tr.findTestSubject('description').find('.euiTableCellContent'); + const $jobGroups = $description.findTestSubjects('jobGroup'); + const jobGroups = []; + for (const el of $jobGroups.toArray()) { + // collect this group in our array + jobGroups.push( + $(el) + .text() + .trim() + ); + + // remove this element from $description so it doesn't pollute it's text value + $(el).remove(); + } + + rows.push({ + id: $tr + .findTestSubject('id') + .find('.euiTableCellContent') + .text() + .trim(), + description: $description + .text() + .replace(/( $)/g, '') + .trim(), + jobGroups, + recordCount: $tr + .findTestSubject('recordCount') + .find('.euiTableCellContent') + .text() + .trim(), + memoryStatus: $tr + .findTestSubject('memoryStatus') + .find('.euiTableCellContent') + .text() + .trim(), + jobState: $tr + .findTestSubject('jobState') + .find('.euiTableCellContent') + .text() + .trim(), + datafeedState: $tr + .findTestSubject('datafeedState') + .find('.euiTableCellContent') + .text() + .trim(), + latestTimestamp: $tr + .findTestSubject('latestTimestamp') + .find('.euiTableCellContent') + .text() + .trim(), + }); + } + + return rows; + } + + public async parseJobCounts(jobId: string) { + return await this.withDetailsOpen(jobId, async () => { + // click counts tab + await testSubjects.click(this.detailsSelector(jobId, 'tab-counts')); + + const countsTable = await testSubjects.find( + this.detailsSelector(jobId, 'details-counts counts') + ); + const modelSizeStatsTable = await testSubjects.find( + this.detailsSelector(jobId, 'details-counts modelSizeStats') + ); + + // parse a table by reading each row + async function parseTable(el: typeof countsTable) { + const $ = await el.parseDomContent(); + const vars: Record = {}; + + for (const row of $('tr').toArray()) { + const [name, value] = $(row) + .find('td') + .toArray(); + + vars[ + $(name) + .text() + .trim() + ] = $(value) + .text() + .trim(); + } + + return vars; + } + + return { + counts: await parseTable(countsTable), + modelSizeStats: await parseTable(modelSizeStatsTable), + }; + }); + } + + public rowSelector(jobId: string, subSelector?: string) { + const row = `mlJobListTable row-${jobId}`; + return !subSelector ? row : `${row} ${subSelector}`; + } + + public detailsSelector(jobId: string, subSelector?: string) { + const row = `mlJobListTable details-${jobId}`; + return !subSelector ? row : `${row} ${subSelector}`; + } + + public async withDetailsOpen(jobId: string, block: () => Promise): Promise { + await this.ensureDetailsOpen(jobId); + try { + return await block(); + } finally { + await this.ensureDetailsClosed(jobId); + } + } + + public async ensureDetailsOpen(jobId: string) { + await retry.try(async () => { + if (!(await testSubjects.exists(this.detailsSelector(jobId)))) { + await testSubjects.click(this.rowSelector(jobId, 'detailsToggle')); + } + + await testSubjects.existOrFail(this.detailsSelector(jobId)); + }); + } + + public async ensureDetailsClosed(jobId: string) { + await retry.try(async () => { + if (await testSubjects.exists(this.detailsSelector(jobId))) { + await testSubjects.click(this.rowSelector(jobId, 'detailsToggle')); + await testSubjects.missingOrFail(this.detailsSelector(jobId)); + } + }); + } + + public async waitForJobsToLoad() { + await retry.waitFor( + 'jobs table to exist', + async () => await testSubjects.exists('mlJobListTable') + ); + + await retry.waitFor( + 'jobs table to be done loading', + async () => await testSubjects.exists('mlJobListTable&loaded') + ); + } + + public async filterWithSearchString(filter: string) { + await this.waitForJobsToLoad(); + const searchBar = await testSubjects.find('mlJobListSearchBar'); + const searchBarInput = await searchBar.findByTagName('input'); + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(filter); + } + })(); +} diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index b93ad7c9121bc..6e929bd4e21e2 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -13,6 +13,7 @@ import { MachineLearningDataVisualizerProvider, MachineLearningJobManagementProvider, MachineLearningJobSourceSelectionProvider, + MachineLearningJobTableProvider, MachineLearningJobTypeSelectionProvider, MachineLearningJobWizardCommonProvider, MachineLearningNavigationProvider, @@ -27,6 +28,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const dataVisualizer = MachineLearningDataVisualizerProvider(context); const jobManagement = MachineLearningJobManagementProvider(context); const jobSourceSelection = MachineLearningJobSourceSelectionProvider(context); + const jobTable = MachineLearningJobTableProvider(context); const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context); const jobWizardCommon = MachineLearningJobWizardCommonProvider(context); const navigation = MachineLearningNavigationProvider(context); @@ -40,6 +42,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { dataVisualizer, jobManagement, jobSourceSelection, + jobTable, jobTypeSelection, jobWizardCommon, navigation, diff --git a/x-pack/test/kerberos_api_integration/apis/index.ts b/x-pack/test/kerberos_api_integration/apis/index.ts index 6bb924818a672..00818c2b59eee 100644 --- a/x-pack/test/kerberos_api_integration/apis/index.ts +++ b/x-pack/test/kerberos_api_integration/apis/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { describe('apis Kerberos', function() { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./security')); }); } diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/index.js b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/index.js index 85f2a82cc9641..0ef60bb929826 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/index.js +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/index.js @@ -6,7 +6,7 @@ export default function ({ loadTestFile }) { describe('apis', function () { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./oidc_auth')); }); } diff --git a/x-pack/test/oidc_api_integration/apis/implicit_flow/index.ts b/x-pack/test/oidc_api_integration/apis/implicit_flow/index.ts index 0503efea77eab..22ce3b17a5949 100644 --- a/x-pack/test/oidc_api_integration/apis/implicit_flow/index.ts +++ b/x-pack/test/oidc_api_integration/apis/implicit_flow/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function({ loadTestFile }: FtrProviderContext) { describe('apis', function() { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./oidc_auth')); }); } diff --git a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts index 4689ed447632f..0c77ff3a0640e 100644 --- a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts @@ -7,12 +7,14 @@ import expect from '@kbn/expect'; import { JSDOM } from 'jsdom'; import request, { Cookie } from 'request'; +import { format as formatURL } from 'url'; import { createTokens, getStateAndNonce } from '../../fixtures/oidc_tools'; import { FtrProviderContext } from '../../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); + const config = getService('config'); describe('OpenID Connect Implicit Flow authentication', () => { describe('finishing handshake', () => { @@ -31,22 +33,30 @@ export default function({ getService }: FtrProviderContext) { it('should return an HTML page that will parse URL fragment', async () => { const response = await supertest.get('/api/security/v1/oidc/implicit').expect(200); const dom = new JSDOM(response.text, { + url: formatURL({ ...config.get('servers.kibana'), auth: false }), runScripts: 'dangerously', + resources: 'usable', beforeParse(window) { // JSDOM doesn't support changing of `window.location` and throws an exception if script - // tries to do that and we have to workaround this behaviour. - Object.defineProperty(window, 'location', { - value: { - href: - 'https://kibana.com/api/security/v1/oidc/implicit#token=some_token&access_token=some_access_token', - replace(newLocation: string) { - this.href = newLocation; + // tries to do that and we have to workaround this behaviour. We also need to wait until our + // script is loaded and executed, __isScriptExecuted__ is used exactly for that. + (window as Record).__isScriptExecuted__ = new Promise(resolve => { + Object.defineProperty(window, 'location', { + value: { + href: + 'https://kibana.com/api/security/v1/oidc/implicit#token=some_token&access_token=some_access_token', + replace(newLocation: string) { + this.href = newLocation; + resolve(); + }, }, - }, + }); }); }, }); + await (dom.window as Record).__isScriptExecuted__; + // Check that proxy page is returned with proper headers. expect(response.headers['content-type']).to.be('text/html; charset=utf-8'); expect(response.headers['cache-control']).to.be('private, no-cache, no-store'); diff --git a/x-pack/test/oidc_api_integration/implicit_flow.config.ts b/x-pack/test/oidc_api_integration/implicit_flow.config.ts index a7854488097a6..93f2349a40099 100644 --- a/x-pack/test/oidc_api_integration/implicit_flow.config.ts +++ b/x-pack/test/oidc_api_integration/implicit_flow.config.ts @@ -20,17 +20,20 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { esTestCluster: { ...oidcAPITestsConfig.get('esTestCluster'), - serverArgs: oidcAPITestsConfig.get('esTestCluster.serverArgs').map((arg: string) => { - if (arg.startsWith('xpack.security.authc.realms.oidc.oidc1.rp.response_type')) { - return 'xpack.security.authc.realms.oidc.oidc1.rp.response_type=id_token token'; - } + serverArgs: oidcAPITestsConfig + .get('esTestCluster.serverArgs') + .reduce((serverArgs: string[], arg: string) => { + // We should change `response_type` to `id_token token` and get rid of unnecessary `token_endpoint`. + if (arg.startsWith('xpack.security.authc.realms.oidc.oidc1.rp.response_type')) { + serverArgs.push( + 'xpack.security.authc.realms.oidc.oidc1.rp.response_type=id_token token' + ); + } else if (!arg.startsWith('xpack.security.authc.realms.oidc.oidc1.op.token_endpoint')) { + serverArgs.push(arg); + } - if (arg.startsWith('xpack.security.authc.realms.oidc.oidc1.op.token_endpoint')) { - return 'xpack.security.authc.realms.oidc.oidc1.op.token_endpoint=should_not_be_used'; - } - - return arg; - }), + return serverArgs; + }, []), }, }; } diff --git a/x-pack/test/pki_api_integration/apis/index.ts b/x-pack/test/pki_api_integration/apis/index.ts index 47ffb25835d43..d859ed172ac69 100644 --- a/x-pack/test/pki_api_integration/apis/index.ts +++ b/x-pack/test/pki_api_integration/apis/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { describe('apis PKI', function() { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./security')); }); } diff --git a/x-pack/test/reporting/functional/index.js b/x-pack/test/reporting/functional/index.js index 17aeb03eeadb5..fa473f454a925 100644 --- a/x-pack/test/reporting/functional/index.js +++ b/x-pack/test/reporting/functional/index.js @@ -6,7 +6,7 @@ export default function ({ loadTestFile }) { describe('reporting app', function () { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./reporting')); }); } diff --git a/x-pack/test/saml_api_integration/apis/index.js b/x-pack/test/saml_api_integration/apis/index.js index b4e6503f201e5..ac08d2e078abf 100644 --- a/x-pack/test/saml_api_integration/apis/index.js +++ b/x-pack/test/saml_api_integration/apis/index.js @@ -6,7 +6,7 @@ export default function ({ loadTestFile }) { describe('apis SAML', function () { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./security')); }); } diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts index e24de7c7ae77f..fadefd2743b30 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/index.ts @@ -12,7 +12,7 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { const supertest = getService('supertest'); describe('saved objects security only enabled', function() { - this.tags('ciGroup4'); + this.tags('ciGroup9'); before(async () => { await createUsersAndRoles(es, supertest); diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts index b54345d78456f..4493a5332b62c 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/index.ts @@ -13,7 +13,7 @@ export default function({ loadTestFile, getService }: TestInvoker) { const supertest = getService('supertest'); describe('spaces api with security', function() { - this.tags('ciGroup3'); + this.tags('ciGroup8'); before(async () => { await createUsersAndRoles(es, supertest); diff --git a/x-pack/test/token_api_integration/auth/index.js b/x-pack/test/token_api_integration/auth/index.js index 528a5c4bf8cf5..e7b5a5b46a503 100644 --- a/x-pack/test/token_api_integration/auth/index.js +++ b/x-pack/test/token_api_integration/auth/index.js @@ -6,7 +6,7 @@ export default function ({ loadTestFile }) { describe('token-based auth', function () { - this.tags('ciGroup1'); + this.tags('ciGroup6'); loadTestFile(require.resolve('./login')); loadTestFile(require.resolve('./logout')); loadTestFile(require.resolve('./header')); diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts index f87bb10aaf083..e28ea819bfdfc 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts @@ -17,7 +17,7 @@ export default function uiCapabilitiesTests({ loadTestFile, getService }: FtrPro const featuresService: FeaturesService = getService('features'); describe('ui capabilities', function() { - this.tags('ciGroup4'); + this.tags('ciGroup9'); before(async () => { const features = await featuresService.get(); diff --git a/x-pack/test/ui_capabilities/security_only/tests/index.ts b/x-pack/test/ui_capabilities/security_only/tests/index.ts index a941e64839726..b84c02f9d65c0 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/index.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/index.ts @@ -13,7 +13,7 @@ export default function uiCapabilitesTests({ loadTestFile, getService }: FtrProv const securityService: SecurityService = getService('security'); describe('ui capabilities', function() { - this.tags('ciGroup4'); + this.tags('ciGroup9'); before(async () => { for (const user of UserScenarios) { diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts index 177ca0064a6bf..294f545c7d90f 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts @@ -14,7 +14,7 @@ export default function uiCapabilitesTests({ loadTestFile, getService }: FtrProv const featuresService: FeaturesService = getService('features'); describe('ui capabilities', function() { - this.tags('ciGroup4'); + this.tags('ciGroup9'); before(async () => { const features = await featuresService.get(); diff --git a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js index 82e9214f55398..1b7406b37022a 100644 --- a/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js +++ b/x-pack/test/upgrade_assistant_integration/upgrade_assistant/index.js @@ -6,7 +6,7 @@ export default function ({ loadTestFile }) { describe('upgrade assistant', function () { - this.tags('ciGroup2'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./reindexing')); }); diff --git a/x-pack/test/visual_regression/tests/maps/index.js b/x-pack/test/visual_regression/tests/maps/index.js index c080e5727b243..de5c50e900ca8 100644 --- a/x-pack/test/visual_regression/tests/maps/index.js +++ b/x-pack/test/visual_regression/tests/maps/index.js @@ -26,7 +26,7 @@ export default function ({ loadTestFile, getService }) { await esArchiver.unload('maps/kibana'); }); - this.tags('ciGroup5'); + this.tags('ciGroup10'); loadTestFile(require.resolve('./vector_styling')); }); } diff --git a/yarn.lock b/yarn.lock index f82b220d9c255..3c82dacf6e9c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2303,28 +2303,34 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== -"@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.2.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.3.0.tgz#50a2754016b6f30a994ceda6d9a0a8c36adda849" - integrity sha512-j4ZwhaHmwsCb4DlDOIWnI5YyKDNMoNThsmwEpfHx6a1EpsGZ9qYLxP++LMlmBRjtGptGHFsGItJ768snllFWpA== +"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.6.0.tgz#ec7670432ae9c8eb710400d112c201a362d83393" + integrity sha512-w4/WHG7C4WWFyE5geCieFJF6MZkbW4VAriol5KlmQXpAQdxvV0p26sqNZOW6Qyw6Y0l9K4g+cHvvczR2sEEpqg== dependencies: type-detect "4.0.8" -"@sinonjs/formatio@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.1.0.tgz#6ac9d1eb1821984d84c4996726e45d1646d8cce5" - integrity sha512-ZAR2bPHOl4Xg6eklUGpsdiIJ4+J1SNag1DHHrG/73Uz/nVwXqjgUtRPLoS+aVyieN9cSbc0E4LsU984tWcDyNg== +"@sinonjs/formatio@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.1.tgz#52310f2f9bcbc67bdac18c94ad4901b95fde267e" + integrity sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ== dependencies: - "@sinonjs/samsam" "^2 || ^3" + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^3.1.0" -"@sinonjs/samsam@^2 || ^3", "@sinonjs/samsam@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.0.2.tgz#304fb33bd5585a0b2df8a4c801fcb47fa84d8e43" - integrity sha512-m08g4CS3J6lwRQk1pj1EO+KEVWbrbXsmi9Pw0ySmrIbcVxVaedoFgLvFsV8wHLwh01EpROVz3KvVcD1Jmks9FQ== +"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a" + integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== dependencies: - "@sinonjs/commons" "^1.0.2" + "@sinonjs/commons" "^1.3.0" array-from "^2.1.1" - lodash.get "^4.4.2" + lodash "^4.17.15" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== "@slack/client@^4.8.0": version "4.8.0" @@ -3033,10 +3039,10 @@ resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6" integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg== -"@types/chroma-js@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-1.4.1.tgz#7c52d461173d569ba1f27e0c2dd26ee76691ec82" - integrity sha512-i9hUiO3bwgmzZUDwBuR65WqsBQ/nwN+H2fKX0bykXCdd8cFQEuIj8vI7FXjyb2f5z5h+pv76I/uakikKSgaqTA== +"@types/chroma-js@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-1.4.2.tgz#3152c8dedfa8621f1ccaaabb40722a8aca808bcf" + integrity sha512-Ni8yCN1vF0yfnfKf5bNrBm+92EdZIX2sUk+A4t4QvO1x/9G04rGyC0nik4i5UcNfx8Q7MhX4XUDcy2nrkKQLFg== "@types/chromedriver@^2.38.0": version "2.38.0" @@ -3315,10 +3321,10 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.2.tgz#0e670ea254d559241b6eeb3894f8754991e73220" integrity sha512-ui3WwXmjTaY73fOQ3/m3nnajU/Orhi6cEu5rzX+BrAAJxa3eITXZ5ch9suPqtM03OWhAHhPSyBGCN4UKoxO20Q== -"@types/history@^4.6.2": - version "4.6.2" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0" - integrity sha512-eVAb52MJ4lfPLiO9VvTgv8KaZDEIqCwhv+lXOMLlt4C1YHTShgmMULEg0RrCbnqfYd6QKfHsMp0MiX0vWISpSw== +"@types/history@^4.7.3": + version "4.7.3" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.3.tgz#856c99cdc1551d22c22b18b5402719affec9839a" + integrity sha512-cS5owqtwzLN5kY+l+KgKdRJ/Cee8tlmQoGQuIE9tWnSmS3JMKzmxo2HIAk2wODMifGwO20d62xZQLYz+RLfXmw== "@types/hoek@^4.1.3": version "4.1.3" @@ -3567,10 +3573,10 @@ dependencies: "@types/node" "*" -"@types/mocha@^5.2.6": - version "5.2.6" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.6.tgz#b8622d50557dd155e9f2f634b7d68fd38de5e94b" - integrity sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw== +"@types/mocha@^5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== "@types/moment-timezone@^0.5.8": version "0.5.8" @@ -3634,10 +3640,12 @@ resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-0.0.25.tgz#b6f55062827a4787fe4ab151cf3412a468e65271" integrity sha512-ShHzHkYD+Ldw3eyttptCpUhF1/mkInWwasQkCNXZHOsJMJ/UMa8wXrxSrTJaVk0r4pLK/VnESVM0wFsfQzNEKQ== -"@types/object-hash@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.2.0.tgz#d65904331bd0b05c7d5ece75f9ddfdbe82affd30" - integrity sha512-0JKYQRatHdzijO/ni7JV5eHUJWaMRpGvwiABk8U5iAk5Corm0yLNEfYGNkZWYc+wCyCKKpg0+TsZIvP8AymIYA== +"@types/object-hash@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.0.tgz#b20db2074129f71829d61ff404e618c4ac3d73cf" + integrity sha512-il4NIe4jTx4lfhkKaksmmGHw5EsVkO8sHWkpJHM9m59r1dtsVadLSrJqdE8zU74NENDAsR3oLIOlooRAXlPLNA== + dependencies: + "@types/node" "*" "@types/opn@^5.1.0": version "5.1.0" @@ -3894,10 +3902,10 @@ dependencies: "@types/node" "*" -"@types/sinon@^7.0.0": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.3.tgz#f8647e883d873962130f906a6114a4e187755696" - integrity sha512-cjmJQLx2B5Hp9SzO7rdSivipo3kBqRqeYkTW17nLST1tn5YLWBjTdnzdmeTJXA1+KrrBLsEuvKQ0fUPGrfazQg== +"@types/sinon@^7.0.13": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" + integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== "@types/stack-utils@^1.0.1": version "1.0.1" @@ -4493,6 +4501,13 @@ agent-base@^4.1.0: dependencies: es6-promisify "^5.0.0" +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + agentkeepalive@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c" @@ -9732,7 +9747,7 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" -del@^4.0.0, del@^4.1.1: +del@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== @@ -14486,7 +14501,7 @@ history-extra@^5.0.1: resolved "https://registry.yarnpkg.com/history-extra/-/history-extra-5.0.1.tgz#95a2e59dda526c4241d0ae1b124a77a5e4675ce8" integrity sha512-6XV1L1lHgporVWgppa/Kq+Fnz4lhBew7iMxYCTfzVmoEywsAKJnTjdw1zOd+EGLHGYp0/V8jSVMEgqx4QbHLTw== -history@4.9.0: +history@4.9.0, history@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca" integrity sha512-H2DkjCjXf0Op9OAr6nJ56fcRkTSNrUiv41vNJ6IswJjif6wlpZK0BTfFbi7qK9dXLSYZxkq5lBsj3vUjlYBYZA== @@ -14804,6 +14819,14 @@ https-proxy-agent@2.2.1, https-proxy-agent@^2.2.0, https-proxy-agent@^2.2.1: agent-base "^4.1.0" debug "^3.1.0" +https-proxy-agent@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz#271ea8e90f836ac9f119daccd39c19ff7dfb0793" + integrity sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -15861,13 +15884,6 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-path-inside@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.0.0.tgz#28f249020fe6906671fe31a76118f2cd490441fd" - integrity sha512-OmUXvSq+P7aI/aRbl1dzwdlyLn8vW7Nr2/11S7y/dcLLgnQ89hgYJp7tfc+A5SRid3rNCLpruOp2CAV68/iOcA== - dependencies: - path-is-inside "^1.0.2" - is-path-inside@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" @@ -18504,15 +18520,10 @@ loglevelnext@^1.0.1: es6-symbol "^3.1.1" object.assign "^4.1.0" -lolex@^2.3.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.6.0.tgz#cf9166f3c9dece3cdeb5d6b01fce50f14a1203e3" - integrity sha512-e1UtIo1pbrIqEXib/yMjHciyqkng5lc0rrIbytgjmRgDR9+2ceNIAcwOWSgylRjoEP9VdVguCSRwnNmlbnOUwA== - -lolex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-3.0.0.tgz#f04ee1a8aa13f60f1abd7b0e8f4213ec72ec193e" - integrity sha512-hcnW80h3j2lbUfFdMArd5UPA/vxZJ+G8vobd+wg3nVEQA0EigStbYcrG030FJxL6xiDDPEkoMatV9xIh5OecQQ== +lolex@^4.1.0, lolex@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" + integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== long@^2.4.0: version "2.4.0" @@ -19855,16 +19866,16 @@ nigel@3.x.x: hoek "5.x.x" vise "3.x.x" -nise@^1.4.7: - version "1.4.8" - resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.8.tgz#ce91c31e86cf9b2c4cac49d7fcd7f56779bfd6b0" - integrity sha512-kGASVhuL4tlAV0tvA34yJYZIVihrUt/5bDwpp4tTluigxUr2bBlJeDXmivb6NuEdFkqvdv/Ybb9dm16PSKUhtw== +nise@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.2.tgz#b6d29af10e48b321b307e10e065199338eeb2652" + integrity sha512-/6RhOUlicRCbE9s+94qCUsyE+pKlVJ5AhIv+jEE7ESKwnbXqulKZ1FYU+XAtHHWE9TinYvAxDUJAb912PwPoWA== dependencies: - "@sinonjs/formatio" "^3.1.0" + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/text-encoding" "^0.7.1" just-extend "^4.0.2" - lolex "^2.3.2" + lolex "^4.1.0" path-to-regexp "^1.7.0" - text-encoding "^0.6.4" no-case@^2.2.0, no-case@^2.3.2: version "2.3.2" @@ -20646,7 +20657,7 @@ opentracing@^0.14.0: resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.3.tgz#23e3ad029fa66a653926adbe57e834469f8550aa" integrity sha1-I+OtAp+mamU5Jq2+V+g0Rp+FUKo= -opn@^5.3.0, opn@^5.4.0: +opn@^5.3.0: version "5.4.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035" integrity sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw== @@ -25438,17 +25449,17 @@ simplify-js@^1.2.1: resolved "https://registry.yarnpkg.com/simplify-js/-/simplify-js-1.2.3.tgz#a3422c1b9884d60421345eb44d2b872662df27f5" integrity sha512-0IkEqs+5c5vROkHaifGfbqHf5tYDcsTBy6oJPRbFCSwp2uzEr+PpH3dNP7wD8O3d7zdUCjLVq1/xHkwA/JjlFA== -sinon@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.2.2.tgz#388ecabd42fa93c592bfc71d35a70894d5a0ca07" - integrity sha512-WLagdMHiEsrRmee3jr6IIDntOF4kbI6N2pfbi8wkv50qaUQcBglkzkjtoOEbeJ2vf1EsrHhLI+5Ny8//WHdMoA== +sinon@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.4.2.tgz#ecd54158fef2fcfbdb231a3fa55140e8cb02ad6c" + integrity sha512-pY5RY99DKelU3pjNxcWo6XqeB1S118GBcVIIdDi6V+h6hevn1izcg2xv1hTHW/sViRXU7sUOxt4wTUJ3gsW2CQ== dependencies: - "@sinonjs/commons" "^1.2.0" - "@sinonjs/formatio" "^3.1.0" - "@sinonjs/samsam" "^3.0.2" + "@sinonjs/commons" "^1.4.0" + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/samsam" "^3.3.3" diff "^3.5.0" - lolex "^3.0.0" - nise "^1.4.7" + lolex "^4.2.0" + nise "^1.5.2" supports-color "^5.5.0" sisteransi@^1.0.0: @@ -26914,11 +26925,6 @@ test-exclude@^5.2.3: read-pkg-up "^4.0.0" require-main-filename "^2.0.0" -text-encoding@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" - integrity sha1-45mpgiV6J22uQou5KEXLcb3CbRk= - text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"