diff --git a/.eslintrc.js b/.eslintrc.js index 97a35d8b50e565..199f3743fd621e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -643,6 +643,18 @@ module.exports = { // '@typescript-eslint/unbound-method': 'error', }, }, + // { + // // will introduced after the other warns are fixed + // // typescript and javascript for front end react performance + // files: ['x-pack/legacy/plugins/siem/public/**/!(*.test).{js,ts,tsx}'], + // plugins: ['react-perf'], + // rules: { + // // 'react-perf/jsx-no-new-object-as-prop': 'error', + // // 'react-perf/jsx-no-new-array-as-prop': 'error', + // // 'react-perf/jsx-no-new-function-as-prop': 'error', + // // 'react/jsx-no-bind': 'error', + // }, + // }, { // typescript and javascript for front and back end files: ['x-pack/legacy/plugins/siem/**/*.{js,ts,tsx}'], @@ -747,8 +759,6 @@ module.exports = { // will introduced after the other warns are fixed // 'react/sort-comp': 'error', 'react/void-dom-elements-no-children': 'error', - // will introduced after the other warns are fixed - // 'react/jsx-no-bind': 'error', 'react/jsx-no-comment-textnodes': 'error', 'react/jsx-no-literals': 'error', 'react/jsx-no-target-blank': 'error', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2e2b20a46baede..de7159489689e9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,7 @@ /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app /src/legacy/core_plugins/metrics/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app +/src/legacy/core_plugins/vis_type_xy/ @elastic/kibana-app # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon /src/plugins/home/public @elastic/kibana-app /src/plugins/home/server/*.ts @elastic/kibana-app @@ -28,15 +29,7 @@ /src/plugins/dev_tools/ @elastic/kibana-app # App Architecture -/src/plugins/data/ @elastic/kibana-app-arch -/src/plugins/embeddable/ @elastic/kibana-app-arch -/src/plugins/expressions/ @elastic/kibana-app-arch -/src/plugins/kibana_react/ @elastic/kibana-app-arch -/src/plugins/kibana_utils/ @elastic/kibana-app-arch -/src/plugins/navigation/ @elastic/kibana-app-arch -/src/plugins/ui_actions/ @elastic/kibana-app-arch -/src/plugins/visualizations/ @elastic/kibana-app-arch -/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +/packages/kbn-interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/data/ @elastic/kibana-app-arch /src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch @@ -48,6 +41,19 @@ /src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +/src/plugins/bfetch/ @elastic/kibana-app-arch +/src/plugins/dashboard_embeddable_container/ @elastic/kibana-app-arch +/src/plugins/data/ @elastic/kibana-app-arch +/src/plugins/embeddable/ @elastic/kibana-app-arch +/src/plugins/expressions/ @elastic/kibana-app-arch +/src/plugins/inspector/ @elastic/kibana-app-arch +/src/plugins/kibana_react/ @elastic/kibana-app-arch +/src/plugins/kibana_utils/ @elastic/kibana-app-arch +/src/plugins/management/ @elastic/kibana-app-arch +/src/plugins/navigation/ @elastic/kibana-app-arch +/src/plugins/ui_actions/ @elastic/kibana-app-arch +/src/plugins/visualizations/ @elastic/kibana-app-arch +/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui diff --git a/.i18nrc.json b/.i18nrc.json index 1230151212f573..7d7685b5c1ef1b 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -22,7 +22,10 @@ "interpreter": "src/legacy/core_plugins/interpreter", "kbn": "src/legacy/core_plugins/kibana", "kbnDocViews": "src/legacy/core_plugins/kbn_doc_views", - "management": ["src/legacy/core_plugins/management", "src/plugins/management"], + "management": [ + "src/legacy/core_plugins/management", + "src/plugins/management" + ], "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", "kibana_utils": "src/plugins/kibana_utils", @@ -43,6 +46,7 @@ "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], "visTypeVega": "src/legacy/core_plugins/vis_type_vega", "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", + "visTypeXy": "src/legacy/core_plugins/vis_type_xy", "visualizations": [ "src/plugins/visualizations", "src/legacy/core_plugins/visualizations" diff --git a/.sass-lint.yml b/.sass-lint.yml index fba2c003484f6a..9c64c1e5eea569 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -3,6 +3,7 @@ files: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/vis_type_vislib/**/*.s+(a|c)ss' + - 'src/legacy/core_plugins/vis_type_xy/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.disablesuburltracking.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.disablesuburltracking.md index 0054adc693dc3e..9182137e10c18f 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.disablesuburltracking.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.disablesuburltracking.md @@ -1,17 +1,17 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [disableSubUrlTracking](./kibana-plugin-public.chromenavlink.disablesuburltracking.md) - -## ChromeNavLink.disableSubUrlTracking property - -> Warning: This API is now obsolete. -> -> - -A flag that tells legacy chrome to ignore the link when tracking sub-urls - -Signature: - -```typescript -readonly disableSubUrlTracking?: boolean; -``` + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) > [disableSubUrlTracking](./kibana-plugin-public.chromenavlink.disablesuburltracking.md) + +## ChromeNavLink.disableSubUrlTracking property + +> Warning: This API is now obsolete. +> +> + +A flag that tells legacy chrome to ignore the link when tracking sub-urls + +Signature: + +```typescript +readonly disableSubUrlTracking?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.chromenavlink.md b/docs/development/core/public/kibana-plugin-public.chromenavlink.md index 810e520badf47e..b532c11ac08daf 100644 --- a/docs/development/core/public/kibana-plugin-public.chromenavlink.md +++ b/docs/development/core/public/kibana-plugin-public.chromenavlink.md @@ -1,33 +1,33 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) - -## ChromeNavLink interface - - -Signature: - -```typescript -export interface ChromeNavLink -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [active](./kibana-plugin-public.chromenavlink.active.md) | boolean | Indicates whether or not this app is currently on the screen. | -| [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | -| [category](./kibana-plugin-public.chromenavlink.category.md) | AppCategory | The category the app lives in | -| [disabled](./kibana-plugin-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | -| [disableSubUrlTracking](./kibana-plugin-public.chromenavlink.disablesuburltracking.md) | boolean | A flag that tells legacy chrome to ignore the link when tracking sub-urls | -| [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | -| [hidden](./kibana-plugin-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | -| [icon](./kibana-plugin-public.chromenavlink.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.chromenavlink.id.md) | string | A unique identifier for looking up links. | -| [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled. | -| [order](./kibana-plugin-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | -| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an application. | -| [title](./kibana-plugin-public.chromenavlink.title.md) | string | The title of the application. | -| [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | -| [url](./kibana-plugin-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications. | - + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [ChromeNavLink](./kibana-plugin-public.chromenavlink.md) + +## ChromeNavLink interface + + +Signature: + +```typescript +export interface ChromeNavLink +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [active](./kibana-plugin-public.chromenavlink.active.md) | boolean | Indicates whether or not this app is currently on the screen. | +| [baseUrl](./kibana-plugin-public.chromenavlink.baseurl.md) | string | The base route used to open the root of an application. | +| [category](./kibana-plugin-public.chromenavlink.category.md) | AppCategory | The category the app lives in | +| [disabled](./kibana-plugin-public.chromenavlink.disabled.md) | boolean | Disables a link from being clickable. | +| [disableSubUrlTracking](./kibana-plugin-public.chromenavlink.disablesuburltracking.md) | boolean | A flag that tells legacy chrome to ignore the link when tracking sub-urls | +| [euiIconType](./kibana-plugin-public.chromenavlink.euiicontype.md) | string | A EUI iconType that will be used for the app's icon. This icon takes precendence over the icon property. | +| [hidden](./kibana-plugin-public.chromenavlink.hidden.md) | boolean | Hides a link from the navigation. | +| [icon](./kibana-plugin-public.chromenavlink.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.chromenavlink.id.md) | string | A unique identifier for looking up links. | +| [linkToLastSubUrl](./kibana-plugin-public.chromenavlink.linktolastsuburl.md) | boolean | Whether or not the subUrl feature should be enabled. | +| [order](./kibana-plugin-public.chromenavlink.order.md) | number | An ordinal used to sort nav links relative to one another for display. | +| [subUrlBase](./kibana-plugin-public.chromenavlink.suburlbase.md) | string | A url base that legacy apps can set to match deep URLs to an application. | +| [title](./kibana-plugin-public.chromenavlink.title.md) | string | The title of the application. | +| [tooltip](./kibana-plugin-public.chromenavlink.tooltip.md) | string | A tooltip shown when hovering over an app link. | +| [url](./kibana-plugin-public.chromenavlink.url.md) | string | A url that legacy apps can set to deep link into their applications. | + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md index c61907f3663012..2ebee16874c801 100644 --- a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md @@ -30,7 +30,7 @@ const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ | Method | Description | | --- | --- | | [rename(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.rename.md) | Rename a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the oldKey was found and deprecation applied. | -| [renameFromRoot(oldKey, newKey)](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) | Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. | +| [renameFromRoot(oldKey, newKey, silent)](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) | Rename a configuration property from the root configuration. Will log a deprecation warning if the oldKey was found and deprecation applied.This should be only used when renaming properties from different configuration's path. To rename properties from inside a plugin's configuration, use 'rename' instead. | | [unused(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unused.md) | Remove a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the unused key was found and deprecation applied. | | [unusedFromRoot(unusedKey)](./kibana-plugin-server.configdeprecationfactory.unusedfromroot.md) | Remove a configuration property from the root configuration. Will log a deprecation warning if the unused key was found and deprecation applied.This should be only used when removing properties from outside of a plugin's configuration. To remove properties from inside a plugin's configuration, use 'unused' instead. | diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md index 269f242ec35da6..40ea891b17c957 100644 --- a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md @@ -11,7 +11,7 @@ This should be only used when renaming properties from different configuration's Signature: ```typescript -renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; +renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; ``` ## Parameters @@ -20,6 +20,7 @@ renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; | --- | --- | --- | | oldKey | string | | | newKey | string | | +| silent | boolean | | Returns: diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md new file mode 100644 index 00000000000000..ee7e1e5b7c9c94 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.host.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [host](./kibana-plugin-server.httpserverinfo.host.md) + +## HttpServerInfo.host property + +The hostname of the server + +Signature: + +```typescript +host: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.md new file mode 100644 index 00000000000000..6dbdb11ddb66ef --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) + +## HttpServerInfo interface + + +Signature: + +```typescript +export interface HttpServerInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [host](./kibana-plugin-server.httpserverinfo.host.md) | string | The hostname of the server | +| [name](./kibana-plugin-server.httpserverinfo.name.md) | string | The name of the Kibana server | +| [port](./kibana-plugin-server.httpserverinfo.port.md) | number | The port the server is listening on | +| [protocol](./kibana-plugin-server.httpserverinfo.protocol.md) | 'http' | 'https' | 'socket' | The protocol used by the server | + diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md new file mode 100644 index 00000000000000..8d3a45c90a3427 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.name.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [name](./kibana-plugin-server.httpserverinfo.name.md) + +## HttpServerInfo.name property + +The name of the Kibana server + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md new file mode 100644 index 00000000000000..5dd5a53830c441 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.port.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [port](./kibana-plugin-server.httpserverinfo.port.md) + +## HttpServerInfo.port property + +The port the server is listening on + +Signature: + +```typescript +port: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md b/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md new file mode 100644 index 00000000000000..08afb5c3f72133 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpserverinfo.protocol.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) > [protocol](./kibana-plugin-server.httpserverinfo.protocol.md) + +## HttpServerInfo.protocol property + +The protocol used by the server + +Signature: + +```typescript +protocol: 'http' | 'https' | 'socket'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md new file mode 100644 index 00000000000000..4501a7e26f75fa --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.getserverinfo.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [getServerInfo](./kibana-plugin-server.httpservicesetup.getserverinfo.md) + +## HttpServiceSetup.getServerInfo property + +Provides common [information](./kibana-plugin-server.httpserverinfo.md) about the running http server. + +Signature: + +```typescript +getServerInfo: () => HttpServerInfo; +``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index 2a4b0e09977c1c..c2d53ec1eaf52e 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -86,6 +86,7 @@ async (context, request, response) => { | [createCookieSessionStorageFactory](./kibana-plugin-server.httpservicesetup.createcookiesessionstoragefactory.md) | <T>(cookieOptions: SessionStorageCookieOptions<T>) => Promise<SessionStorageFactory<T>> | Creates cookie based session storage factory [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | | [createRouter](./kibana-plugin-server.httpservicesetup.createrouter.md) | () => IRouter | Provides ability to declare a handler function for a particular path and HTTP request method. | | [csp](./kibana-plugin-server.httpservicesetup.csp.md) | ICspConfig | The CSP config used for Kibana. | +| [getServerInfo](./kibana-plugin-server.httpservicesetup.getserverinfo.md) | () => HttpServerInfo | Provides common [information](./kibana-plugin-server.httpserverinfo.md) about the running http server. | | [isTlsEnabled](./kibana-plugin-server.httpservicesetup.istlsenabled.md) | boolean | Flag showing whether a server was configured to use TLS connection. | | [registerAuth](./kibana-plugin-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | | [registerOnPostAuth](./kibana-plugin-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. | diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index e7b13346525406..a3abeff44c25cb 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -64,6 +64,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ErrorHttpResponseOptions](./kibana-plugin-server.errorhttpresponseoptions.md) | HTTP response parameters | | [FakeRequest](./kibana-plugin-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | | [HttpResponseOptions](./kibana-plugin-server.httpresponseoptions.md) | HTTP response parameters | +| [HttpServerInfo](./kibana-plugin-server.httpserverinfo.md) | | | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to hapi server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. | | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [IContextContainer](./kibana-plugin-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 146d4e97b6cf4a..a34f956ace263d 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -74,6 +74,12 @@ specified explicitly. *Impact:* Define `xpack.security.authc.saml.realm` when using the SAML authentication provider instead. +[float] +==== `/api/security/v1/saml` endpoint is no longer supported +*Details:* The deprecated `/api/security/v1/saml` endpoint is no longer supported. + +*Impact:* Rely on `/api/security/saml/callback` endpoint when using SAML instead. This change should be reflected in Kibana `server.xsrf.whitelist` config as well as in Elasticsearch and Identity Provider SAML settings. + [float] === `optimize` directory is now in the `data` folder *Details:* Generated bundles have moved to the configured `path.data` folder. diff --git a/package.json b/package.json index e249546e715812..f9a3bfd99b1098 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "@elastic/charts": "^16.1.0", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", @@ -414,6 +414,7 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react": "^7.17.0", "eslint-plugin-react-hooks": "^2.3.0", + "eslint-plugin-react-perf": "^3.2.3", "exit-hook": "^2.2.0", "faker": "1.1.0", "fetch-mock": "^7.3.9", diff --git a/packages/kbn-plugin-generator/index.js b/packages/kbn-plugin-generator/index.js index 90274288357b82..15adce7f01c8e5 100644 --- a/packages/kbn-plugin-generator/index.js +++ b/packages/kbn-plugin-generator/index.js @@ -29,6 +29,7 @@ exports.run = function run(argv) { const options = getopts(argv, { alias: { h: 'help', + i: 'internal', }, }); @@ -40,17 +41,22 @@ exports.run = function run(argv) { if (options.help) { console.log( dedent(chalk` - {dim usage:} node scripts/generate-plugin {bold [name]} - - generate a fresh Kibana plugin in the plugins/ directory + # {dim Usage:} + node scripts/generate-plugin {bold [name]} + Generate a fresh Kibana plugin in the plugins/ directory + + # {dim Core Kibana plugins:} + node scripts/generate-plugin {bold [name]} -i + To generate a core Kibana plugin inside the src/plugins/ directory, add the -i flag. `) + '\n' ); process.exit(1); } const name = options._[0]; + const isKibanaPlugin = options.internal; const template = resolve(__dirname, './sao_template'); - const kibanaPlugins = resolve(__dirname, '../../plugins'); + const kibanaPlugins = resolve(__dirname, isKibanaPlugin ? '../../src/plugins' : '../../plugins'); const targetPath = resolve(kibanaPlugins, snakeCase(name)); sao({ @@ -58,6 +64,8 @@ exports.run = function run(argv) { targetPath: targetPath, configOptions: { name, + isKibanaPlugin, + targetPath, }, }).catch(error => { console.error(chalk`{red fatal error}!`); diff --git a/src/es_archiver/index.js b/packages/kbn-plugin-generator/index.js.d.ts similarity index 88% rename from src/es_archiver/index.js rename to packages/kbn-plugin-generator/index.js.d.ts index f7a579a98a42d9..46f7c43fd57907 100644 --- a/src/es_archiver/index.js +++ b/packages/kbn-plugin-generator/index.js.d.ts @@ -16,5 +16,9 @@ * specific language governing permissions and limitations * under the License. */ - -export { EsArchiver } from './es_archiver'; +interface PluginGenerator { + /** + * Run plugin generator. + */ + run: (...args: any[]) => any; +} diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index aa6611f3b6738d..129125c4583d52 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -61,7 +61,8 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug expect(stats.isDirectory()).toBe(true); }); - it(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { + // skipped until internationalization is re-introduced + it.skip(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { // Link to the error that happens when the blank line is not there: // https://github.com/elastic/kibana/pull/45044#issuecomment-530092627 const intlFile = `${generatedPath}/.i18nrc.json`; @@ -78,16 +79,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug }); }); - it(`'yarn test:server' should exit 0`, async () => { - await execa('yarn', ['test:server'], { - cwd: generatedPath, - env: { - DISABLE_JUNIT_REPORTER: '1', - }, - }); - }); - - it(`'yarn build' should exit 0`, async () => { + it.skip(`'yarn build' should exit 0`, async () => { await execa('yarn', ['build'], { cwd: generatedPath }); }); @@ -109,7 +101,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug '--migrations.skip=true', ], cwd: generatedPath, - wait: /ispec_plugin.+Status changed from uninitialized to green - Ready/, + wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), }); await proc.stop('kibana'); }); @@ -120,7 +112,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug await execa('yarn', ['preinstall'], { cwd: generatedPath }); }); - it(`'yarn lint' should exit 0`, async () => { + it.skip(`'yarn lint' should exit 0`, async () => { await execa('yarn', ['lint'], { cwd: generatedPath }); }); diff --git a/packages/kbn-plugin-generator/sao_template/sao.js b/packages/kbn-plugin-generator/sao_template/sao.js index f7401cba843588..aed4b9a02838f5 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.js +++ b/packages/kbn-plugin-generator/sao_template/sao.js @@ -17,21 +17,19 @@ * under the License. */ -const { resolve, relative, dirname } = require('path'); +const { relative } = require('path'); const startCase = require('lodash.startcase'); const camelCase = require('lodash.camelcase'); const snakeCase = require('lodash.snakecase'); -const execa = require('execa'); const chalk = require('chalk'); +const execa = require('execa'); const pkg = require('../package.json'); const kibanaPkgPath = require.resolve('../../../package.json'); const kibanaPkg = require(kibanaPkgPath); // eslint-disable-line import/no-dynamic-require -const KBN_DIR = dirname(kibanaPkgPath); - -module.exports = function({ name }) { +module.exports = function({ name, targetPath, isKibanaPlugin }) { return { prompts: { description: { @@ -47,41 +45,38 @@ module.exports = function({ name }) { message: 'Should an app component be generated?', default: true, }, - generateTranslations: { - type: 'confirm', - message: 'Should translation files be generated?', - default: true, - }, - generateHack: { - type: 'confirm', - message: 'Should a hack component be generated?', - default: true, - }, generateApi: { type: 'confirm', message: 'Should a server API be generated?', default: true, }, + // generateTranslations: { + // type: 'confirm', + // message: 'Should translation files be generated?', + // default: true, + // }, generateScss: { type: 'confirm', message: 'Should SCSS be used?', when: answers => answers.generateApp, default: true, }, + generateEslint: { + type: 'confirm', + message: 'Would you like to use a custom eslint file?', + default: !isKibanaPlugin, + }, }, filters: { + 'public/**/index.scss': 'generateScss', 'public/**/*': 'generateApp', - 'translations/**/*': 'generateTranslations', - '.i18nrc.json': 'generateTranslations', - 'public/hack.js': 'generateHack', 'server/**/*': 'generateApi', - 'public/app.scss': 'generateScss', - '.kibana-plugin-helpers.json': 'generateScss', + // 'translations/**/*': 'generateTranslations', + // '.i18nrc.json': 'generateTranslations', + 'eslintrc.js': 'generateEslint', }, move: { - gitignore: '.gitignore', 'eslintrc.js': '.eslintrc.js', - 'package_template.json': 'package.json', }, data: answers => Object.assign( @@ -91,34 +86,36 @@ module.exports = function({ name }) { camelCase, snakeCase, name, + isKibanaPlugin, + kbnVersion: answers.kbnVersion, + upperCamelCaseName: name.charAt(0).toUpperCase() + camelCase(name).slice(1), + hasUi: !!answers.generateApp, + hasServer: !!answers.generateApi, + hasScss: !!answers.generateScss, + relRoot: isKibanaPlugin ? '../../../..' : '../../..', }, answers ), enforceNewFolder: true, installDependencies: false, - gitInit: true, + gitInit: !isKibanaPlugin, async post({ log }) { - await execa('yarn', ['kbn', 'bootstrap'], { - cwd: KBN_DIR, - stdio: 'inherit', - }); - - const dir = relative(process.cwd(), resolve(KBN_DIR, 'plugins', snakeCase(name))); + const dir = relative(process.cwd(), targetPath); + // Apply eslint to the generated plugin try { - await execa('yarn', ['lint', '--fix'], { - cwd: dir, - all: true, - }); + await execa('yarn', ['lint:es', `./${dir}/**/*.ts*`, '--no-ignore', '--fix']); } catch (error) { - throw new Error(`Failure when running prettier on the generated output: ${error.all}`); + console.error(error); + throw new Error( + `Failure when running prettier on the generated output: ${error.all || error}` + ); } log.success(chalk`🎉 -Your plugin has been created in {bold ${dir}}. Move into that directory to run it: +Your plugin has been created in {bold ${dir}}. - {bold cd "${dir}"} {bold yarn start} `); }, diff --git a/packages/kbn-plugin-generator/sao_template/sao.test.js b/packages/kbn-plugin-generator/sao_template/sao.test.js index 80149c008dad84..0dbdb7d3c097b8 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.test.js +++ b/packages/kbn-plugin-generator/sao_template/sao.test.js @@ -19,8 +19,6 @@ const sao = require('sao'); -const templatePkg = require('../package.json'); - const template = { fromPath: __dirname, configOptions: { @@ -32,121 +30,57 @@ function getFileContents(file) { return file.contents.toString(); } -function getConfig(file) { - const contents = getFileContents(file).replace(/\r?\n/gm, ''); - return contents.split('kibana.Plugin(')[1]; -} - describe('plugin generator sao integration', () => { test('skips files when answering no', async () => { const res = await sao.mockPrompt(template, { generateApp: false, - generateHack: false, generateApi: false, }); - expect(res.fileList).not.toContain('public/app.js'); - expect(res.fileList).not.toContain('public/__tests__/index.js'); - expect(res.fileList).not.toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).not.toContain('app:'); - expect(uiExports).not.toContain('hacks:'); - expect(uiExports).not.toContain('init(server, options)'); - expect(uiExports).not.toContain('registerFeature('); + expect(res.fileList).toContain('common/index.ts'); + expect(res.fileList).not.toContain('public/index.ts'); + expect(res.fileList).not.toContain('server/index.ts'); }); it('includes app when answering yes', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: false, - generateApi: false, - }); - - // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).not.toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); - expect(uiExports).not.toContain('hacks:'); - }); - - it('includes hack when answering yes', async () => { - const res = await sao.mockPrompt(template, { - generateApp: true, - generateHack: true, generateApi: false, }); // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('hacks:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); + expect(res.fileList).toContain('common/index.ts'); + expect(res.fileList).toContain('public/index.ts'); + expect(res.fileList).toContain('public/plugin.ts'); + expect(res.fileList).toContain('public/types.ts'); + expect(res.fileList).toContain('public/components/app.tsx'); + expect(res.fileList).not.toContain('server/index.ts'); }); it('includes server api when answering yes', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).toContain('public/hack.js'); - expect(res.fileList).toContain('server/routes/example.js'); - expect(res.fileList).toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('hacks:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); - }); - - it('plugin config has correct name and main path', async () => { - const res = await sao.mockPrompt(template, { - generateApp: true, - generateHack: true, - generateApi: true, - }); - - const indexContents = getFileContents(res.files['index.js']); - const nameLine = indexContents.match('name: (.*)')[1]; - const mainLine = indexContents.match('main: (.*)')[1]; - - expect(nameLine).toContain('some_fancy_plugin'); - expect(mainLine).toContain('plugins/some_fancy_plugin/app'); + expect(res.fileList).toContain('public/plugin.ts'); + expect(res.fileList).toContain('server/plugin.ts'); + expect(res.fileList).toContain('server/index.ts'); + expect(res.fileList).toContain('server/types.ts'); + expect(res.fileList).toContain('server/routes/index.ts'); }); - it('plugin package has correct name', async () => { + it('plugin package has correct title', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); - const packageContents = getFileContents(res.files['package.json']); - const pkg = JSON.parse(packageContents); + const contents = getFileContents(res.files['common/index.ts']); + const controllerLine = contents.match("PLUGIN_NAME = '(.*)'")[1]; - expect(pkg.name).toBe('some_fancy_plugin'); + expect(controllerLine).toContain('Some fancy plugin'); }); it('package has version "kibana" with master', async () => { @@ -154,10 +88,10 @@ describe('plugin generator sao integration', () => { kbnVersion: 'master', }); - const packageContents = getFileContents(res.files['package.json']); + const packageContents = getFileContents(res.files['kibana.json']); const pkg = JSON.parse(packageContents); - expect(pkg.kibana.version).toBe('kibana'); + expect(pkg.version).toBe('master'); }); it('package has correct version', async () => { @@ -165,39 +99,26 @@ describe('plugin generator sao integration', () => { kbnVersion: 'v6.0.0', }); - const packageContents = getFileContents(res.files['package.json']); - const pkg = JSON.parse(packageContents); - - expect(pkg.kibana.version).toBe('v6.0.0'); - }); - - it('package has correct templateVersion', async () => { - const res = await sao.mockPrompt(template, { - kbnVersion: 'master', - }); - - const packageContents = getFileContents(res.files['package.json']); + const packageContents = getFileContents(res.files['kibana.json']); const pkg = JSON.parse(packageContents); - expect(pkg.kibana.templateVersion).toBe(templatePkg.version); + expect(pkg.version).toBe('v6.0.0'); }); it('sample app has correct values', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); - const contents = getFileContents(res.files['public/app.js']); - const controllerLine = contents.match('setRootController(.*)')[1]; + const contents = getFileContents(res.files['common/index.ts']); + const controllerLine = contents.match("PLUGIN_ID = '(.*)'")[1]; expect(controllerLine).toContain('someFancyPlugin'); }); it('includes dotfiles', async () => { const res = await sao.mockPrompt(template); - expect(res.files['.gitignore']).toBeTruthy(); expect(res.files['.eslintrc.js']).toBeTruthy(); }); }); diff --git a/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json b/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json deleted file mode 100644 index 1a8aea88538768..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "paths": { - "<%= camelCase(name) %>": "./" - }, - "translations": [ - "translations/zh-CN.json" - ] -} - diff --git a/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json b/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json deleted file mode 100644 index 383368c7f8ce1d..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "styleSheetToCompile": "public/app.scss" -} diff --git a/packages/kbn-plugin-generator/sao_template/template/README.md b/packages/kbn-plugin-generator/sao_template/template/README.md index 59c3adf2713c8d..1e0139428fcbc4 100755 --- a/packages/kbn-plugin-generator/sao_template/template/README.md +++ b/packages/kbn-plugin-generator/sao_template/template/README.md @@ -6,34 +6,7 @@ --- -## development +## Development -See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following yarn scripts. +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. - - `yarn kbn bootstrap` - - Install dependencies and crosslink Kibana and all projects/plugins. - - > ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change. - - - `yarn start` - - Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana` - - ``` - yarn start --elasticsearch.hosts http://localhost:9220 - ``` - - - `yarn build` - - Build a distributable archive of your plugin. - - - `yarn test:browser` - - Run the browser tests in a real web browser. - - - `yarn test:mocha` - - Run the server tests using mocha. - -For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks checkout the `package.json` file, or run `yarn run`. diff --git a/packages/kbn-plugin-generator/sao_template/template/common/index.ts b/packages/kbn-plugin-generator/sao_template/template/common/index.ts new file mode 100644 index 00000000000000..90ffcb70045aa5 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = '<%= camelCase(name) %>'; +export const PLUGIN_NAME = '<%= name %>'; diff --git a/packages/kbn-plugin-generator/sao_template/template/eslintrc.js b/packages/kbn-plugin-generator/sao_template/template/eslintrc.js old mode 100755 new mode 100644 index e1dfadc212b7e5..b68d42e32e0472 --- a/packages/kbn-plugin-generator/sao_template/template/eslintrc.js +++ b/packages/kbn-plugin-generator/sao_template/template/eslintrc.js @@ -1,24 +1,9 @@ -module.exports = { - root: true, +module.exports = { + root: true, extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - settings: { - 'import/resolver': { - '@kbn/eslint-import-resolver-kibana': { - rootPackageName: '<%= snakeCase(name) %>', - }, - }, - }, - overrides: [ - { - files: ['**/public/**/*'], - settings: { - 'import/resolver': { - '@kbn/eslint-import-resolver-kibana': { - forceNode: false, - rootPackageName: '<%= snakeCase(name) %>', - }, - }, - }, - }, - ] -}; + <%_ if (!isKibanaPlugin) { -%> + rules: { + "@kbn/eslint/require-license-header": "off" + } + <%_ } -%> +}; \ No newline at end of file diff --git a/packages/kbn-plugin-generator/sao_template/template/gitignore b/packages/kbn-plugin-generator/sao_template/template/gitignore deleted file mode 100755 index db28fed19376d5..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/gitignore +++ /dev/null @@ -1,6 +0,0 @@ -npm-debug.log* -node_modules -/build/ -<%_ if (generateScss) { -%> -/public/app.css -<%_ } -%> diff --git a/packages/kbn-plugin-generator/sao_template/template/index.js b/packages/kbn-plugin-generator/sao_template/template/index.js deleted file mode 100755 index 4bc3347ae60194..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/index.js +++ /dev/null @@ -1,89 +0,0 @@ -<% if (generateScss) { -%> -import { resolve } from 'path'; -import { existsSync } from 'fs'; - -<% } -%> - -<% if (generateApp) { -%> -import { i18n } from '@kbn/i18n'; -<% } -%> - -<% if (generateApi) { -%> -import exampleRoute from './server/routes/example'; - -<% } -%> -export default function (kibana) { - return new kibana.Plugin({ - require: ['elasticsearch'], - name: '<%= snakeCase(name) %>', - uiExports: { - <%_ if (generateApp) { -%> - app: { - title: '<%= startCase(name) %>', - description: '<%= description %>', - main: 'plugins/<%= snakeCase(name) %>/app', - }, - <%_ } -%> - <%_ if (generateHack) { -%> - hacks: [ - 'plugins/<%= snakeCase(name) %>/hack' - ], - <%_ } -%> - <%_ if (generateScss) { -%> - styleSheetPaths: [resolve(__dirname, 'public/app.scss'), resolve(__dirname, 'public/app.css')].find(p => existsSync(p)), - <%_ } -%> - }, - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - <%_ if (generateApi || generateApp) { -%> - - // eslint-disable-next-line no-unused-vars - init(server, options) { - <%_ if (generateApp) { -%> - const xpackMainPlugin = server.plugins.xpack_main; - if (xpackMainPlugin) { - const featureId = '<%= snakeCase(name) %>'; - - xpackMainPlugin.registerFeature({ - id: featureId, - name: i18n.translate('<%= camelCase(name) %>.featureRegistry.featureName', { - defaultMessage: '<%= name %>', - }), - navLinkId: featureId, - icon: 'questionInCircle', - app: [featureId, 'kibana'], - catalogue: [], - privileges: { - all: { - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['show'], - }, - read: { - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['show'], - }, - }, - }); - } - <%_ } -%> - - <%_ if (generateApi) { -%> - // Add server routes and initialize the plugin here - exampleRoute(server); - <%_ } -%> - } - <%_ } -%> - }); -} diff --git a/packages/kbn-plugin-generator/sao_template/template/kibana.json b/packages/kbn-plugin-generator/sao_template/template/kibana.json new file mode 100644 index 00000000000000..f8bb07040abeb6 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "<%= camelCase(name) %>", + "version": "<%= kbnVersion %>", + "server": <%= hasServer %>, + "ui": <%= hasUi %>, + "requiredPlugins": ["navigation"], + "optionalPlugins": [] +} diff --git a/packages/kbn-plugin-generator/sao_template/template/package_template.json b/packages/kbn-plugin-generator/sao_template/template/package_template.json deleted file mode 100644 index 4b6629fa902685..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/package_template.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "<%= snakeCase(name) %>", - "version": "0.0.0", - "description": "<%= description %>", - "main": "index.js", - "kibana": { - "version": "<%= (kbnVersion === 'master') ? 'kibana' : kbnVersion %>", - "templateVersion": "<%= templateVersion %>" - }, - "scripts": { - "preinstall": "node ../../preinstall_check", - "kbn": "node ../../scripts/kbn", - "es": "node ../../scripts/es", - "lint": "eslint .", - "start": "plugin-helpers start", - "test:server": "plugin-helpers test:server", - "test:browser": "plugin-helpers test:browser", - "build": "plugin-helpers build" - }, - <%_ if (generateTranslations) { _%> - "dependencies": { - "@kbn/i18n": "link:../../packages/kbn-i18n" - }, - <%_ } _%> - "devDependencies": { - "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana", - "@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana", - "@kbn/expect": "link:../../packages/kbn-expect", - "@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers", - "babel-eslint": "^10.0.1", - "eslint": "^5.16.0", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jest": "^22.4.1", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-mocha": "^5.3.0", - "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-react": "^7.12.4" - } -} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js b/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js deleted file mode 100755 index 9320bd7b028a86..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import expect from '@kbn/expect'; - -describe('suite', () => { - it('is a test', () => { - expect(true).to.equal(true); - }); -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/app.js b/packages/kbn-plugin-generator/sao_template/template/public/app.js deleted file mode 100755 index 37a7c37e916a0f..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/app.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; -import { render, unmountComponentAtNode } from 'react-dom'; -<%_ if (generateTranslations) { _%> -import { I18nProvider } from '@kbn/i18n/react'; -<%_ } _%> - -import { Main } from './components/main'; - -const app = uiModules.get('apps/<%= camelCase(name) %>'); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -function RootController($scope, $element, $http) { - const domNode = $element[0]; - - // render react to DOM - <%_ if (generateTranslations) { _%> - render( - -
- , - domNode - ); - <%_ } else { _%> - render(
, domNode); - <%_ } _%> - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('<%= camelCase(name) %>', RootController); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/application.tsx b/packages/kbn-plugin-generator/sao_template/template/public/application.tsx new file mode 100644 index 00000000000000..8106a18a784e7b --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/application.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from '<%= relRoot %>/src/core/public'; +import { AppPluginStartDependencies } from './types'; +import { <%= upperCamelCaseName %>App } from './components/app'; + + +export const renderApp = ( + { notifications, http }: CoreStart, + { navigation }: AppPluginStartDependencies, + { appBasePath, element }: AppMountParameters + ) => { + ReactDOM.render( + <<%= upperCamelCaseName %>App + basename={appBasePath} + notifications={notifications} + http={http} + navigation={navigation} + />, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); + }; + \ No newline at end of file diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx new file mode 100644 index 00000000000000..7b259a9c5b99d6 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx @@ -0,0 +1,129 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import { + EuiButton, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageHeader, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { CoreStart } from '<%= relRoot %>/../src/core/public'; +import { NavigationPublicPluginStart } from '<%= relRoot %>/../src/plugins/navigation/public'; + +import { PLUGIN_ID, PLUGIN_NAME } from '../../common'; + +interface <%= upperCamelCaseName %>AppDeps { + basename: string; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; +} + +export const <%= upperCamelCaseName %>App = ({ basename, notifications, http, navigation }: <%= upperCamelCaseName %>AppDeps) => { + // Use React hooks to manage state. + const [timestamp, setTimestamp] = useState(); + + const onClickHandler = () => { +<%_ if (generateApi) { -%> + // Use the core http service to make a response to the server API. + http.get('/api/<%= snakeCase(name) %>/example').then(res => { + setTimestamp(res.time); + // Use the core notifications service to display a success message. + notifications.toasts.addSuccess(i18n.translate('<%= camelCase(name) %>.dataUpdated', { + defaultMessage: 'Data updated', + })); + }); +<%_ } else { -%> + setTimestamp(new Date().toISOString()); + notifications.toasts.addSuccess(PLUGIN_NAME); +<%_ } -%> + }; + + // Render the application DOM. + // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. + return ( + + + <> + + + + + +

+ +

+
+
+ + + +

+ +

+
+
+ + +

+ +

+ +

+ +

+ + + +
+
+
+
+
+ +
+
+ ); +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js b/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js deleted file mode 100644 index 68710baa1bee8e..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Main } from './main'; diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js b/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js deleted file mode 100644 index 59fd667c709aa2..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { - EuiPage, - EuiPageHeader, - EuiTitle, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentBody, - EuiText -} from '@elastic/eui'; -<%_ if (generateTranslations) { _%> -import { FormattedMessage } from '@kbn/i18n/react'; -<%_ } _%> - -export class Main extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { - /* - FOR EXAMPLE PURPOSES ONLY. There are much better ways to - manage state and update your UI than this. - */ - const { httpClient } = this.props; - httpClient.get('../api/<%= name %>/example').then((resp) => { - this.setState({ time: resp.data.time }); - }); - } - render() { - const { title } = this.props; - return ( - - - - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - {title} Hello World! - <%_ } _%> -

-
-
- - - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - Congratulations - <%_ } _%> -

-
-
- - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - You have successfully created your first Kibana Plugin! - <%_ } _%> -

-

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - The server time (via API call) is {this.state.time || 'NO API CALL YET'} - <%_ } _%> -

-
-
-
-
-
- ); - } -} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/hack.js b/packages/kbn-plugin-generator/sao_template/template/public/hack.js deleted file mode 100755 index 775526c8e44a30..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/hack.js +++ /dev/null @@ -1,7 +0,0 @@ -import $ from 'jquery'; - -$(document.body).on('keypress', function (event) { - if (event.which === 58) { - alert('boo!'); - } -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/app.scss b/packages/kbn-plugin-generator/sao_template/template/public/index.scss similarity index 100% rename from packages/kbn-plugin-generator/sao_template/template/public/app.scss rename to packages/kbn-plugin-generator/sao_template/template/public/index.scss diff --git a/packages/kbn-plugin-generator/sao_template/template/public/index.ts b/packages/kbn-plugin-generator/sao_template/template/public/index.ts new file mode 100644 index 00000000000000..2999dc7264ddb6 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/index.ts @@ -0,0 +1,16 @@ +<%_ if (hasScss) { -%> +import './index.scss'; +<%_ } -%> + +import { <%= upperCamelCaseName %>Plugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new <%= upperCamelCaseName %>Plugin(); +} +export { + <%= upperCamelCaseName %>PluginSetup, + <%= upperCamelCaseName %>PluginStart, +} from './types'; + diff --git a/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts b/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts new file mode 100644 index 00000000000000..76f7f1a6f99082 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts @@ -0,0 +1,42 @@ +import { i18n } from '@kbn/i18n'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '<%= relRoot %>/src/core/public'; +import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart, AppPluginStartDependencies } from './types'; +import { PLUGIN_NAME } from '../common'; + +export class <%= upperCamelCaseName %>Plugin + implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> { + + public setup(core: CoreSetup): <%= upperCamelCaseName %>PluginSetup { + // Register an application into the side navigation menu + core.application.register({ + id: '<%= camelCase(name) %>', + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + // Render the application + return renderApp(coreStart, depsStart as AppPluginStartDependencies, params); + }, + }); + + // Return methods that should be available to other plugins + return { + getGreeting() { + return i18n.translate('<%= camelCase(name) %>.greetingText', { + defaultMessage: 'Hello from {name}!', + values: { + name: PLUGIN_NAME, + }, + }); + }, + }; + } + + public start(core: CoreStart): <%= upperCamelCaseName %>PluginStart { + return {}; + } + + public stop() {} +} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/types.ts b/packages/kbn-plugin-generator/sao_template/template/public/types.ts new file mode 100644 index 00000000000000..2ebb0c0d1257f5 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/types.ts @@ -0,0 +1,11 @@ +import { NavigationPublicPluginStart } from '<%= relRoot %>/src/plugins/navigation/public'; + +export interface <%= upperCamelCaseName %>PluginSetup { + getGreeting: () => string; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginStart {} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js b/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js deleted file mode 100755 index 9320bd7b028a86..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import expect from '@kbn/expect'; - -describe('suite', () => { - it('is a test', () => { - expect(true).to.equal(true); - }); -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/server/index.ts b/packages/kbn-plugin-generator/sao_template/template/server/index.ts new file mode 100644 index 00000000000000..816b8faec2a45b --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/index.ts @@ -0,0 +1,15 @@ +import { PluginInitializerContext } from '<%= relRoot %>/src/core/server'; +import { <%= upperCamelCaseName %>Plugin } from './plugin'; + + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + + export function plugin(initializerContext: PluginInitializerContext) { + return new <%= upperCamelCaseName %>Plugin(initializerContext); +} + +export { + <%= upperCamelCaseName %>PluginSetup, + <%= upperCamelCaseName %>PluginStart, +} from './types'; diff --git a/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts b/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts new file mode 100644 index 00000000000000..d6a343209e39ec --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts @@ -0,0 +1,30 @@ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '<%= relRoot %>/src/core/server'; + +import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class <%= upperCamelCaseName %>Plugin + implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('<%= name %>: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('<%= name %>: Started'); + return {}; + } + + public stop() {} +} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js b/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js deleted file mode 100755 index 5a612645f48fc3..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function (server) { - - server.route({ - path: '/api/<%= name %>/example', - method: 'GET', - handler() { - return { time: (new Date()).toISOString() }; - } - }); - -} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts b/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts new file mode 100644 index 00000000000000..d8bb00f0dea6c5 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts @@ -0,0 +1,17 @@ +import { IRouter } from '<%= relRoot %>/../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/<%= snakeCase(name) %>/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/types.ts b/packages/kbn-plugin-generator/sao_template/template/server/types.ts new file mode 100644 index 00000000000000..adbc5e93f03c5f --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginStart {} diff --git a/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json b/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json deleted file mode 100644 index 3447511c6739a1..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "formats": { - "number": { - "currency": { - "style": "currency" - }, - "percent": { - "style": "percent" - } - }, - "date": { - "short": { - "month": "numeric", - "day": "numeric", - "year": "2-digit" - }, - "medium": { - "month": "short", - "day": "numeric", - "year": "numeric" - }, - "long": { - "month": "long", - "day": "numeric", - "year": "numeric" - }, - "full": { - "weekday": "long", - "month": "long", - "day": "numeric", - "year": "numeric" - } - }, - "time": { - "short": { - "hour": "numeric", - "minute": "numeric" - }, - "medium": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric" - }, - "long": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short" - }, - "full": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short" - } - }, - "relative": { - "years": { - "units": "year" - }, - "months": { - "units": "month" - }, - "days": { - "units": "day" - }, - "hours": { - "units": "hour" - }, - "minutes": { - "units": "minute" - }, - "seconds": { - "units": "second" - } - } - }, - "messages": { - "<%= camelCase(name) %>.congratulationsText": "您已经成功创建第一个 Kibana 插件。", - "<%= camelCase(name) %>.congratulationsTitle": "恭喜!", - "<%= camelCase(name) %>.helloWorldText": "{title} 您好,世界!", - "<%= camelCase(name) %>.serverTimeText": "服务器时间(通过 API 调用)为 {time}" - } -} diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json new file mode 100644 index 00000000000000..fe0f7112f1fa9e --- /dev/null +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["**/*", "index.js.d.ts"], + "exclude": ["sao_template/template/*"] +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 72926cae7dbc4b..96fd525efa3ec8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -56,7 +56,7 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid return transformDeprecations(settingsWithDefaults, logDeprecation); } -export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any) { +export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any = {}) { return new Config({ settings: await getSettingsFromFile(log, path, settingOverrides), primary: true, diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 47a47449927e49..ea15d2e34ea3a9 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "devDependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "@elastic/charts": "^16.1.0", "@kbn/dev-utils": "1.0.0", "@kbn/i18n": "1.0.0", diff --git a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap index 0ebc44ba67862a..cf3b48f237286b 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/nav_drawer.test.tsx.snap @@ -25,6 +25,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` Object { "baseUrl": "http://localhost:5601/app/siem", "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -36,6 +37,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` Object { "baseUrl": "http://localhost:5601/app/metrics", "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -90,6 +92,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` Object { "baseUrl": "http://localhost:5601/app/logs", "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -120,6 +123,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` }, Object { "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -136,6 +140,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` }, Object { "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -215,6 +220,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` }, Object { "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -390,7 +396,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` "listItems": Array [], "title": "Recent items", }, - "iconType": "clock", + "iconType": "recentlyViewedApp", "isDisabled": true, "label": "Recently viewed", }, @@ -408,7 +414,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` "aria-label": "Recently viewed", "className": "euiNavDrawerGroup__item", "data-name": "Recently viewed", - "iconType": "clock", + "iconType": "recentlyViewedApp", "isDisabled": true, "label": "Recently viewed", "onClick": [Function], @@ -427,7 +433,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` aria-label="Recently viewed" className="euiNavDrawerGroup__item" data-name="Recently viewed" - iconType="clock" + iconType="recentlyViewedApp" isDisabled={true} key="title-0" label="Recently viewed" @@ -463,18 +469,18 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` > - O - , - "iconType": undefined, + "iconType": "logoObservability", "label": "Observability", "onClick": [Function], "size": "s", @@ -689,6 +693,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` Object { "aria-label": "Security", "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -696,12 +701,8 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` "data-name": "Security", "data-test-subj": "navDrawerAppsMenuLink", "href": "http://localhost:5601/app/siem", - "icon": - S - , - "iconType": undefined, + "icon": undefined, + "iconType": "logoSecurity", "isActive": false, "isDisabled": undefined, "key": "siem", @@ -809,13 +810,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` className="euiNavDrawerGroup__item" data-name="Observability" data-test-subj="navDrawerCategory" - icon={ - - O - - } + iconType="logoObservability" key="title-1" label="Observability" onClick={[Function]} @@ -849,11 +844,30 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` onFocus={[Function]} type="button" > - - O - + + + + @@ -868,6 +882,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` aria-label="Security" category={ Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, } @@ -876,13 +891,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` data-name="Security" data-test-subj="navDrawerAppsMenuLink" href="http://localhost:5601/app/siem" - icon={ - - S - - } + iconType="logoSecurity" isActive={false} key="siem" label="Security" @@ -910,6 +919,7 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` aria-label="Security" category={ Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, } @@ -923,11 +933,30 @@ exports[`NavDrawer Advanced setting set to grouped renders grouped items 1`] = ` onFocus={[Function]} order={500} > - - S - + + + + @@ -1222,6 +1251,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t Object { "baseUrl": "http://localhost:5601/app/siem", "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -1233,6 +1263,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t Object { "baseUrl": "http://localhost:5601/app/metrics", "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -1285,6 +1316,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t }, Object { "category": Object { + "euiIconType": "logoSecurity", "label": "Security", "order": 3000, }, @@ -1301,6 +1333,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t }, Object { "category": Object { + "euiIconType": "logoObservability", "label": "Observability", "order": 2000, }, @@ -1508,7 +1541,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t "listItems": Array [], "title": "Recent items", }, - "iconType": "clock", + "iconType": "recentlyViewedApp", "isDisabled": true, "label": "Recently viewed", }, @@ -1526,7 +1559,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t "aria-label": "Recently viewed", "className": "euiNavDrawerGroup__item", "data-name": "Recently viewed", - "iconType": "clock", + "iconType": "recentlyViewedApp", "isDisabled": true, "label": "Recently viewed", "onClick": [Function], @@ -1545,7 +1578,7 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t aria-label="Recently viewed" className="euiNavDrawerGroup__item" data-name="Recently viewed" - iconType="clock" + iconType="recentlyViewedApp" isDisabled={true} key="title-0" label="Recently viewed" @@ -1581,18 +1614,18 @@ exports[`NavDrawer Advanced setting set to grouped renders individual items if t > { const fullOldPath = getPath(rootPath, oldKey); const oldValue = get(config, fullOldPath); @@ -40,11 +41,16 @@ const _rename = ( const newValue = get(config, fullNewPath); if (newValue === undefined) { set(config, fullNewPath, oldValue); - log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); + + if (!silent) { + log(`"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`); + } } else { - log( - `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` - ); + if (!silent) { + log( + `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"` + ); + } } return config; }; @@ -67,11 +73,11 @@ const _unused = ( const rename = (oldKey: string, newKey: string): ConfigDeprecation => (config, rootPath, log) => _rename(config, rootPath, log, oldKey, newKey); -const renameFromRoot = (oldKey: string, newKey: string): ConfigDeprecation => ( +const renameFromRoot = (oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation => ( config, rootPath, log -) => _rename(config, '', log, oldKey, newKey); +) => _rename(config, '', log, oldKey, newKey, silent); const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => _unused(config, rootPath, log, unusedKey); diff --git a/src/core/server/config/deprecation/types.ts b/src/core/server/config/deprecation/types.ts index 19fba7800c919d..dbfbad771f0749 100644 --- a/src/core/server/config/deprecation/types.ts +++ b/src/core/server/config/deprecation/types.ts @@ -102,7 +102,7 @@ export interface ConfigDeprecationFactory { * ] * ``` */ - renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; /** * Remove a configuration property from inside a plugin's configuration path. * Will log a deprecation warning if the unused key was found and deprecation applied. diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index f8ef49b0f6d18b..a9fc80c86d878e 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -62,6 +62,7 @@ beforeAll(() => { beforeEach(() => { config = { + name: 'kibana', host: '127.0.0.1', maxPayload: new ByteSizeValue(1024), port: 10002, @@ -1077,4 +1078,37 @@ describe('setup contract', () => { expect(isTlsEnabled).toBe(false); }); }); + + describe('#getServerInfo', () => { + it('returns correct information', async () => { + let { getServerInfo } = await server.setup(config); + + expect(getServerInfo()).toEqual({ + host: '127.0.0.1', + name: 'kibana', + port: 10002, + protocol: 'http', + }); + + ({ getServerInfo } = await server.setup({ + ...config, + port: 12345, + name: 'custom-name', + host: 'localhost', + })); + + expect(getServerInfo()).toEqual({ + host: 'localhost', + name: 'custom-name', + port: 12345, + protocol: 'http', + }); + }); + + it('returns correct protocol when ssl is enabled', async () => { + const { getServerInfo } = await server.setup(configWithSSL); + + expect(getServerInfo().protocol).toEqual('https'); + }); + }); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index fdc272041ce354..025ab2bf56ac2d 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -35,7 +35,7 @@ import { import { IsAuthenticated, AuthStateStorage, GetAuthState } from './auth_state_storage'; import { AuthHeadersStorage, GetAuthHeaders } from './auth_headers_storage'; import { BasePath } from './base_path_service'; -import { HttpServiceSetup } from './types'; +import { HttpServiceSetup, HttpServerInfo } from './types'; /** @internal */ export interface HttpServerSetup { @@ -58,6 +58,7 @@ export interface HttpServerSetup { get: GetAuthState; isAuthenticated: IsAuthenticated; }; + getServerInfo: () => HttpServerInfo; } /** @internal */ @@ -122,6 +123,12 @@ export class HttpServer { isAuthenticated: this.authState.isAuthenticated, }, getAuthHeaders: this.authRequestHeaders.get, + getServerInfo: () => ({ + name: config.name, + host: config.host, + port: config.port, + protocol: this.server!.info.protocol, + }), isTlsEnabled: config.ssl.enabled, // Return server instance with the connection options so that we can properly // bridge core and the "legacy" Kibana internally. Once this bridge isn't diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 2b2d98d937e859..30032ff5da7968 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -77,12 +77,19 @@ const createSetupContractMock = () => { auth: createAuthMock(), getAuthHeaders: jest.fn(), isTlsEnabled: false, + getServerInfo: jest.fn(), }; setupContract.createCookieSessionStorageFactory.mockResolvedValue( sessionStorageMock.createFactory() ); setupContract.createRouter.mockImplementation(() => mockRouter.create()); setupContract.getAuthHeaders.mockReturnValue({ authorization: 'authorization-header' }); + setupContract.getServerInfo.mockReturnValue({ + host: 'localhost', + name: 'kibana', + port: 80, + protocol: 'http', + }); return setupContract; }; diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index b16352838fad11..6dc7ece1359df7 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -721,6 +721,7 @@ describe('Auth', () => { res.ok({ headers: { 'www-authenticate': 'from handler', + 'another-header': 'yet another header', }, }) ); diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts index 45d7478df98051..50d3d7b47bf8d6 100644 --- a/src/core/server/http/lifecycle/on_pre_response.ts +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -120,8 +120,8 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo ...(result.headers as any), // hapi types don't specify string[] as valid value }; } else { + findHeadersIntersection(response.headers, result.headers, log); for (const [headerName, headerValue] of Object.entries(result.headers)) { - findHeadersIntersection(response.headers, result.headers, log); response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value } } diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 01b852c26ec934..63278441080556 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -252,6 +252,11 @@ export interface HttpServiceSetup { contextName: T, provider: RequestHandlerContextProvider ) => RequestHandlerContextContainer; + + /** + * Provides common {@link HttpServerInfo | information} about the running http server. + */ + getServerInfo: () => HttpServerInfo; } /** @internal */ @@ -273,3 +278,15 @@ export interface HttpServiceStart { /** Indicates if http server is listening on a given port */ isListening: (port: number) => boolean; } + +/** @public */ +export interface HttpServerInfo { + /** The name of the Kibana server */ + name: string; + /** The hostname of the server */ + host: string; + /** The port the server is listening on */ + port: number; + /** The protocol used by the server */ + protocol: 'http' | 'https' | 'socket'; +} diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 91f38c9f2ddbe9..c45acd7f0129a1 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -103,6 +103,7 @@ export { GetAuthState, HttpResponseOptions, HttpResponsePayload, + HttpServerInfo, HttpServiceSetup, HttpServiceStart, ErrorHttpResponseOptions, diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts index d46960289a8d01..17f77a6e9328f3 100644 --- a/src/core/server/kibana_config.ts +++ b/src/core/server/kibana_config.ts @@ -25,9 +25,7 @@ export const config = { path: 'kibana', schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), - defaultAppId: schema.string({ defaultValue: 'home' }), index: schema.string({ defaultValue: '.kibana' }), - disableWelcomeScreen: schema.boolean({ defaultValue: false }), autocompleteTerminateAfter: schema.duration({ defaultValue: 100000 }), autocompleteTimeout: schema.duration({ defaultValue: 1000 }), }), diff --git a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 74ecaa9f09c0e1..3b16bed92df976 100644 --- a/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/src/core/server/legacy/config/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -72,6 +72,7 @@ Object { }, }, }, + "loggers": undefined, "root": Object { "level": "off", }, @@ -90,6 +91,7 @@ Object { }, }, }, + "loggers": undefined, "root": Object { "level": "all", }, diff --git a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts index 30bb150e6c15a8..3e496648c3af90 100644 --- a/src/core/server/legacy/config/legacy_object_to_config_adapter.ts +++ b/src/core/server/legacy/config/legacy_object_to_config_adapter.ts @@ -19,12 +19,13 @@ import { ConfigPath } from '../../config'; import { ObjectToConfigAdapter } from '../../config/object_to_config_adapter'; +import { LoggingConfigType } from '../../logging/logging_config'; import { LegacyVars } from '../types'; /** * Represents logging config supported by the legacy platform. */ -interface LegacyLoggingConfig { +export interface LegacyLoggingConfig { silent?: boolean; verbose?: boolean; quiet?: boolean; @@ -33,18 +34,24 @@ interface LegacyLoggingConfig { events?: Record; } +type MixedLoggingConfig = LegacyLoggingConfig & Partial; + /** * Represents adapter between config provided by legacy platform and `Config` * supported by the current platform. * @internal */ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { - private static transformLogging(configValue: LegacyLoggingConfig = {}) { + private static transformLogging(configValue: MixedLoggingConfig = {}) { + const { appenders, root, loggers, ...legacyLoggingConfig } = configValue; + const loggingConfig = { appenders: { - default: { kind: 'legacy-appender', legacyLoggingConfig: configValue }, + ...appenders, + default: { kind: 'legacy-appender', legacyLoggingConfig }, }, - root: { level: 'info' }, + root: { level: 'info', ...root }, + loggers, }; if (configValue.silent) { diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts new file mode 100644 index 00000000000000..66234f677903f8 --- /dev/null +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -0,0 +1,239 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +import { + getPlatformLogsFromMock, + getLegacyPlatformLogsFromMock, +} from '../../logging/integration_tests/utils'; + +import { LegacyLoggingConfig } from '../config/legacy_object_to_config_adapter'; + +function createRoot(legacyLoggingConfig: LegacyLoggingConfig = {}) { + return kbnTestServer.createRoot({ + migrations: { skip: true }, // otherwise stuck in polling ES + logging: { + // legacy platform config + silent: false, + json: false, + ...legacyLoggingConfig, + events: { + log: ['test-file-legacy'], + }, + // platform config + appenders: { + 'test-console': { + kind: 'console', + layout: { + highlight: false, + kind: 'pattern', + }, + }, + }, + loggers: [ + { + context: 'test-file', + appenders: ['test-console'], + level: 'info', + }, + ], + }, + }); +} + +describe('logging service', () => { + let mockConsoleLog: jest.SpyInstance; + let mockStdout: jest.SpyInstance; + + beforeAll(async () => { + mockConsoleLog = jest.spyOn(global.console, 'log'); + mockStdout = jest.spyOn(global.process.stdout, 'write'); + }); + + afterAll(async () => { + mockConsoleLog.mockRestore(); + mockStdout.mockRestore(); + }); + + describe('compatibility', () => { + describe('uses configured loggers', () => { + let root: ReturnType; + beforeAll(async () => { + root = createRoot(); + + await root.setup(); + await root.start(); + }, 30000); + + afterAll(async () => { + await root.shutdown(); + }); + + beforeEach(() => { + mockConsoleLog.mockClear(); + mockStdout.mockClear(); + }); + + it('when context matches', async () => { + root.logger.get('test-file').info('handled by NP'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + const loggedString = getPlatformLogsFromMock(mockConsoleLog); + expect(loggedString).toMatchInlineSnapshot(` + Array [ + "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] handled by NP", + ] + `); + }); + + it('falls back to the root legacy logger otherwise', async () => { + root.logger.get('test-file-legacy').info('handled by LP'); + + expect(mockStdout).toHaveBeenCalledTimes(1); + + const loggedString = getLegacyPlatformLogsFromMock(mockStdout); + expect(loggedString).toMatchInlineSnapshot(` + Array [ + " log [xx:xx:xx.xxx] [info][test-file-legacy] handled by LP + ", + ] + `); + }); + }); + + describe('logging config respects legacy logging settings', () => { + let root: ReturnType; + + afterEach(async () => { + mockConsoleLog.mockClear(); + mockStdout.mockClear(); + await root.shutdown(); + }); + + it('"silent": true', async () => { + root = createRoot({ silent: true }); + + await root.setup(); + await root.start(); + + const platformLogger = root.logger.get('test-file'); + platformLogger.info('info'); + platformLogger.warn('warn'); + platformLogger.error('error'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(3); + + expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` + Array [ + "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + ] + `); + + mockStdout.mockClear(); + + const legacyPlatformLogger = root.logger.get('test-file-legacy'); + legacyPlatformLogger.info('info'); + legacyPlatformLogger.warn('warn'); + legacyPlatformLogger.error('error'); + + expect(mockStdout).toHaveBeenCalledTimes(0); + }); + + it('"quiet": true', async () => { + root = createRoot({ quiet: true }); + + await root.setup(); + await root.start(); + + const platformLogger = root.logger.get('test-file'); + platformLogger.info('info'); + platformLogger.warn('warn'); + platformLogger.error('error'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(3); + + expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` + Array [ + "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + ] + `); + + mockStdout.mockClear(); + + const legacyPlatformLogger = root.logger.get('test-file-legacy'); + legacyPlatformLogger.info('info'); + legacyPlatformLogger.warn('warn'); + legacyPlatformLogger.error('error'); + + expect(mockStdout).toHaveBeenCalledTimes(1); + expect(getLegacyPlatformLogsFromMock(mockStdout)).toMatchInlineSnapshot(` + Array [ + " log [xx:xx:xx.xxx] [error][test-file-legacy] error + ", + ] + `); + }); + + it('"verbose": true', async () => { + root = createRoot({ verbose: true }); + + await root.setup(); + await root.start(); + + const platformLogger = root.logger.get('test-file'); + platformLogger.info('info'); + platformLogger.warn('warn'); + platformLogger.error('error'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(3); + + expect(getPlatformLogsFromMock(mockConsoleLog)).toMatchInlineSnapshot(` + Array [ + "[xxxx-xx-xxTxx:xx:xx.xxxZ][INFO ][test-file] info", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][WARN ][test-file] warn", + "[xxxx-xx-xxTxx:xx:xx.xxxZ][ERROR][test-file] error", + ] + `); + + mockStdout.mockClear(); + + const legacyPlatformLogger = root.logger.get('test-file-legacy'); + legacyPlatformLogger.info('info'); + legacyPlatformLogger.warn('warn'); + legacyPlatformLogger.error('error'); + + expect(mockStdout).toHaveBeenCalledTimes(3); + expect(getLegacyPlatformLogsFromMock(mockStdout)).toMatchInlineSnapshot(` + Array [ + " log [xx:xx:xx.xxx] [info][test-file-legacy] info + ", + " log [xx:xx:xx.xxx] [warning][test-file-legacy] warn + ", + " log [xx:xx:xx.xxx] [error][test-file-legacy] error + ", + ] + `); + }); + }); + }); +}); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index d0e0453564f943..f9b18afadc9382 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -292,6 +292,7 @@ export class LegacyService implements CoreService { }, csp: setupDeps.core.http.csp, isTlsEnabled: setupDeps.core.http.isTlsEnabled, + getServerInfo: setupDeps.core.http.getServerInfo, }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, diff --git a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap index 10509b20e89423..fe1407563a6351 100644 --- a/src/core/server/logging/__snapshots__/logging_config.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_config.test.ts.snap @@ -13,6 +13,8 @@ Object { } `; +exports[`\`schema\` throws if \`root\` logger does not have "default" appender configured. 1`] = `"[root]: \\"default\\" appender required for migration period till the next major release"`; + exports[`\`schema\` throws if \`root\` logger does not have appenders configured. 1`] = `"[root.appenders]: array size is [0], but cannot be smaller than [1]"`; exports[`fails if loggers use unknown appenders. 1`] = `"Logger \\"some.nested.context\\" contains unsupported appender key \\"unknown\\"."`; diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index 871acb8c465ca8..3aa86495e4d825 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -42,12 +42,6 @@ export type AppenderConfigType = TypeOf; */ export interface Appender { append(record: LogRecord): void; - - /** - * Used to signal to `Logger` that log level filtering should be ignored for this appender. Defaults to `false`. - * @deprecated Should be removed once the `LegacyAppender` is removed. - */ - receiveAllLevels?: boolean; } /** diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts new file mode 100644 index 00000000000000..7142f91300f124 --- /dev/null +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +function createRoot() { + return kbnTestServer.createRoot({ + logging: { + silent: true, // set "true" in kbnTestServer + appenders: { + 'test-console': { + kind: 'console', + layout: { + highlight: false, + kind: 'pattern', + pattern: '{level}|{context}|{message}', + }, + }, + }, + loggers: [ + { + context: 'parent', + appenders: ['test-console'], + level: 'warn', + }, + { + context: 'parent.child', + appenders: ['test-console'], + level: 'error', + }, + ], + }, + }); +} + +describe('logging service', () => { + describe('logs according to context hierarchy', () => { + let root: ReturnType; + let mockConsoleLog: jest.SpyInstance; + beforeAll(async () => { + mockConsoleLog = jest.spyOn(global.console, 'log'); + root = createRoot(); + + await root.setup(); + }, 30000); + + beforeEach(() => { + mockConsoleLog.mockClear(); + }); + + afterAll(async () => { + mockConsoleLog.mockRestore(); + await root.shutdown(); + }); + + it('uses the most specific context', () => { + const logger = root.logger.get('parent.child'); + + logger.error('error from "parent.child" context'); + logger.warn('warning from "parent.child" context'); + logger.info('info from "parent.child" context'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(1); + expect(mockConsoleLog).toHaveBeenCalledWith( + 'ERROR|parent.child|error from "parent.child" context' + ); + }); + + it('uses parent context', () => { + const logger = root.logger.get('parent.another-child'); + + logger.error('error from "parent.another-child" context'); + logger.warn('warning from "parent.another-child" context'); + logger.info('info from "parent.another-child" context'); + + expect(mockConsoleLog).toHaveBeenCalledTimes(2); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 1, + 'ERROR|parent.another-child|error from "parent.another-child" context' + ); + expect(mockConsoleLog).toHaveBeenNthCalledWith( + 2, + 'WARN |parent.another-child|warning from "parent.another-child" context' + ); + }); + + it('falls back to the root settings', () => { + const logger = root.logger.get('fallback'); + + logger.error('error from "fallback" context'); + logger.warn('warning from fallback" context'); + logger.info('info from "fallback" context'); + + // output muted by silent: true + expect(mockConsoleLog).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/src/core/server/logging/integration_tests/utils.ts b/src/core/server/logging/integration_tests/utils.ts new file mode 100644 index 00000000000000..81a76ce76ad733 --- /dev/null +++ b/src/core/server/logging/integration_tests/utils.ts @@ -0,0 +1,68 @@ +/* + * 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 Fs from 'fs'; +import Util from 'util'; +const readFile = Util.promisify(Fs.readFile); + +function replaceAllNumbers(input: string) { + return input.replace(/\d/g, 'x'); +} + +function replaceTimestamp(input: string) { + return input.replace(/\[(.*?)\]/, (full, key) => `[${replaceAllNumbers(key)}]`); +} + +function stripColors(input: string) { + return input.replace(/\u001b[^m]+m/g, ''); +} + +function normalizePlatformLogging(input: string) { + return replaceTimestamp(input); +} + +function normalizeLegacyPlatformLogging(input: string) { + return replaceTimestamp(stripColors(input)); +} + +export function getPlatformLogsFromMock(logMock: jest.SpyInstance) { + return logMock.mock.calls.map(([message]) => message).map(normalizePlatformLogging); +} + +export function getLegacyPlatformLogsFromMock(stdoutMock: jest.SpyInstance) { + return stdoutMock.mock.calls + .map(([message]) => message) + .map(String) + .map(normalizeLegacyPlatformLogging); +} + +export async function getPlatformLogsFromFile(path: string) { + const fileContent = await readFile(path, 'utf-8'); + return fileContent + .split('\n') + .map(s => normalizePlatformLogging(s)) + .join('\n'); +} + +export async function getLegacyPlatformLogsFromFile(path: string) { + const fileContent = await readFile(path, 'utf-8'); + return fileContent + .split('\n') + .map(s => normalizeLegacyPlatformLogging(s)) + .join('\n'); +} diff --git a/src/core/server/logging/logger.test.ts b/src/core/server/logging/logger.test.ts index eeebb8ad5a0fa7..026e24fc5df543 100644 --- a/src/core/server/logging/logger.test.ts +++ b/src/core/server/logging/logger.test.ts @@ -410,85 +410,3 @@ test('passes log record to appenders only if log level is supported.', () => { }); } }); - -test('passes log record to appender with receiveAllLevels: true, regardless if log level is supported', () => { - const receiveAllAppender = { append: jest.fn(), receiveAllLevels: true }; - const warnLogger = new BaseLogger(context, LogLevel.Warn, [receiveAllAppender], factory); - - warnLogger.trace('trace-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(1); - expect(receiveAllAppender.append.mock.calls[0][0]).toMatchObject({ - level: LogLevel.Trace, - message: 'trace-message', - }); - - warnLogger.debug('debug-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(2); - expect(receiveAllAppender.append.mock.calls[1][0]).toMatchObject({ - level: LogLevel.Debug, - message: 'debug-message', - }); - - warnLogger.info('info-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(3); - expect(receiveAllAppender.append.mock.calls[2][0]).toMatchObject({ - level: LogLevel.Info, - message: 'info-message', - }); - - warnLogger.warn('warn-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(4); - expect(receiveAllAppender.append.mock.calls[3][0]).toMatchObject({ - level: LogLevel.Warn, - message: 'warn-message', - }); - - warnLogger.error('error-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(5); - expect(receiveAllAppender.append.mock.calls[4][0]).toMatchObject({ - level: LogLevel.Error, - message: 'error-message', - }); - - warnLogger.fatal('fatal-message'); - expect(receiveAllAppender.append).toHaveBeenCalledTimes(6); - expect(receiveAllAppender.append.mock.calls[5][0]).toMatchObject({ - level: LogLevel.Fatal, - message: 'fatal-message', - }); -}); - -test('passes log record to appender with receiveAllLevels: false, only if log level is supported', () => { - const notReceiveAllAppender = { append: jest.fn(), receiveAllLevels: false }; - const warnLogger = new BaseLogger(context, LogLevel.Warn, [notReceiveAllAppender], factory); - - warnLogger.trace('trace-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(0); - - warnLogger.debug('debug-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(0); - - warnLogger.info('info-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(0); - - warnLogger.warn('warn-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(1); - expect(notReceiveAllAppender.append.mock.calls[0][0]).toMatchObject({ - level: LogLevel.Warn, - message: 'warn-message', - }); - - warnLogger.error('error-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(2); - expect(notReceiveAllAppender.append.mock.calls[1][0]).toMatchObject({ - level: LogLevel.Error, - message: 'error-message', - }); - - warnLogger.fatal('fatal-message'); - expect(notReceiveAllAppender.append).toHaveBeenCalledTimes(3); - expect(notReceiveAllAppender.append.mock.calls[2][0]).toMatchObject({ - level: LogLevel.Fatal, - message: 'fatal-message', - }); -}); diff --git a/src/core/server/logging/logger.ts b/src/core/server/logging/logger.ts index ab6906ff5d6847..ac79c1916c07ba 100644 --- a/src/core/server/logging/logger.ts +++ b/src/core/server/logging/logger.ts @@ -136,12 +136,12 @@ export class BaseLogger implements Logger { } public log(record: LogRecord) { - const supportedLevel = this.level.supports(record.level); + if (!this.level.supports(record.level)) { + return; + } for (const appender of this.appenders) { - if (supportedLevel || appender.receiveAllLevels) { - appender.append(record); - } + appender.append(record); } } diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index 8eb79ac46e499a..b3631abb9ff002 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -33,6 +33,16 @@ test('`schema` throws if `root` logger does not have appenders configured.', () ).toThrowErrorMatchingSnapshot(); }); +test('`schema` throws if `root` logger does not have "default" appender configured.', () => { + expect(() => + config.schema.validate({ + root: { + appenders: ['console'], + }, + }) + ).toThrowErrorMatchingSnapshot(); +}); + test('`getParentLoggerContext()` returns correct parent context name.', () => { expect(LoggingConfig.getParentLoggerContext('a.b.c')).toEqual('a.b'); expect(LoggingConfig.getParentLoggerContext('a.b')).toEqual('a'); @@ -46,15 +56,23 @@ test('`getLoggerContext()` returns correct joined context name.', () => { expect(LoggingConfig.getLoggerContext([])).toEqual('root'); }); -test('correctly fills in default `appenders` config.', () => { +test('correctly fills in default config.', () => { const configValue = new LoggingConfig(config.schema.validate({})); - expect(configValue.appenders.size).toBe(1); + expect(configValue.appenders.size).toBe(3); expect(configValue.appenders.get('default')).toEqual({ kind: 'console', layout: { kind: 'pattern', highlight: true }, }); + expect(configValue.appenders.get('console')).toEqual({ + kind: 'console', + layout: { kind: 'pattern', highlight: true }, + }); + expect(configValue.appenders.get('file')).toEqual({ + kind: 'file', + layout: { kind: 'pattern', highlight: false }, + }); }); test('correctly fills in custom `appenders` config.', () => { diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index 84d707a3247e67..f1fbf787737b4a 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -72,13 +72,22 @@ export const config = { loggers: schema.arrayOf(createLoggerSchema, { defaultValue: [], }), - root: schema.object({ - appenders: schema.arrayOf(schema.string(), { - defaultValue: [DEFAULT_APPENDER_NAME], - minSize: 1, - }), - level: createLevelSchema, - }), + root: schema.object( + { + appenders: schema.arrayOf(schema.string(), { + defaultValue: [DEFAULT_APPENDER_NAME], + minSize: 1, + }), + level: createLevelSchema, + }, + { + validate(rawConfig) { + if (!rawConfig.appenders.includes(DEFAULT_APPENDER_NAME)) { + return `"${DEFAULT_APPENDER_NAME}" appender required for migration period till the next major release`; + } + }, + } + ), }), }; @@ -118,12 +127,26 @@ export class LoggingConfig { */ public readonly appenders: Map = new Map([ [ - DEFAULT_APPENDER_NAME, + 'default', + { + kind: 'console', + layout: { kind: 'pattern', highlight: true }, + } as AppenderConfigType, + ], + [ + 'console', { kind: 'console', layout: { kind: 'pattern', highlight: true }, } as AppenderConfigType, ], + [ + 'file', + { + kind: 'file', + layout: { kind: 'pattern', highlight: false }, + } as AppenderConfigType, + ], ]); /** diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 50ce507520d048..97f836f8ef37d7 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -43,7 +43,7 @@ import { uuidServiceMock } from './uuid/uuid_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { - kibana: { defaultAppId: 'home-mocks', index: '.kibana-tests' }, + kibana: { index: '.kibana-tests' }, elasticsearch: { shardTimeout: duration('30s'), requestTimeout: duration('30s'), @@ -105,6 +105,7 @@ function createCoreSetupMock() { get: httpService.auth.get, isAuthenticated: httpService.auth.isAuthenticated, }, + getServerInfo: httpService.getServerInfo, }; httpMock.createRouter.mockImplementation(() => httpService.createRouter('')); diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 3fcd7fbbbe1ff9..823299771544cd 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -75,7 +75,7 @@ describe('Plugin Context', () => { .pipe(first()) .toPromise(); expect(configObject).toStrictEqual({ - kibana: { defaultAppId: 'home', index: '.kibana' }, + kibana: { index: '.kibana' }, elasticsearch: { shardTimeout: duration(30, 's'), requestTimeout: duration(30, 's'), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 30e5209b2fc6a1..77300900e84f33 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -164,6 +164,7 @@ export function createPluginSetupContext( auth: { get: deps.http.auth.get, isAuthenticated: deps.http.auth.isAuthenticated }, csp: deps.http.csp, isTlsEnabled: deps.http.isTlsEnabled, + getServerInfo: deps.http.getServerInfo, }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index a89e2f8c684e40..9ae04787767bb6 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -214,7 +214,7 @@ export interface Plugin< export const SharedGlobalConfigKeys = { // We can add more if really needed - kibana: ['defaultAppId', 'index'] as const, + kibana: ['index'] as const, elasticsearch: ['shardTimeout', 'requestTimeout', 'pingTimeout', 'startupTimeout'] as const, path: ['data'] as const, }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e4ea06769007a1..fb27fcccc2abe1 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -509,7 +509,7 @@ export type ConfigDeprecation = (config: Record, fromPath: string, // @public export interface ConfigDeprecationFactory { rename(oldKey: string, newKey: string): ConfigDeprecation; - renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + renameFromRoot(oldKey: string, newKey: string, silent?: boolean): ConfigDeprecation; unused(unusedKey: string): ConfigDeprecation; unusedFromRoot(unusedKey: string): ConfigDeprecation; } @@ -736,6 +736,14 @@ export interface HttpResponseOptions { // @public export type HttpResponsePayload = undefined | string | Record | Buffer | Stream; +// @public (undocumented) +export interface HttpServerInfo { + host: string; + name: string; + port: number; + protocol: 'http' | 'https' | 'socket'; +} + // @public export interface HttpServiceSetup { // (undocumented) @@ -747,6 +755,7 @@ export interface HttpServiceSetup { createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => Promise>; createRouter: () => IRouter; csp: ICspConfig; + getServerInfo: () => HttpServerInfo; isTlsEnabled: boolean; registerAuth: (handler: AuthenticationHandler) => void; registerOnPostAuth: (handler: OnPostAuthHandler) => void; diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts index 3e3cc2fef2a229..2285bd6afd3651 100644 --- a/src/core/utils/default_app_categories.ts +++ b/src/core/utils/default_app_categories.ts @@ -31,6 +31,7 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ label: i18n.translate('core.ui.observabilityNavList.label', { defaultMessage: 'Observability', }), + euiIconType: 'logoObservability', order: 2000, }, security: { @@ -38,6 +39,7 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({ defaultMessage: 'Security', }), order: 3000, + euiIconType: 'logoSecurity', }, management: { label: i18n.translate('core.ui.managementNavList.label', { diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 5217fdf002be9c..823c70e80fe7cd 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -56,23 +56,23 @@ export KIBANA_PKG_BRANCH="$kbnBranch" ### ### download node ### +nodeVersion="$(cat "$dir/.node-version")" +nodeDir="$cacheDir/node/$nodeVersion" +nodeBin="$nodeDir/bin" +classifier="x64.tar.gz" + UNAME=$(uname) OS="linux" if [[ "$UNAME" = *"MINGW64_NT"* ]]; then OS="win" + nodeBin="$HOME/node" + classifier="x64.zip" +elif [[ "$UNAME" == "Darwin" ]]; then + OS="darwin" fi echo " -- Running on OS: $OS" -nodeVersion="$(cat "$dir/.node-version")" -nodeDir="$cacheDir/node/$nodeVersion" - -if [[ "$OS" == "win" ]]; then - nodeBin="$HOME/node" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-win-x64.zip" -else - nodeBin="$nodeDir/bin" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-linux-x64.tar.gz" -fi +nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-${OS}-${classifier}" if [[ "$installNode" == "true" ]]; then echo " -- node: version=v${nodeVersion} dir=$nodeDir" diff --git a/src/es_archiver/actions/edit.js b/src/es_archiver/actions/edit.ts similarity index 91% rename from src/es_archiver/actions/edit.js rename to src/es_archiver/actions/edit.ts index 5e3a3490133c71..de63081a1ea1b5 100644 --- a/src/es_archiver/actions/edit.js +++ b/src/es_archiver/actions/edit.ts @@ -22,12 +22,23 @@ import Fs from 'fs'; import { createGunzip, createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { promisify } from 'util'; import globby from 'globby'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; const unlinkAsync = promisify(Fs.unlink); -export async function editAction({ prefix, dataDir, log, handler }) { +export async function editAction({ + prefix, + dataDir, + log, + handler, +}: { + prefix: string; + dataDir: string; + log: ToolingLog; + handler: () => Promise; +}) { const archives = ( await globby('**/*.gz', { cwd: prefix ? resolve(dataDir, prefix) : dataDir, diff --git a/src/es_archiver/actions/empty_kibana_index.js b/src/es_archiver/actions/empty_kibana_index.ts similarity index 73% rename from src/es_archiver/actions/empty_kibana_index.js rename to src/es_archiver/actions/empty_kibana_index.ts index 386863ec18a43d..5f96fbc5f996ca 100644 --- a/src/es_archiver/actions/empty_kibana_index.js +++ b/src/es_archiver/actions/empty_kibana_index.ts @@ -16,13 +16,25 @@ * specific language governing permissions and limitations * under the License. */ + +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; + import { migrateKibanaIndex, deleteKibanaIndices, createStats } from '../lib'; -export async function emptyKibanaIndexAction({ client, log, kbnClient }) { +export async function emptyKibanaIndexAction({ + client, + log, + kbnClient, +}: { + client: Client; + log: ToolingLog; + kbnClient: KbnClient; +}) { const stats = createStats('emptyKibanaIndex', log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); - await deleteKibanaIndices({ client, stats }); - await migrateKibanaIndex({ client, log, stats, kibanaPluginIds }); + await deleteKibanaIndices({ client, stats, log }); + await migrateKibanaIndex({ client, log, kibanaPluginIds }); return stats; } diff --git a/src/es_archiver/actions/index.js b/src/es_archiver/actions/index.ts similarity index 100% rename from src/es_archiver/actions/index.js rename to src/es_archiver/actions/index.ts diff --git a/src/es_archiver/actions/load.js b/src/es_archiver/actions/load.ts similarity index 84% rename from src/es_archiver/actions/load.js rename to src/es_archiver/actions/load.ts index ea02ce9dd3ad33..404fd0daea91d4 100644 --- a/src/es_archiver/actions/load.js +++ b/src/es_archiver/actions/load.ts @@ -19,6 +19,9 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable } from 'stream'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '../../legacy/utils'; @@ -38,12 +41,26 @@ import { // pipe a series of streams into each other so that data and errors // flow from the first stream to the last. Errors from the last stream // are not listened for -const pipeline = (...streams) => +const pipeline = (...streams: Readable[]) => streams.reduce((source, dest) => - source.once('error', error => dest.emit('error', error)).pipe(dest) + source.once('error', error => dest.emit('error', error)).pipe(dest as any) ); -export async function loadAction({ name, skipExisting, client, dataDir, log, kbnClient }) { +export async function loadAction({ + name, + skipExisting, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + skipExisting: boolean; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const files = prioritizeMappings(await readDirectory(inputDir)); @@ -64,12 +81,12 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn { objectMode: true } ); - const progress = new Progress('load progress'); + const progress = new Progress(); progress.activate(log); await createPromiseFromStreams([ recordStream, - createCreateIndexStream({ client, stats, skipExisting, log, kibanaPluginIds }), + createCreateIndexStream({ client, stats, skipExisting, log }), createIndexDocRecordsStream(client, stats, progress), ]); @@ -77,7 +94,7 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn const result = stats.toJSON(); for (const [index, { docs }] of Object.entries(result)) { - if (!docs && docs.indexed > 0) { + if (docs && docs.indexed > 0) { log.info('[%s] Indexed %d docs into %j', name, docs.indexed, index); } } diff --git a/src/es_archiver/actions/rebuild_all.js b/src/es_archiver/actions/rebuild_all.ts similarity index 84% rename from src/es_archiver/actions/rebuild_all.js rename to src/es_archiver/actions/rebuild_all.ts index 9379a29c38130c..1467a1d0430b76 100644 --- a/src/es_archiver/actions/rebuild_all.js +++ b/src/es_archiver/actions/rebuild_all.ts @@ -18,13 +18,12 @@ */ import { resolve, dirname, relative } from 'path'; - import { stat, rename, createReadStream, createWriteStream } from 'fs'; - +import { Readable, Writable } from 'stream'; import { fromNode } from 'bluebird'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { prioritizeMappings, readDirectory, @@ -33,12 +32,20 @@ import { createFormatArchiveStreams, } from '../lib'; -async function isDirectory(path) { +async function isDirectory(path: string): Promise { const stats = await fromNode(cb => stat(path, cb)); return stats.isDirectory(); } -export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { +export async function rebuildAllAction({ + dataDir, + log, + rootDir = dataDir, +}: { + dataDir: string; + log: ToolingLog; + rootDir?: string; +}) { const childNames = prioritizeMappings(await readDirectory(dataDir)); for (const childName of childNames) { const childPath = resolve(dataDir, childName); @@ -58,11 +65,11 @@ export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { const tempFile = childPath + (gzip ? '.rebuilding.gz' : '.rebuilding'); await createPromiseFromStreams([ - createReadStream(childPath), + createReadStream(childPath) as Readable, ...createParseArchiveStreams({ gzip }), ...createFormatArchiveStreams({ gzip }), createWriteStream(tempFile), - ]); + ] as [Readable, ...Writable[]]); await fromNode(cb => rename(tempFile, childPath, cb)); log.info(`${archiveName} Rebuilt ${childName}`); diff --git a/src/es_archiver/actions/save.js b/src/es_archiver/actions/save.ts similarity index 83% rename from src/es_archiver/actions/save.js rename to src/es_archiver/actions/save.ts index 2c264ed2ee3a9e..7a3a9dd97c0ab1 100644 --- a/src/es_archiver/actions/save.js +++ b/src/es_archiver/actions/save.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createWriteStream, mkdirSync } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; import { createListStream, createPromiseFromStreams } from '../../legacy/utils'; - import { createStats, createGenerateIndexRecordsStream, @@ -30,7 +32,21 @@ import { Progress, } from '../lib'; -export async function saveAction({ name, indices, client, dataDir, log, raw }) { +export async function saveAction({ + name, + indices, + client, + dataDir, + log, + raw, +}: { + name: string; + indices: string | string[]; + client: Client; + dataDir: string; + log: ToolingLog; + raw: boolean; +}) { const outputDir = resolve(dataDir, name); const stats = createStats(name, log); @@ -48,7 +64,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateIndexRecordsStream(client, stats), ...createFormatArchiveStreams(), createWriteStream(resolve(outputDir, 'mappings.json')), - ]), + ] as [Readable, ...Writable[]]), // export all documents from matching indexes into data.json.gz createPromiseFromStreams([ @@ -56,7 +72,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateDocRecordsStream(client, stats, progress), ...createFormatArchiveStreams({ gzip: !raw }), createWriteStream(resolve(outputDir, `data.json${raw ? '' : '.gz'}`)), - ]), + ] as [Readable, ...Writable[]]), ]); progress.deactivate(); diff --git a/src/es_archiver/actions/unload.js b/src/es_archiver/actions/unload.ts similarity index 79% rename from src/es_archiver/actions/unload.js rename to src/es_archiver/actions/unload.ts index 2acf8d2d719865..130a6b542b218a 100644 --- a/src/es_archiver/actions/unload.js +++ b/src/es_archiver/actions/unload.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { isGzip, createStats, @@ -32,7 +34,19 @@ import { createDeleteIndexStream, } from '../lib'; -export async function unloadAction({ name, client, dataDir, log, kbnClient }) { +export async function unloadAction({ + name, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); @@ -42,11 +56,11 @@ export async function unloadAction({ name, client, dataDir, log, kbnClient }) { log.info('[%s] Unloading indices from %j', name, filename); await createPromiseFromStreams([ - createReadStream(resolve(inputDir, filename)), + createReadStream(resolve(inputDir, filename)) as Readable, ...createParseArchiveStreams({ gzip: isGzip(filename) }), createFilterRecordsStream('index'), createDeleteIndexStream(client, stats, log, kibanaPluginIds), - ]); + ] as [Readable, ...Writable[]]); } return stats.toJSON(); diff --git a/src/es_archiver/cli.js b/src/es_archiver/cli.ts similarity index 90% rename from src/es_archiver/cli.js rename to src/es_archiver/cli.ts index 56d1fdca89780c..252f99f8f47afb 100644 --- a/src/es_archiver/cli.js +++ b/src/es_archiver/cli.ts @@ -17,7 +17,7 @@ * under the License. */ -/************************************************************* +/** *********************************************************** * * Run `node scripts/es_archiver --help` for usage information * @@ -27,17 +27,17 @@ import { resolve } from 'path'; import { readFileSync } from 'fs'; import { format as formatUrl } from 'url'; import readline from 'readline'; - import { Command } from 'commander'; import * as legacyElasticsearch from 'elasticsearch'; -import { EsArchiver } from './es_archiver'; import { ToolingLog } from '@kbn/dev-utils'; import { readConfigFile } from '@kbn/test'; +import { EsArchiver } from './es_archiver'; + const cmd = new Command('node scripts/es_archiver'); -const resolveConfigPath = v => resolve(process.cwd(), v); +const resolveConfigPath = (v: string) => resolve(process.cwd(), v); const defaultConfigPath = resolveConfigPath('test/functional/config.js'); cmd @@ -56,6 +56,7 @@ cmd defaultConfigPath ) .on('--help', () => { + // eslint-disable-next-line no-console console.log(readFileSync(resolve(__dirname, './cli_help.txt'), 'utf8')); }); @@ -95,10 +96,10 @@ cmd output: process.stdout, }); - await new Promise(resolve => { + await new Promise(resolveInput => { rl.question(`Press enter when you're done`, () => { rl.close(); - resolve(); + resolveInput(); }); }); }) @@ -112,12 +113,12 @@ cmd cmd.parse(process.argv); -const missingCommand = cmd.args.every(a => !(a instanceof Command)); +const missingCommand = cmd.args.every(a => !((a as any) instanceof Command)); if (missingCommand) { execute(); } -async function execute(fn) { +async function execute(fn?: (esArchiver: EsArchiver, command: Command) => void): Promise { try { const log = new ToolingLog({ level: cmd.verbose ? 'debug' : 'info', @@ -134,7 +135,7 @@ async function execute(fn) { // log and count all validation errors let errorCount = 0; - const error = msg => { + const error = (msg: string) => { errorCount++; log.error(msg); }; @@ -170,11 +171,12 @@ async function execute(fn) { dataDir: resolve(cmd.dir), kibanaUrl: cmd.kibanaUrl, }); - await fn(esArchiver, cmd); + await fn!(esArchiver, cmd); } finally { await client.close(); } } catch (err) { + // eslint-disable-next-line no-console console.log('FATAL ERROR', err.stack); } } diff --git a/src/es_archiver/es_archiver.d.ts b/src/es_archiver/es_archiver.d.ts deleted file mode 100644 index c50ae19d99cbf3..00000000000000 --- a/src/es_archiver/es_archiver.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ToolingLog } from '@kbn/dev-utils'; -import { Client } from 'elasticsearch'; -import { createStats } from './lib/stats'; - -export type JsonStats = ReturnType['toJSON']>; - -export class EsArchiver { - constructor(options: { client: Client; dataDir: string; log: ToolingLog; kibanaUrl: string }); - public save( - name: string, - indices: string | string[], - options?: { raw?: boolean } - ): Promise; - public load(name: string, options?: { skipExisting?: boolean }): Promise; - public unload(name: string): Promise; - public rebuildAll(): Promise; - public edit(prefix: string, handler: () => Promise): Promise; - public loadIfNeeded(name: string): Promise; - public emptyKibanaIndex(): Promise; -} diff --git a/src/es_archiver/es_archiver.js b/src/es_archiver/es_archiver.ts similarity index 83% rename from src/es_archiver/es_archiver.js rename to src/es_archiver/es_archiver.ts index 705706d0e58778..5614dfd842087b 100644 --- a/src/es_archiver/es_archiver.js +++ b/src/es_archiver/es_archiver.ts @@ -17,7 +17,8 @@ * under the License. */ -import { KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { saveAction, @@ -29,7 +30,22 @@ import { } from './actions'; export class EsArchiver { - constructor({ client, dataDir, log, kibanaUrl }) { + private readonly client: Client; + private readonly dataDir: string; + private readonly log: ToolingLog; + private readonly kbnClient: KbnClient; + + constructor({ + client, + dataDir, + log, + kibanaUrl, + }: { + client: Client; + dataDir: string; + log: ToolingLog; + kibanaUrl: string; + }) { this.client = client; this.dataDir = dataDir; this.log = log; @@ -46,7 +62,7 @@ export class EsArchiver { * @property {Boolean} options.raw - should the archive be raw (unzipped) or not * @return Promise */ - async save(name, indices, { raw = false } = {}) { + async save(name: string, indices: string | string[], { raw = false }: { raw?: boolean } = {}) { return await saveAction({ name, indices, @@ -66,9 +82,7 @@ export class EsArchiver { * be ignored or overwritten * @return Promise */ - async load(name, options = {}) { - const { skipExisting } = options; - + async load(name: string, { skipExisting = false }: { skipExisting?: boolean } = {}) { return await loadAction({ name, skipExisting: !!skipExisting, @@ -85,7 +99,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async unload(name) { + async unload(name: string) { return await unloadAction({ name, client: this.client, @@ -103,7 +117,6 @@ export class EsArchiver { */ async rebuildAll() { return await rebuildAllAction({ - client: this.client, dataDir: this.dataDir, log: this.log, }); @@ -117,7 +130,7 @@ export class EsArchiver { * @param {() => Promise} handler * @return Promise */ - async edit(prefix, handler) { + async edit(prefix: string, handler: () => Promise) { return await editAction({ prefix, log: this.log, @@ -132,7 +145,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async loadIfNeeded(name) { + async loadIfNeeded(name: string) { return await this.load(name, { skipExisting: true }); } diff --git a/src/es_archiver/index.d.ts b/src/es_archiver/index.ts similarity index 100% rename from src/es_archiver/index.d.ts rename to src/es_archiver/index.ts diff --git a/src/es_archiver/lib/__tests__/stats.js b/src/es_archiver/lib/__tests__/stats.ts similarity index 95% rename from src/es_archiver/lib/__tests__/stats.js rename to src/es_archiver/lib/__tests__/stats.ts index ccc24c25fb8601..28e337b3da5298 100644 --- a/src/es_archiver/lib/__tests__/stats.js +++ b/src/es_archiver/lib/__tests__/stats.ts @@ -17,26 +17,26 @@ * under the License. */ -import expect from '@kbn/expect'; import { uniq } from 'lodash'; import sinon from 'sinon'; +import expect from '@kbn/expect'; +import { ToolingLog } from '@kbn/dev-utils'; import { createStats } from '../'; -import { ToolingLog } from '@kbn/dev-utils'; -function createBufferedLog() { - const log = new ToolingLog({ +function createBufferedLog(): ToolingLog & { buffer: string } { + const log: ToolingLog = new ToolingLog({ level: 'debug', writeTo: { - write: chunk => (log.buffer += chunk), + write: chunk => ((log as any).buffer += chunk), }, }); - log.buffer = ''; - return log; + (log as any).buffer = ''; + return log as ToolingLog & { buffer: string }; } -function assertDeepClones(a, b) { - const path = []; +function assertDeepClones(a: any, b: any) { + const path: string[] = []; try { (function recurse(one, two) { if (typeof one !== 'object' || typeof two !== 'object') { diff --git a/src/es_archiver/lib/archives/__tests__/format.js b/src/es_archiver/lib/archives/__tests__/format.ts similarity index 89% rename from src/es_archiver/lib/archives/__tests__/format.js rename to src/es_archiver/lib/archives/__tests__/format.ts index 20ead30824d064..f472f094134d7b 100644 --- a/src/es_archiver/lib/archives/__tests__/format.js +++ b/src/es_archiver/lib/archives/__tests__/format.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream from 'stream'; +import Stream, { Readable, Writable } from 'stream'; import { createGunzip } from 'zlib'; import expect from '@kbn/expect'; @@ -43,11 +43,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -58,7 +58,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); @@ -73,11 +73,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([1, 2, { foo: 'bar' }, [1, 2]]), ...createFormatArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -89,7 +89,7 @@ describe('esArchiver createFormatArchiveStreams', () => { ...createFormatArchiveStreams({ gzip: true }), createGunzip(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.be(INPUT_JSON); }); }); @@ -100,7 +100,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); diff --git a/src/es_archiver/lib/archives/__tests__/parse.js b/src/es_archiver/lib/archives/__tests__/parse.ts similarity index 93% rename from src/es_archiver/lib/archives/__tests__/parse.js rename to src/es_archiver/lib/archives/__tests__/parse.ts index 2e1506e543a358..ba30156b5af393 100644 --- a/src/es_archiver/lib/archives/__tests__/parse.js +++ b/src/es_archiver/lib/archives/__tests__/parse.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream, { PassThrough, Transform } from 'stream'; +import Stream, { PassThrough, Readable, Writable, Transform } from 'stream'; import { createGzip } from 'zlib'; import expect from '@kbn/expect'; @@ -66,13 +66,13 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, 1]); }); it('provides each JSON object as soon as it is parsed', async () => { - let onReceived; + let onReceived: (resolved: any) => void; const receivedPromise = new Promise(resolve => (onReceived = resolve)); const input = new PassThrough(); const check = new Transform({ @@ -80,16 +80,16 @@ describe('esArchiver createParseArchiveStreams', () => { readableObjectMode: true, transform(chunk, env, callback) { onReceived(chunk); - callback(null, chunk); + callback(undefined, chunk); }, }); const finalPromise = createPromiseFromStreams([ - input, + input as Readable, ...createParseArchiveStreams(), check, createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); input.write(Buffer.from('{"a": 1}\n\n{"a":')); expect(await receivedPromise).to.eql({ a: 1 }); @@ -110,7 +110,7 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('Unexpected number'); @@ -149,7 +149,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, { a: 2 }]); }); @@ -161,7 +161,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([]); }); @@ -173,7 +173,7 @@ describe('esArchiver createParseArchiveStreams', () => { createListStream([Buffer.from('{"a": 1}')]), ...createParseArchiveStreams({ gzip: true }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('incorrect header check'); diff --git a/src/es_archiver/lib/archives/constants.js b/src/es_archiver/lib/archives/constants.ts similarity index 100% rename from src/es_archiver/lib/archives/constants.js rename to src/es_archiver/lib/archives/constants.ts diff --git a/src/es_archiver/lib/archives/filenames.js b/src/es_archiver/lib/archives/filenames.ts similarity index 91% rename from src/es_archiver/lib/archives/filenames.js rename to src/es_archiver/lib/archives/filenames.ts index 4ced04401d28d5..24c355edda278a 100644 --- a/src/es_archiver/lib/archives/filenames.js +++ b/src/es_archiver/lib/archives/filenames.ts @@ -19,7 +19,7 @@ import { basename, extname } from 'path'; -export function isGzip(path) { +export function isGzip(path: string) { return extname(path) === '.gz'; } @@ -28,7 +28,7 @@ export function isGzip(path) { * @param {String} path * @return {Boolean} */ -export function isMappingFile(path) { +export function isMappingFile(path: string) { return basename(path, '.gz') === 'mappings.json'; } @@ -41,7 +41,7 @@ export function isMappingFile(path) { * @param {Array} filenames * @return {Array} */ -export function prioritizeMappings(filenames) { +export function prioritizeMappings(filenames: string[]) { return filenames.slice().sort((fa, fb) => { if (isMappingFile(fa) === isMappingFile(fb)) return 0; return isMappingFile(fb) ? 1 : -1; diff --git a/src/es_archiver/lib/archives/format.js b/src/es_archiver/lib/archives/format.ts similarity index 93% rename from src/es_archiver/lib/archives/format.js rename to src/es_archiver/lib/archives/format.ts index 01fca87e7ba988..9bef4c9adbf059 100644 --- a/src/es_archiver/lib/archives/format.js +++ b/src/es_archiver/lib/archives/format.ts @@ -19,14 +19,12 @@ import { createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { PassThrough } from 'stream'; - import stringify from 'json-stable-stringify'; import { createMapStream, createIntersperseStream } from '../../../legacy/utils'; - import { RECORD_SEPARATOR } from './constants'; -export function createFormatArchiveStreams({ gzip = false } = {}) { +export function createFormatArchiveStreams({ gzip = false }: { gzip?: boolean } = {}) { return [ createMapStream(record => stringify(record, { space: ' ' })), createIntersperseStream(RECORD_SEPARATOR), diff --git a/src/es_archiver/lib/archives/index.js b/src/es_archiver/lib/archives/index.ts similarity index 99% rename from src/es_archiver/lib/archives/index.js rename to src/es_archiver/lib/archives/index.ts index 4020f52e45a35b..6aa489ea5a46de 100644 --- a/src/es_archiver/lib/archives/index.js +++ b/src/es_archiver/lib/archives/index.ts @@ -18,7 +18,5 @@ */ export { isGzip, prioritizeMappings } from './filenames'; - export { createParseArchiveStreams } from './parse'; - export { createFormatArchiveStreams } from './format'; diff --git a/src/es_archiver/lib/archives/parse.js b/src/es_archiver/lib/archives/parse.ts similarity index 91% rename from src/es_archiver/lib/archives/parse.js rename to src/es_archiver/lib/archives/parse.ts index 4fe1df72592292..0f4460c925019d 100644 --- a/src/es_archiver/lib/archives/parse.js +++ b/src/es_archiver/lib/archives/parse.ts @@ -29,7 +29,7 @@ export function createParseArchiveStreams({ gzip = false } = {}) { gzip ? createGunzip() : new PassThrough(), createReplaceStream('\r\n', '\n'), createSplitStream(RECORD_SEPARATOR), - createFilterStream(l => l.match(/[^\s]/)), - createMapStream(json => JSON.parse(json.trim())), + createFilterStream(l => !!l.match(/[^\s]/)), + createMapStream(json => JSON.parse(json.trim())), ]; } diff --git a/src/es_archiver/lib/directory.js b/src/es_archiver/lib/directory.ts similarity index 88% rename from src/es_archiver/lib/directory.js rename to src/es_archiver/lib/directory.ts index 5aee10cfea65d9..8581207fa795d1 100644 --- a/src/es_archiver/lib/directory.js +++ b/src/es_archiver/lib/directory.ts @@ -18,10 +18,9 @@ */ import { readdir } from 'fs'; - import { fromNode } from 'bluebird'; -export async function readDirectory(path) { - const allNames = await fromNode(cb => readdir(path, cb)); +export async function readDirectory(path: string) { + const allNames = await fromNode(cb => readdir(path, cb)); return allNames.filter(name => !name.startsWith('.')); } diff --git a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts index bf4aab208127f4..03599cdc9fbcfe 100644 --- a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts @@ -143,7 +143,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }, }, ]); - sinon.assert.calledTwice(stats.archivedDoc); + sinon.assert.calledTwice(stats.archivedDoc as any); expect(progress.getTotal()).to.be(2); expect(progress.getComplete()).to.be(2); }); diff --git a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts index 2535642c27cc95..35b068a6910907 100644 --- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts @@ -26,12 +26,12 @@ import { Progress } from '../../progress'; import { createIndexDocRecordsStream } from '../index_doc_records_stream'; import { createStubStats, createStubClient, createPersonDocRecords } from './stubs'; -const recordsToBulkBody = records => { +const recordsToBulkBody = (records: any[]) => { return records.reduce((acc, record) => { const { index, id, source } = record.value; return [...acc, { index: { _index: index, _id: id } }, source]; - }, []); + }, [] as any[]); }; describe('esArchiver: createIndexDocRecordsStream()', () => { diff --git a/src/es_archiver/lib/docs/__tests__/stubs.js b/src/es_archiver/lib/docs/__tests__/stubs.ts similarity index 74% rename from src/es_archiver/lib/docs/__tests__/stubs.js rename to src/es_archiver/lib/docs/__tests__/stubs.ts index 9ed48efa7d03ac..698d62e450cb46 100644 --- a/src/es_archiver/lib/docs/__tests__/stubs.js +++ b/src/es_archiver/lib/docs/__tests__/stubs.ts @@ -17,17 +17,22 @@ * under the License. */ +import { Client } from 'elasticsearch'; import sinon from 'sinon'; import Chance from 'chance'; import { times } from 'lodash'; + +import { Stats } from '../../stats'; + const chance = new Chance(); -export const createStubStats = () => ({ - indexedDoc: sinon.stub(), - archivedDoc: sinon.stub(), -}); +export const createStubStats = (): Stats => + ({ + indexedDoc: sinon.stub(), + archivedDoc: sinon.stub(), + } as any); -export const createPersonDocRecords = n => +export const createPersonDocRecords = (n: number) => times(n, () => ({ type: 'doc', value: { @@ -42,15 +47,21 @@ export const createPersonDocRecords = n => }, })); -export const createStubClient = (responses = []) => { - const createStubClientMethod = name => +type MockClient = Client & { + assertNoPendingResponses: () => void; +}; + +export const createStubClient = ( + responses: Array<(name: string, params: any) => any | Promise> = [] +): MockClient => { + const createStubClientMethod = (name: string) => sinon.spy(async params => { if (responses.length === 0) { throw new Error(`unexpected client.${name} call`); } const response = responses.shift(); - return await response(name, params); + return await response!(name, params); }); return { @@ -63,5 +74,5 @@ export const createStubClient = (responses = []) => { throw new Error(`There are ${responses.length} unsent responses.`); } }, - }; + } as any; }; diff --git a/src/es_archiver/lib/docs/generate_doc_records_stream.js b/src/es_archiver/lib/docs/generate_doc_records_stream.ts similarity index 80% rename from src/es_archiver/lib/docs/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/generate_doc_records_stream.ts index be8b0351d95c8e..e255a0abc36c5f 100644 --- a/src/es_archiver/lib/docs/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/generate_doc_records_stream.ts @@ -18,33 +18,36 @@ */ import { Transform } from 'stream'; +import { Client, SearchParams, SearchResponse } from 'elasticsearch'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; const SCROLL_SIZE = 1000; const SCROLL_TIMEOUT = '1m'; -export function createGenerateDocRecordsStream(client, stats, progress) { +export function createGenerateDocRecordsStream(client: Client, stats: Stats, progress: Progress) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(index, enc, callback) { try { - let remainingHits = null; - let resp = null; + let remainingHits = 0; + let resp: SearchResponse | null = null; while (!resp || remainingHits > 0) { if (!resp) { resp = await client.search({ - index: index, + index, scroll: SCROLL_TIMEOUT, size: SCROLL_SIZE, _source: true, - rest_total_hits_as_int: true, - }); + rest_total_hits_as_int: true, // not declared on SearchParams type + } as SearchParams); remainingHits = resp.hits.total; progress.addToTotal(remainingHits); } else { resp = await client.scroll({ - scrollId: resp._scroll_id, + scrollId: resp._scroll_id!, scroll: SCROLL_TIMEOUT, }); } @@ -68,7 +71,7 @@ export function createGenerateDocRecordsStream(client, stats, progress) { progress.addToComplete(resp.hits.hits.length); } - callback(null); + callback(undefined); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/docs/index.js b/src/es_archiver/lib/docs/index.ts similarity index 100% rename from src/es_archiver/lib/docs/index.js rename to src/es_archiver/lib/docs/index.ts diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.js b/src/es_archiver/lib/docs/index_doc_records_stream.ts similarity index 86% rename from src/es_archiver/lib/docs/index_doc_records_stream.js rename to src/es_archiver/lib/docs/index_doc_records_stream.ts index 73fb75c52ff0a6..8236ae8adb6db7 100644 --- a/src/es_archiver/lib/docs/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/index_doc_records_stream.ts @@ -17,11 +17,14 @@ * under the License. */ +import { Client } from 'elasticsearch'; import { Writable } from 'stream'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; -export function createIndexDocRecordsStream(client, stats, progress) { - async function indexDocs(docs) { - const body = []; +export function createIndexDocRecordsStream(client: Client, stats: Stats, progress: Progress) { + async function indexDocs(docs: any[]) { + const body: any[] = []; docs.forEach(doc => { stats.indexedDoc(doc.index); diff --git a/src/es_archiver/lib/index.js b/src/es_archiver/lib/index.ts similarity index 96% rename from src/es_archiver/lib/index.js rename to src/es_archiver/lib/index.ts index 246dd8169cd6b0..960d51e411859e 100644 --- a/src/es_archiver/lib/index.js +++ b/src/es_archiver/lib/index.ts @@ -30,7 +30,7 @@ export { export { createFilterRecordsStream } from './records'; -export { createStats } from './stats'; +export { createStats, Stats } from './stats'; export { isGzip, diff --git a/src/es_archiver/lib/indices/__tests__/create_index_stream.js b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts similarity index 76% rename from src/es_archiver/lib/indices/__tests__/create_index_stream.js rename to src/es_archiver/lib/indices/__tests__/create_index_stream.ts index 9e0f83c9f7eb96..c90497eded88ce 100644 --- a/src/es_archiver/lib/indices/__tests__/create_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts @@ -34,10 +34,13 @@ import { createStubIndexRecord, createStubDocRecord, createStubClient, + createStubLogger, } from './stubs'; const chance = new Chance(); +const log = createStubLogger(); + describe('esArchiver: createCreateIndexStream()', () => { describe('defaults', () => { it('deletes existing indices, creates all', async () => { @@ -48,15 +51,15 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), ]); expect(stats.getTestSummary()).to.eql({ deletedIndex: 1, createdIndex: 2, }); - sinon.assert.callCount(client.indices.delete, 1); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 1); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('deletes existing aliases, creates all', async () => { @@ -67,14 +70,19 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats, log: { debug: () => {} } }), + createCreateIndexStream({ client, stats, log }), ]); - expect(client.indices.getAlias.calledOnce).to.be.ok(); - expect(client.indices.getAlias.args[0][0]).to.eql({ name: 'existing-index', ignore: [404] }); - expect(client.indices.delete.calledOnce).to.be.ok(); - expect(client.indices.delete.args[0][0]).to.eql({ index: ['actual-index'] }); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).to.eql({ + name: 'existing-index', + ignore: [404], + }); + expect((client.indices.delete as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.delete as sinon.SinonSpy).args[0][0]).to.eql({ + index: ['actual-index'], + }); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('passes through "hit" records', async () => { @@ -86,7 +94,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubDocRecord('index', 1), createStubDocRecord('index', 2), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -101,11 +109,11 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('index', { foo: {} }), createStubDocRecord('index', 1), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); - sinon.assert.calledWith(client.indices.create, { + sinon.assert.calledWith(client.indices.create as sinon.SinonSpy, { method: 'PUT', index: 'index', body: { @@ -126,7 +134,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream([createStubIndexRecord('index'), ...randoms]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -140,7 +148,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream(nonRecordValues), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -161,6 +169,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), ]); @@ -169,9 +178,12 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing - expect(client.indices.create.args[0][0]).to.have.property('index', 'new-index'); + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing + expect((client.indices.create as sinon.SinonSpy).args[0][0]).to.have.property( + 'index', + 'new-index' + ); }); it('filters documents for skipped indices', async () => { @@ -190,6 +202,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), createConcatStream([]), @@ -199,8 +212,8 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing expect(output).to.have.length(2); expect(output).to.eql([ diff --git a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts similarity index 66% rename from src/es_archiver/lib/indices/__tests__/delete_index_stream.js rename to src/es_archiver/lib/indices/__tests__/delete_index_stream.ts index 955d1fff8779ed..1c989ba158a29d 100644 --- a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts @@ -23,7 +23,14 @@ import { createListStream, createPromiseFromStreams } from '../../../../legacy/u import { createDeleteIndexStream } from '../delete_index_stream'; -import { createStubStats, createStubClient, createStubIndexRecord } from './stubs'; +import { + createStubStats, + createStubClient, + createStubIndexRecord, + createStubLogger, +} from './stubs'; + +const log = createStubLogger(); describe('esArchiver: createDeleteIndexStream()', () => { it('deletes the index without checking if it exists', async () => { @@ -32,13 +39,13 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.notCalled(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.notCalled(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('reports the delete when the index existed', async () => { @@ -47,12 +54,12 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.calledOnce(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.calledOnce(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); }); diff --git a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js rename to src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts index 3523e9e82b153f..7a3712ca1a336f 100644 --- a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts @@ -45,10 +45,10 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { archivedIndex: 4, }); - sinon.assert.callCount(client.indices.get, 4); - sinon.assert.notCalled(client.indices.create); - sinon.assert.notCalled(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.callCount(client.indices.get as sinon.SinonSpy, 4); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('filters index metadata from settings', async () => { @@ -60,9 +60,9 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createGenerateIndexRecordsStream(client, stats), ]); - const params = client.indices.get.args[0][0]; + const params = (client.indices.get as sinon.SinonSpy).args[0][0]; expect(params).to.have.property('filterPath'); - const filters = params.filterPath; + const filters: string[] = params.filterPath; expect(filters.some(path => path.includes('index.creation_date'))).to.be(true); expect(filters.some(path => path.includes('index.uuid'))).to.be(true); expect(filters.some(path => path.includes('index.version'))).to.be(true); @@ -73,7 +73,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const stats = createStubStats(); const client = createStubClient(['index1', 'index2', 'index3']); - const indexRecords = await createPromiseFromStreams([ + const indexRecords = await createPromiseFromStreams([ createListStream(['index1', 'index2', 'index3']), createGenerateIndexRecordsStream(client, stats), createConcatStream([]), diff --git a/src/es_archiver/lib/indices/__tests__/stubs.js b/src/es_archiver/lib/indices/__tests__/stubs.js deleted file mode 100644 index 00649a06f9efef..00000000000000 --- a/src/es_archiver/lib/indices/__tests__/stubs.js +++ /dev/null @@ -1,128 +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 sinon from 'sinon'; - -export const createStubStats = () => ({ - createdIndex: sinon.stub(), - createdAliases: sinon.stub(), - deletedIndex: sinon.stub(), - skippedIndex: sinon.stub(), - archivedIndex: sinon.stub(), - getTestSummary() { - const summary = {}; - Object.keys(this).forEach(key => { - if (this[key].callCount) { - summary[key] = this[key].callCount; - } - }); - return summary; - }, -}); - -export const createStubIndexRecord = (index, aliases = {}) => ({ - type: 'index', - value: { index, aliases }, -}); - -export const createStubDocRecord = (index, id) => ({ - type: 'doc', - value: { index, id }, -}); - -const createEsClientError = errorType => { - const err = new Error(`ES Client Error Stub "${errorType}"`); - err.body = { - error: { - type: errorType, - }, - }; - return err; -}; - -const indexAlias = (aliases, index) => Object.keys(aliases).find(k => aliases[k] === index); - -export const createStubClient = (existingIndices = [], aliases = {}) => ({ - indices: { - get: sinon.spy(async ({ index }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - - return { - [index]: { - mappings: {}, - settings: {}, - }, - }; - }), - existsAlias: sinon.spy(({ name }) => { - return Promise.resolve(aliases.hasOwnProperty(name)); - }), - getAlias: sinon.spy(async ({ index, name }) => { - if (index && existingIndices.indexOf(index) >= 0) { - const result = indexAlias(aliases, index); - return { [index]: { aliases: result ? { [result]: {} } : {} } }; - } - - if (name && aliases[name]) { - return { [aliases[name]]: { aliases: { [name]: {} } } }; - } - - return { status: 404 }; - }), - updateAliases: sinon.spy(async ({ body }) => { - body.actions.forEach(({ add: { index, alias } }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - existingIndices.push({ index, alias }); - }); - - return { ok: true }; - }), - create: sinon.spy(async ({ index }) => { - if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { - throw createEsClientError('resource_already_exists_exception'); - } else { - existingIndices.push(index); - return { ok: true }; - } - }), - delete: sinon.spy(async ({ index }) => { - const indices = Array.isArray(index) ? index : [index]; - if (indices.every(ix => existingIndices.includes(ix))) { - // Delete aliases associated with our indices - indices.forEach(ix => { - const alias = Object.keys(aliases).find(k => aliases[k] === ix); - if (alias) { - delete aliases[alias]; - } - }); - indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); - return { ok: true }; - } else { - throw createEsClientError('index_not_found_exception'); - } - }), - exists: sinon.spy(async () => { - throw new Error('Do not use indices.exists(). React to errors instead.'); - }), - }, -}); diff --git a/src/es_archiver/lib/indices/__tests__/stubs.ts b/src/es_archiver/lib/indices/__tests__/stubs.ts new file mode 100644 index 00000000000000..3f4682299c38d8 --- /dev/null +++ b/src/es_archiver/lib/indices/__tests__/stubs.ts @@ -0,0 +1,154 @@ +/* + * 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 { Client } from 'elasticsearch'; +import sinon from 'sinon'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../../stats'; + +type StubStats = Stats & { + getTestSummary: () => Record; +}; + +export const createStubStats = (): StubStats => + ({ + createdIndex: sinon.stub(), + createdAliases: sinon.stub(), + deletedIndex: sinon.stub(), + skippedIndex: sinon.stub(), + archivedIndex: sinon.stub(), + getTestSummary() { + const summary: Record = {}; + Object.keys(this).forEach(key => { + if (this[key].callCount) { + summary[key] = this[key].callCount; + } + }); + return summary; + }, + } as any); + +export const createStubLogger = (): ToolingLog => + ({ + debug: sinon.stub(), + info: sinon.stub(), + success: sinon.stub(), + warning: sinon.stub(), + error: sinon.stub(), + } as any); + +export const createStubIndexRecord = (index: string, aliases = {}) => ({ + type: 'index', + value: { index, aliases }, +}); + +export const createStubDocRecord = (index: string, id: number) => ({ + type: 'doc', + value: { index, id }, +}); + +const createEsClientError = (errorType: string) => { + const err = new Error(`ES Client Error Stub "${errorType}"`); + (err as any).body = { + error: { + type: errorType, + }, + }; + return err; +}; + +const indexAlias = (aliases: Record, index: string) => + Object.keys(aliases).find(k => aliases[k] === index); + +type StubClient = Client; + +export const createStubClient = ( + existingIndices: string[] = [], + aliases: Record = {} +): StubClient => + ({ + indices: { + get: sinon.spy(async ({ index }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + + return { + [index]: { + mappings: {}, + settings: {}, + }, + }; + }), + existsAlias: sinon.spy(({ name }) => { + return Promise.resolve(aliases.hasOwnProperty(name)); + }), + getAlias: sinon.spy(async ({ index, name }) => { + if (index && existingIndices.indexOf(index) >= 0) { + const result = indexAlias(aliases, index); + return { [index]: { aliases: result ? { [result]: {} } : {} } }; + } + + if (name && aliases[name]) { + return { [aliases[name]]: { aliases: { [name]: {} } } }; + } + + return { status: 404 }; + }), + updateAliases: sinon.spy(async ({ body }) => { + body.actions.forEach( + ({ add: { index, alias } }: { add: { index: string; alias: string } }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + existingIndices.push({ index, alias } as any); + } + ); + + return { ok: true }; + }), + create: sinon.spy(async ({ index }) => { + if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { + throw createEsClientError('resource_already_exists_exception'); + } else { + existingIndices.push(index); + return { ok: true }; + } + }), + delete: sinon.spy(async ({ index }) => { + const indices = Array.isArray(index) ? index : [index]; + if (indices.every(ix => existingIndices.includes(ix))) { + // Delete aliases associated with our indices + indices.forEach(ix => { + const alias = Object.keys(aliases).find(k => aliases[k] === ix); + if (alias) { + delete aliases[alias]; + } + }); + indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); + return { ok: true }; + } else { + throw createEsClientError('index_not_found_exception'); + } + }), + exists: sinon.spy(async () => { + throw new Error('Do not use indices.exists(). React to errors instead.'); + }), + }, + } as any); diff --git a/src/es_archiver/lib/indices/create_index_stream.js b/src/es_archiver/lib/indices/create_index_stream.ts similarity index 81% rename from src/es_archiver/lib/indices/create_index_stream.js rename to src/es_archiver/lib/indices/create_index_stream.ts index 8fe4bc568cd231..df9d3bb623ad65 100644 --- a/src/es_archiver/lib/indices/create_index_stream.js +++ b/src/es_archiver/lib/indices/create_index_stream.ts @@ -17,13 +17,36 @@ * under the License. */ -import { Transform } from 'stream'; - +import { Transform, Readable } from 'stream'; import { get, once } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { Stats } from '../stats'; import { deleteKibanaIndices } from './kibana_index'; import { deleteIndex } from './delete_index'; -export function createCreateIndexStream({ client, stats, skipExisting, log }) { +interface DocRecord { + value: { + index: string; + type: string; + settings: Record; + mappings: Record; + aliases: Record; + }; +} + +export function createCreateIndexStream({ + client, + stats, + skipExisting = false, + log, +}: { + client: Client; + stats: Stats; + skipExisting?: boolean; + log: ToolingLog; +}) { const skipDocsFromIndices = new Set(); // If we're trying to import Kibana index docs, we need to ensure that @@ -31,7 +54,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { // migrations. This only needs to be done once per archive load operation. const deleteKibanaIndicesOnce = once(deleteKibanaIndices); - async function handleDoc(stream, record) { + async function handleDoc(stream: Readable, record: DocRecord) { if (skipDocsFromIndices.has(record.value.index)) { return; } @@ -39,7 +62,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { stream.push(record); } - async function handleIndex(record) { + async function handleIndex(record: DocRecord) { const { index, settings, mappings, aliases } = record.value; const isKibana = index.startsWith('.kibana'); @@ -102,7 +125,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { break; } - callback(null); + callback(); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/indices/delete_index.js b/src/es_archiver/lib/indices/delete_index.ts similarity index 76% rename from src/es_archiver/lib/indices/delete_index.js rename to src/es_archiver/lib/indices/delete_index.ts index 6f60d9533a36b3..e3fca587fbc3d3 100644 --- a/src/es_archiver/lib/indices/delete_index.js +++ b/src/es_archiver/lib/indices/delete_index.ts @@ -18,22 +18,34 @@ */ import { get } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; // see https://github.com/elastic/elasticsearch/blob/99f88f15c5febbca2d13b5b5fda27b844153bf1a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java#L313-L319 const PENDING_SNAPSHOT_STATUSES = ['INIT', 'STARTED', 'WAITING']; -export async function deleteIndex(options) { +export async function deleteIndex(options: { + client: Client; + stats: Stats; + index: string; + log: ToolingLog; + retryIfSnapshottingCount?: number; +}): Promise { const { client, stats, index, log, retryIfSnapshottingCount = 10 } = options; const getIndicesToDelete = async () => { const aliasInfo = await client.indices.getAlias({ name: index, ignore: [404] }); - return aliasInfo.status === 404 ? index : Object.keys(aliasInfo); + return aliasInfo.status === 404 ? [index] : Object.keys(aliasInfo); }; try { const indicesToDelete = await getIndicesToDelete(); await client.indices.delete({ index: indicesToDelete }); - stats.deletedIndex(indicesToDelete); + for (let i = 0; i < indicesToDelete.length; i++) { + const indexToDelete = indicesToDelete[i]; + stats.deletedIndex(indexToDelete); + } } catch (error) { if (retryIfSnapshottingCount > 0 && isDeleteWhileSnapshotInProgressError(error)) { stats.waitingForInProgressSnapshot(index); @@ -56,7 +68,7 @@ export async function deleteIndex(options) { * @param {Error} error * @return {Boolean} */ -export function isDeleteWhileSnapshotInProgressError(error) { +export function isDeleteWhileSnapshotInProgressError(error: object) { return get(error, 'body.error.reason', '').startsWith( 'Cannot delete indices that are being snapshotted' ); @@ -65,13 +77,9 @@ export function isDeleteWhileSnapshotInProgressError(error) { /** * Wait for the any snapshot in any repository that is * snapshotting this index to complete. - * - * @param {EsClient} client - * @param {string} index the name of the index to look for - * @return {Promise} */ -export async function waitForSnapshotCompletion(client, index, log) { - const isSnapshotPending = async (repository, snapshot) => { +export async function waitForSnapshotCompletion(client: Client, index: string, log: ToolingLog) { + const isSnapshotPending = async (repository: string, snapshot: string) => { const { snapshots: [status], } = await client.snapshot.status({ @@ -83,7 +91,7 @@ export async function waitForSnapshotCompletion(client, index, log) { return PENDING_SNAPSHOT_STATUSES.includes(status.state); }; - const getInProgressSnapshots = async repository => { + const getInProgressSnapshots = async (repository: string) => { const { snapshots: inProgressSnapshots } = await client.snapshot.get({ repository, snapshot: '_current', @@ -91,9 +99,9 @@ export async function waitForSnapshotCompletion(client, index, log) { return inProgressSnapshots; }; - for (const repository of Object.keys(await client.snapshot.getRepository())) { + for (const repository of Object.keys(await client.snapshot.getRepository({} as any))) { const allInProgress = await getInProgressSnapshots(repository); - const found = allInProgress.find(s => s.indices.includes(index)); + const found = allInProgress.find((s: any) => s.indices.includes(index)); if (!found) { continue; diff --git a/src/es_archiver/lib/indices/delete_index_stream.js b/src/es_archiver/lib/indices/delete_index_stream.ts similarity index 86% rename from src/es_archiver/lib/indices/delete_index_stream.js rename to src/es_archiver/lib/indices/delete_index_stream.ts index 31a49ed30a124d..b4e1e530e1f84e 100644 --- a/src/es_archiver/lib/indices/delete_index_stream.js +++ b/src/es_archiver/lib/indices/delete_index_stream.ts @@ -18,11 +18,19 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; import { cleanKibanaIndices } from './kibana_index'; -export function createDeleteIndexStream(client, stats, log, kibanaPluginIds) { +export function createDeleteIndexStream( + client: Client, + stats: Stats, + log: ToolingLog, + kibanaPluginIds: string[] +) { return new Transform({ readableObjectMode: true, writableObjectMode: true, diff --git a/src/es_archiver/lib/indices/generate_index_records_stream.js b/src/es_archiver/lib/indices/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/generate_index_records_stream.js rename to src/es_archiver/lib/indices/generate_index_records_stream.ts index 1d1a44aa634c24..b4b98f8ae262c7 100644 --- a/src/es_archiver/lib/indices/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/generate_index_records_stream.ts @@ -18,14 +18,16 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { Stats } from '../stats'; -export function createGenerateIndexRecordsStream(client, stats) { +export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(indexOrAlias, enc, callback) { try { - const resp = await client.indices.get({ + const resp = (await client.indices.get({ index: indexOrAlias, filterPath: [ '*.settings', @@ -36,7 +38,7 @@ export function createGenerateIndexRecordsStream(client, stats) { '-*.settings.index.version', '-*.settings.index.provided_name', ], - }); + })) as Record; for (const [index, { settings, mappings }] of Object.entries(resp)) { const { diff --git a/src/es_archiver/lib/indices/index.js b/src/es_archiver/lib/indices/index.ts similarity index 100% rename from src/es_archiver/lib/indices/index.js rename to src/es_archiver/lib/indices/index.ts diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.ts similarity index 70% rename from src/es_archiver/lib/indices/kibana_index.js rename to src/es_archiver/lib/indices/kibana_index.ts index 744132bdcef69e..de67ba7c4e31ec 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.ts @@ -17,29 +17,34 @@ * under the License. */ -import _ from 'lodash'; +import { get } from 'lodash'; import fs from 'fs'; -import path from 'path'; +import Path from 'path'; import { promisify } from 'util'; import { toArray } from 'rxjs/operators'; +import { Client, CreateDocumentParams } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; -import { collectUiExports } from '../../../legacy/ui/ui_exports'; import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; import { SavedObjectsSchema } from '../../../core/server/saved_objects'; +// @ts-ignore +import { collectUiExports } from '../../../legacy/ui/ui_exports'; +// @ts-ignore import { findPluginSpecs } from '../../../legacy/plugin_discovery'; /** * Load the uiExports for a Kibana instance, only load uiExports from xpack if * it is enabled in the Kibana server. */ -const getUiExports = async kibanaPluginIds => { +const getUiExports = async (kibanaPluginIds: string[]) => { const xpackEnabled = kibanaPluginIds.includes('xpack_main'); const { spec$ } = await findPluginSpecs({ plugins: { - scanDirs: [path.resolve(__dirname, '../../../legacy/core_plugins')], - paths: xpackEnabled ? [path.resolve(__dirname, '../../../../x-pack')] : [], + scanDirs: [Path.resolve(__dirname, '../../../legacy/core_plugins')], + paths: xpackEnabled ? [Path.resolve(__dirname, '../../../../x-pack')] : [], }, }); @@ -50,7 +55,15 @@ const getUiExports = async kibanaPluginIds => { /** * Deletes all indices that start with `.kibana` */ -export async function deleteKibanaIndices({ client, stats, log }) { +export async function deleteKibanaIndices({ + client, + stats, + log, +}: { + client: Client; + stats: Stats; + log: ToolingLog; +}) { const indexNames = await fetchKibanaIndices(client); if (!indexNames.length) { return; @@ -76,37 +89,52 @@ export async function deleteKibanaIndices({ client, stats, log }) { * builds up an object that implements just enough of the kbnMigrations interface * as is required by migrations. */ -export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { +export async function migrateKibanaIndex({ + client, + log, + kibanaPluginIds, +}: { + client: Client; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { const uiExports = await getUiExports(kibanaPluginIds); const kibanaVersion = await loadKibanaVersion(); - const config = { + const config: Record = { 'xpack.task_manager.index': '.kibana_task_manager', }; + const logger = { + trace: log.verbose.bind(log), + debug: log.debug.bind(log), + info: log.info.bind(log), + warn: log.warning.bind(log), + error: log.error.bind(log), + fatal: log.error.bind(log), + log: (entry: any) => log.info(entry.message), + get: () => logger, + }; + const migratorOptions = { - config: { get: path => config[path] }, + config: { get: (path: string) => config[path] } as any, savedObjectsConfig: { scrollDuration: '5m', batchSize: 100, pollInterval: 100, + skip: false, }, kibanaConfig: { index: '.kibana', - }, - logger: { - trace: log.verbose.bind(log), - debug: log.debug.bind(log), - info: log.info.bind(log), - warn: log.warning.bind(log), - error: log.error.bind(log), - }, - version: kibanaVersion, + } as any, + logger, + kibanaVersion, savedObjectSchemas: new SavedObjectsSchema(uiExports.savedObjectSchemas), savedObjectMappings: uiExports.savedObjectMappings, savedObjectMigrations: uiExports.savedObjectMigrations, savedObjectValidations: uiExports.savedObjectValidations, - callCluster: (path, ...args) => _.get(client, path).call(client, ...args), + callCluster: (path: string, ...args: any[]) => + (get(client, path) as Function).call(client, ...args), }; return await new KibanaMigrator(migratorOptions).runMigrations(); @@ -114,8 +142,8 @@ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { async function loadKibanaVersion() { const readFile = promisify(fs.readFile); - const packageJson = await readFile(path.join(__dirname, '../../../../package.json')); - return JSON.parse(packageJson).version; + const packageJson = await readFile(Path.join(__dirname, '../../../../package.json')); + return JSON.parse(packageJson.toString('utf-8')).version; } /** @@ -123,16 +151,24 @@ async function loadKibanaVersion() { * .kibana, .kibana_1, .kibana_323, etc. This finds all indices starting * with .kibana, then filters out any that aren't actually Kibana's core * index (e.g. we don't want to remove .kibana_task_manager or the like). - * - * @param {string} index */ -async function fetchKibanaIndices(client) { +async function fetchKibanaIndices(client: Client) { const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = index => /^\.kibana(:?_\d*)?$/.test(index); - return kibanaIndices.map(x => x.index).filter(isKibanaIndex); + const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + return kibanaIndices.map((x: { index: string }) => x.index).filter(isKibanaIndex); } -export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds }) { +export async function cleanKibanaIndices({ + client, + stats, + log, + kibanaPluginIds, +}: { + client: Client; + stats: Stats; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { if (!kibanaPluginIds.includes('spaces')) { return await deleteKibanaIndices({ client, @@ -178,7 +214,7 @@ export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds } stats.deletedIndex('.kibana'); } -export async function createDefaultSpace({ index, client }) { +export async function createDefaultSpace({ index, client }: { index: string; client: Client }) { await client.create({ index, id: 'space:default', @@ -193,5 +229,5 @@ export async function createDefaultSpace({ index, client }) { _reserved: true, }, }, - }); + } as CreateDocumentParams); } diff --git a/src/es_archiver/lib/records/__tests__/filter_records_stream.js b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts similarity index 97% rename from src/es_archiver/lib/records/__tests__/filter_records_stream.js rename to src/es_archiver/lib/records/__tests__/filter_records_stream.ts index fd35575ca59bae..d5830478decbae 100644 --- a/src/es_archiver/lib/records/__tests__/filter_records_stream.js +++ b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts @@ -51,7 +51,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { it('produces record values that have a matching type', async () => { const type1 = chance.word({ length: 5 }); - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([ { type: type1, value: {} }, { type: type1, value: {} }, diff --git a/src/es_archiver/lib/records/filter_records_stream.js b/src/es_archiver/lib/records/filter_records_stream.ts similarity index 91% rename from src/es_archiver/lib/records/filter_records_stream.js rename to src/es_archiver/lib/records/filter_records_stream.ts index 5a835ffe8e84d5..191cbd3b921e3b 100644 --- a/src/es_archiver/lib/records/filter_records_stream.js +++ b/src/es_archiver/lib/records/filter_records_stream.ts @@ -19,14 +19,14 @@ import { Transform } from 'stream'; -export function createFilterRecordsStream(type) { +export function createFilterRecordsStream(type: string) { return new Transform({ writableObjectMode: true, readableObjectMode: true, transform(record, enc, callback) { if (record && record.type === type) { - callback(null, record); + callback(undefined, record); } else { callback(); } diff --git a/src/es_archiver/lib/records/index.js b/src/es_archiver/lib/records/index.ts similarity index 100% rename from src/es_archiver/lib/records/index.js rename to src/es_archiver/lib/records/index.ts diff --git a/src/es_archiver/lib/stats.ts b/src/es_archiver/lib/stats.ts index 5f73304abf9a8b..c69b764fc72908 100644 --- a/src/es_archiver/lib/stats.ts +++ b/src/es_archiver/lib/stats.ts @@ -37,6 +37,8 @@ export interface IndexStats { }; } +export type Stats = ReturnType; + export function createStats(name: string, log: ToolingLog) { const info = (msg: string, ...args: any[]) => log.info(`[${name}] ${msg}`, ...args); const debug = (msg: string, ...args: any[]) => log.debug(`[${name}] ${msg}`, ...args); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js similarity index 81% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.js index e0854205b132ea..67711bd4599a2d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js @@ -19,9 +19,10 @@ import _ from 'lodash'; import moment from 'moment'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; -export function onBrushEvent(event) { +export async function onBrushEvent(event, getIndexPatterns) { const isNumber = event.data.ordered; const isDate = isNumber && event.data.ordered.date; @@ -29,9 +30,12 @@ export function onBrushEvent(event) { if (!xRaw) return []; const column = xRaw.table.columns[xRaw.column]; if (!column) return []; - const aggConfig = event.aggConfigs[xRaw.column]; - if (!aggConfig) return []; - const indexPattern = aggConfig.getIndexPattern(); + if (!column.meta) return []; + const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); + const aggConfig = deserializeAggConfig({ + ...column.meta, + indexPattern, + }); const field = aggConfig.params.field; if (!field) return []; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js similarity index 71% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js index 215d440edd9d02..a6fe58503cd021 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js @@ -20,21 +20,36 @@ import _ from 'lodash'; import moment from 'moment'; import expect from '@kbn/expect'; + +jest.mock('../../../../../ui/public/agg_types/agg_configs', () => ({ + AggConfigs: function AggConfigs() { + return { + createAggConfig: ({ params }) => ({ + params, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }), + }; + }, +})); + import { onBrushEvent } from './brush_event'; describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; const JAN_01_2014 = 1388559600000; + const aggConfigs = [ + { + params: {}, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }, + ]; + const baseEvent = { - aggConfigs: [ - { - params: {}, - getIndexPattern: () => ({ - timeFieldName: 'time', - }), - }, - ], data: { fieldFormatter: _.constant({}), series: [ @@ -47,6 +62,11 @@ describe('brushEvent', () => { columns: [ { id: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, }, ], }, @@ -69,9 +89,11 @@ describe('brushEvent', () => { expect(onBrushEvent).to.be.a(Function); }); - test('ignores event when data.xAxisField not provided', () => { + test('ignores event when data.xAxisField not provided', async () => { const event = _.cloneDeep(baseEvent); - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => baseEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); @@ -84,22 +106,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('by ignoring the event when range spans zero time', () => { + test('by ignoring the event when range spans zero time', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by updating the timefilter', () => { + test('by updating the timefilter', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: async () => dateEvent.data.indexPattern, + })); expect(filters[0].range.time.gte).to.be(new Date(JAN_01_2014).toISOString()); // Set to a baseline timezone for comparison. expect(filters[0].range.time.lt).to.be(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); @@ -114,17 +140,19 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('creates a new range filter', () => { + test('creates a new range filter', async () => { const event = _.cloneDeep(dateEvent); const rangeBegin = JAN_01_2014; const rangeEnd = rangeBegin + DAY_IN_MS; event.range = [rangeBegin, rangeEnd]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.anotherTimeField.gte).to.equal(moment(rangeBegin).toISOString()); expect(filters[0].range.anotherTimeField.lt).to.equal(moment(rangeEnd).toISOString()); @@ -142,22 +170,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = numberField; numberEvent = _.cloneDeep(baseEvent); - numberEvent.aggConfigs[0].params.field = numberField; numberEvent.data.ordered = { date: false }; }); - test('by ignoring the event when range does not span at least 2 values', () => { + test('by ignoring the event when range does not span at least 2 values', async () => { const event = _.cloneDeep(numberEvent); event.range = [1]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by creating a new filter', () => { + test('by creating a new filter', async () => { const event = _.cloneDeep(numberEvent); event.range = [1, 2, 3, 4]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.numberField.gte).to.equal(1); expect(filters[0].range.numberField.lt).to.equal(4); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts similarity index 92% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts index f0de2f88dcb82c..2cecfd0fe8b767 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.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/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js similarity index 72% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js rename to src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js index 303dec690e62bb..1037c718d0003f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js @@ -17,8 +17,10 @@ * under the License. */ -import { onBrushEvent } from './brush_event'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../../plugins/data/public/services'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -63,11 +65,16 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { * @param {string} cellValue - value of the current cell * @return {array|string} - filter or list of filters to provide to queryFilter.addFilters() */ -const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { +const createFilter = async (table, columnIndex, rowIndex) => { + if (!table || !table.columns || !table.columns[columnIndex]) return; const column = table.columns[columnIndex]; - const aggConfig = aggConfigs[columnIndex]; + const aggConfig = deserializeAggConfig({ + type: column.meta.type, + aggConfigParams: column.meta.aggConfigParams, + indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), + }); let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -85,26 +92,28 @@ const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { return filter; }; -const createFiltersFromEvent = event => { +const createFiltersFromEvent = async event => { const filters = []; const dataPoints = event.data || [event]; - dataPoints - .filter(point => point) - .forEach(val => { - const { table, column, row, value } = val; - const filter = createFilter(event.aggConfigs, table, column, row, value); - if (filter) { - filter.forEach(f => { - if (event.negate) { - f = esFilters.toggleFilterNegated(f); - } - filters.push(f); - }); - } - }); + await Promise.all( + dataPoints + .filter(point => point) + .map(async val => { + const { table, column, row } = val; + const filter = await createFilter(table, column, row); + if (filter) { + filter.forEach(f => { + if (event.negate) { + f = esFilters.toggleFilterNegated(f); + } + filters.push(f); + }); + } + }) + ); return filters; }; -export { createFilter, createFiltersFromEvent, onBrushEvent }; +export { createFilter, createFiltersFromEvent }; diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts new file mode 100644 index 00000000000000..4ea5c78a9fd2b4 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -0,0 +1,91 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// @ts-ignore +import { onBrushEvent } from './filters/brush_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../plugins/data/public/services'; + +export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await onBrushEvent(context.data, getIndexPatterns)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function selectRangeAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: SELECT_RANGE_ACTION, + id: SELECT_RANGE_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await onBrushEvent(data, getIndexPatterns)) || []; + + const selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts new file mode 100644 index 00000000000000..2f622eb1eb6695 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -0,0 +1,126 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getOverlays, getIndexPatterns } from '../../../../../plugins/data/public/services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { applyFiltersPopover } from '../../../../../plugins/data/public/ui/apply_filters'; +// @ts-ignore +import { createFiltersFromEvent } from './filters/create_filters_from_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; + +export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await createFiltersFromEvent(context.data)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function valueClickAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: VALUE_CLICK_ACTION, + id: VALUE_CLICK_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await createFiltersFromEvent(data)) || []; + + let selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (selectedFilters.length > 1) { + const indexPatterns = await Promise.all( + filters.map(filter => { + return getIndexPatterns().get(filter.meta.index!); + }) + ); + + const filterSelectionPromise: Promise = new Promise(resolve => { + const overlay = getOverlays().openModal( + toMountPoint( + applyFiltersPopover( + filters, + indexPatterns, + () => { + overlay.close(); + resolve([]); + }, + (filterSelection: esFilters.Filter[]) => { + overlay.close(); + resolve(filterSelection); + } + ) + ), + { + 'data-test-subj': 'selectFilterOverlay', + } + ); + }); + + selectedFilters = await filterSelectionPromise; + } + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts index a6646ea338c93c..d37c17c2240727 100644 --- a/src/legacy/core_plugins/data/public/legacy.ts +++ b/src/legacy/core_plugins/data/public/legacy.ts @@ -39,8 +39,6 @@ import { plugin } from '.'; const dataPlugin = plugin(); -export const setup = dataPlugin.setup(npSetup.core); +export const setup = dataPlugin.setup(npSetup.core, npSetup.plugins); -export const start = dataPlugin.start(npStart.core, { - data: npStart.plugins.data, -}); +export const start = dataPlugin.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 6bd85ef020f167..da35366cdff318 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -22,6 +22,7 @@ import { DataPublicPluginStart, addSearchStrategy, defaultSearchStrategy, + DataPublicPluginSetup, } from '../../../../plugins/data/public'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; @@ -32,15 +33,27 @@ import { setInjectedMetadata, setFieldFormats, setSearchService, + setOverlays, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; +import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; +import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../plugins/embeddable/public/lib/triggers'; +import { IUiActionsSetup, IUiActionsStart } from '../../../../plugins/ui_actions/public'; export interface DataPluginSetupDependencies { + data: DataPublicPluginSetup; expressions: ExpressionsSetup; + uiActions: IUiActionsSetup; } export interface DataPluginStartDependencies { data: DataPublicPluginStart; + uiActions: IUiActionsStart; } /** @@ -64,19 +77,30 @@ export interface DataStart {} // eslint-disable-line @typescript-eslint/no-empty export class DataPlugin implements Plugin { - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { data, uiActions }: DataPluginSetupDependencies) { setInjectedMetadata(core.injectedMetadata); // This is to be deprecated once we switch to the new search service fully addSearchStrategy(defaultSearchStrategy); + + uiActions.registerAction( + selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) + ); + uiActions.registerAction( + valueClickAction(data.query.filterManager, data.query.timefilter.timefilter) + ); } - public start(core: CoreStart, { data }: DataPluginStartDependencies): DataStart { + public start(core: CoreStart, { data, uiActions }: DataPluginStartDependencies): DataStart { setUiSettings(core.uiSettings); setQueryService(data.query); setIndexPatterns(data.indexPatterns); setFieldFormats(data.fieldFormats); setSearchService(data.search); + setOverlays(core.overlays); + + uiActions.attachAction(SELECT_RANGE_TRIGGER, SELECT_RANGE_ACTION); + uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); return {}; } diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index 6e6d2a15fa2ac8..8f7953c408a971 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -19,9 +19,9 @@ import { set } from 'lodash'; // @ts-ignore -import { createFilter } from '../../../../visualizations/public'; import { FormattedData } from '../../../../../../plugins/inspector/public'; - +// @ts-ignore +import { createFilter } from './create_filter'; interface Column { id: string; name: string; diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js new file mode 100644 index 00000000000000..3f4028a9b5525b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js @@ -0,0 +1,66 @@ +/* + * 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 getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { + if (rowIndex === -1) { + return []; + } + + // get only rows where cell value matches current row for all the fields before columnIndex + const rows = table.rows.filter(row => { + return table.columns.every((column, i) => { + return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; + }); + }); + const terms = rows.map(row => row[table.columns[columnIndex].id]); + + return [ + ...new Set( + terms.filter(term => { + const notOther = term !== '__other__'; + const notMissing = term !== '__missing__'; + return notOther && notMissing; + }) + ), + ]; +}; + +const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { + const column = table.columns[columnIndex]; + const aggConfig = aggConfigs[columnIndex]; + let filter = []; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + if (value === null || value === undefined || !aggConfig.isFilterable()) { + return; + } + if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { + const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); + filter = aggConfig.createFilter(value, { terms }); + } else { + filter = aggConfig.createFilter(value); + } + + if (!Array.isArray(filter)) { + filter = [filter]; + } + + return filter; +}; + +export { createFilter }; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 143283152d1044..b4ea2cd378d61b 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -46,6 +46,7 @@ import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getQueryService, getIndexPatterns } from '../../../../../../plugins/data/public/services'; import { getRequestInspectorStats, getResponseInspectorStats } from '../..'; +import { serializeAggConfig } from './utils'; export interface RequestHandlerParams { searchSource: ISearchSource; @@ -289,6 +290,7 @@ export const esaggs = (): ExpressionFunction { + return { + type: aggConfig.type.name, + indexPatternId: aggConfig.getIndexPattern().id, + aggConfigParams: aggConfig.toJSON().params, + }; +}; + +interface DeserializeAggConfigParams { + type: string; + aggConfigParams: Record; + indexPattern: IndexPattern; +} + +export const deserializeAggConfig = ({ + type, + aggConfigParams, + indexPattern, +}: DeserializeAggConfigParams) => { + const aggConfigs = new AggConfigs(indexPattern); + const aggConfig = aggConfigs.createAggConfig({ + enabled: true, + type, + params: aggConfigParams, + }); + return aggConfig; +}; diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index e1c93ec0e3b1c4..c975d5772e0a8b 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -18,3 +18,4 @@ */ export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; +export { serializeAggConfig } from './expressions/utils'; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8e0497732e2305..8c35044b52c9e1 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -42,9 +42,7 @@ export default function(kibana) { config: function(Joi) { return Joi.object({ enabled: Joi.boolean().default(true), - defaultAppId: Joi.string().default('home'), index: Joi.string().default('.kibana'), - disableWelcomeScreen: Joi.boolean().default(false), autocompleteTerminateAfter: Joi.number() .integer() .min(1) diff --git a/src/legacy/core_plugins/kibana/inject_vars.js b/src/legacy/core_plugins/kibana/inject_vars.js index 4bf11f28732ee1..01623341e4d38b 100644 --- a/src/legacy/core_plugins/kibana/inject_vars.js +++ b/src/legacy/core_plugins/kibana/inject_vars.js @@ -28,8 +28,6 @@ export function injectVars(server) { ); return { - kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'), - disableWelcomeScreen: serverConfig.get('kibana.disableWelcomeScreen'), importAndExportableTypes, autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'), autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'), diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts index 0d461028d994a3..f1fd93fd09b3dd 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/application.ts @@ -46,6 +46,7 @@ import { IEmbeddableStart } from '../../../../../../plugins/embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../plugins/navigation/public'; import { DataPublicPluginStart as NpDataStart } from '../../../../../../plugins/data/public'; import { SharePluginStart } from '../../../../../../plugins/share/public'; +import { KibanaLegacyStart } from '../../../../../../plugins/kibana_legacy/public'; export interface RenderDeps { core: LegacyCoreStart; @@ -62,6 +63,7 @@ export interface RenderDeps { embeddables: IEmbeddableStart; localStorage: Storage; share: SharePluginStart; + config: KibanaLegacyStart['config']; } let angularModuleInstance: IModule | null = null; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index a48c165116304f..0537e3f8fc456d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -88,7 +88,6 @@ export interface DashboardAppScope extends ng.IScope { export function initDashboardAppDirective(app: any, deps: RenderDeps) { app.directive('dashboardApp', function($injector: IInjector) { const confirmModal = $injector.get('confirmModal'); - const config = deps.uiSettings; return { restrict: 'E', @@ -106,7 +105,6 @@ export function initDashboardAppDirective(app: any, deps: RenderDeps) { $route, $scope, $routeParams, - config, confirmModal, indexPatterns: deps.npDataStart.indexPatterns, kbnUrlStateStorage, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index a8eec9c2504a7c..624be02ac3b9da 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -90,7 +90,6 @@ export interface DashboardAppControllerDependencies extends RenderDeps { $routeParams: any; indexPatterns: IndexPatternsContract; dashboardConfig: any; - config: any; confirmModal: ConfirmModalFn; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; @@ -109,7 +108,6 @@ export class DashboardAppController { dashboardConfig, localStorage, indexPatterns, - config, confirmModal, savedQueryService, embeddables, @@ -376,7 +374,7 @@ export class DashboardAppController { dashboardStateManager.getQuery() || { query: '', language: - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), }, queryFilter.getFilters() ); @@ -493,7 +491,7 @@ export class DashboardAppController { { query: '', language: - localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'), + localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), }, queryFilter.getGlobalFilters() ); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js index abc0c789326f8a..b0f70b7a0c68f3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/legacy_app.js @@ -246,10 +246,10 @@ export function initDashboardApp(app, deps) { }, }) .when(`dashboard/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }) .when(`dashboards/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }); }); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap index e76f65c45e4286..f5a00e5435ed6c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/top_nav/__snapshots__/clone_modal.test.js.snap @@ -5,7 +5,6 @@ exports[`renders DashboardCloneModal 1`] = ` diff --git a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts index ca4b18a37504c6..227bcb53ca0df7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/plugin.ts @@ -107,6 +107,7 @@ export class DashboardPlugin implements Plugin { chrome: contextCore.chrome, addBasePath: contextCore.http.basePath.prepend, uiSettings: contextCore.uiSettings, + config: kibana_legacy.config, savedQueryService: npDataStart.query.savedQueries, embeddables, dashboardCapabilities: contextCore.application.capabilities.dashboard, diff --git a/src/legacy/ui/public/directives/__tests__/css_truncate.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js similarity index 92% rename from src/legacy/ui/public/directives/__tests__/css_truncate.js rename to src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js index bf102f5a29fdb9..8dea9c61475dbe 100644 --- a/src/legacy/ui/public/directives/__tests__/css_truncate.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/css_truncate.js @@ -20,7 +20,7 @@ import angular from 'angular'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import 'plugins/kibana/discover/legacy'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; let $parentScope; @@ -30,7 +30,9 @@ let $elem; const init = function(expandable) { // Load the application - ngMock.module('kibana'); + pluginInstance.initializeServices(); + pluginInstance.initializeInnerAngular(); + ngMock.module('app/discover'); // Create the scope ngMock.inject(function($rootScope, $compile) { diff --git a/src/legacy/ui/public/directives/__tests__/fixed_scroll.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/fixed_scroll.js similarity index 96% rename from src/legacy/ui/public/directives/__tests__/fixed_scroll.js rename to src/legacy/core_plugins/kibana/public/discover/__tests__/directives/fixed_scroll.js index b35836967c3f38..49a0df54079ea5 100644 --- a/src/legacy/ui/public/directives/__tests__/fixed_scroll.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/fixed_scroll.js @@ -18,8 +18,8 @@ */ import expect from '@kbn/expect'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import ngMock from 'ng_mock'; -import '../../fixed_scroll'; import $ from 'jquery'; import sinon from 'sinon'; @@ -29,8 +29,9 @@ describe('FixedScroll directive', function() { let compile; let flushPendingTasks; const trash = []; - - beforeEach(ngMock.module('kibana')); + beforeEach(() => pluginInstance.initializeServices()); + beforeEach(() => pluginInstance.initializeInnerAngular()); + beforeEach(ngMock.module('app/discover')); beforeEach( ngMock.inject(function($compile, $rootScope, $timeout) { flushPendingTasks = function flushPendingTasks() { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/get_sort.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/get_sort.js deleted file mode 100644 index d5485bca33cf50..00000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/get_sort.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; - -import { getSort } from '../../../np_ready/angular/doc_table/lib/get_sort'; -import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; - -let indexPattern; - -describe('docTable', function() { - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(function(Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }) - ); - - describe('getSort function', function() { - it('should be a function', function() { - expect(getSort).to.be.a(Function); - }); - - it('should return an array of objects', function() { - expect(getSort([['bytes', 'desc']], indexPattern)).to.eql([{ bytes: 'desc' }]); - - delete indexPattern.timeFieldName; - expect(getSort([['bytes', 'desc']], indexPattern)).to.eql([{ bytes: 'desc' }]); - }); - - it('should passthrough arrays of objects', () => { - expect(getSort([{ bytes: 'desc' }], indexPattern)).to.eql([{ bytes: 'desc' }]); - }); - - it('should return an empty array when passed an unsortable field', function() { - expect(getSort(['non-sortable', 'asc'], indexPattern)).to.eql([]); - expect(getSort(['lol_nope', 'asc'], indexPattern)).to.eql([]); - - delete indexPattern.timeFieldName; - expect(getSort(['non-sortable', 'asc'], indexPattern)).to.eql([]); - }); - - it('should return an empty array ', function() { - expect(getSort([], indexPattern)).to.eql([]); - expect(getSort(['foo'], indexPattern)).to.eql([]); - expect(getSort({ foo: 'bar' }, indexPattern)).to.eql([]); - }); - - it('should return an empty array on non-time patterns', function() { - delete indexPattern.timeFieldName; - - expect(getSort([], indexPattern)).to.eql([]); - expect(getSort(['foo'], indexPattern)).to.eql([]); - expect(getSort({ foo: 'bar' }, indexPattern)).to.eql([]); - }); - }); - - describe('getSort.array function', function() { - it('should have an array method', function() { - expect(getSort.array).to.be.a(Function); - }); - - it('should return an array of arrays for sortable fields', function() { - expect(getSort.array([['bytes', 'desc']], indexPattern)).to.eql([['bytes', 'desc']]); - }); - - it('should return an array of arrays from an array of elasticsearch sort objects', function() { - expect(getSort.array([{ bytes: 'desc' }], indexPattern)).to.eql([['bytes', 'desc']]); - }); - - it('should sort by an empty array when an unsortable field is given', function() { - expect(getSort.array([{ 'non-sortable': 'asc' }], indexPattern)).to.eql([]); - expect(getSort.array([{ lol_nope: 'asc' }], indexPattern)).to.eql([]); - - delete indexPattern.timeFieldName; - expect(getSort.array([{ 'non-sortable': 'asc' }], indexPattern)).to.eql([]); - }); - - it('should return an empty array when passed an empty sort array', () => { - expect(getSort.array([], indexPattern)).to.eql([]); - - delete indexPattern.timeFieldName; - expect(getSort.array([], indexPattern)).to.eql([]); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index fea834686eb4f0..36a6c8eaef40ec 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -42,22 +42,10 @@ import { registerListenEventListener } from 'ui/directives/listen/listen'; // @ts-ignore import { KbnAccessibleClickProvider } from 'ui/accessibility/kbn_accessible_click'; // @ts-ignore -import { FieldNameDirectiveProvider } from 'ui/directives/field_name'; -// @ts-ignore -import { CollapsibleSidebarProvider } from 'ui/collapsible_sidebar/collapsible_sidebar'; -// @ts-ignore -import { CssTruncateProvide } from 'ui/directives/css_truncate'; -// @ts-ignore -import { FixedScrollProvider } from 'ui/fixed_scroll'; -// @ts-ignore -import { DebounceProviderTimeout } from 'ui/directives/debounce/debounce'; -// @ts-ignore import { AppStateProvider } from 'ui/state_management/app_state'; // @ts-ignore import { GlobalStateProvider } from 'ui/state_management/global_state'; // @ts-ignore -import { createRenderCompleteDirective } from 'ui/render_complete/directive'; -// @ts-ignore import { StateManagementConfigProvider } from 'ui/state_management/config_provider'; // @ts-ignore import { KbnUrlProvider, RedirectWhenMissingProvider } from 'ui/url'; @@ -81,11 +69,19 @@ import { createFieldSearchDirective } from './np_ready/components/field_chooser/ import { createIndexPatternSelectDirective } from './np_ready/components/field_chooser/discover_index_pattern_directive'; import { createStringFieldProgressBarDirective } from './np_ready/components/field_chooser/string_progress_bar'; // @ts-ignore +import { FieldNameDirectiveProvider } from './np_ready/angular/directives/field_name'; +// @ts-ignore import { createFieldChooserDirective } from './np_ready/components/field_chooser/field_chooser'; - // @ts-ignore import { createDiscoverFieldDirective } from './np_ready/components/field_chooser/discover_field'; +import { CollapsibleSidebarProvider } from './np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar'; import { DiscoverStartPlugins } from './plugin'; +import { createCssTruncateDirective } from './np_ready/angular/directives/css_truncate'; +// @ts-ignore +import { FixedScrollProvider } from './np_ready/angular/directives/fixed_scroll'; +// @ts-ignore +import { DebounceProviderTimeout } from './np_ready/angular/directives/debounce/debounce'; +import { createRenderCompleteDirective } from './np_ready/angular/directives/render_complete'; /** * returns the main inner angular module, it contains all the parts of Angular Discover @@ -181,7 +177,7 @@ export function initializeInnerAngularModule( .directive('kbnAccessibleClick', KbnAccessibleClickProvider) .directive('fieldName', FieldNameDirectiveProvider) .directive('collapsibleSidebar', CollapsibleSidebarProvider) - .directive('cssTruncate', CssTruncateProvide) + .directive('cssTruncate', createCssTruncateDirective) .directive('fixedScroll', FixedScrollProvider) .directive('renderComplete', createRenderCompleteDirective) .directive('discoverFieldSearch', createFieldSearchDirective) diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 9a0b0731b6b111..d1e1dafe7c8783 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -47,10 +47,6 @@ export function setServices(newServices: any) { services = newServices; } -// import directives that -import 'ui/directives/css_truncate'; -import 'ui/directives/field_name'; - // EXPORT legacy static dependencies, should be migrated when available in a new version; export { angular }; export { wrapInI18nContext } from 'ui/i18n'; @@ -90,7 +86,6 @@ export { } from '../../../../../plugins/data/public'; export { ElasticSearchHit } from './np_ready/doc_views/doc_views_types'; export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; -export { FieldName } from 'ui/directives/field_name/field_name'; export { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; // @ts-ignore export { buildPointSeriesData } from 'ui/agg_response/point_series/point_series'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/_index.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/_index.scss index c65243d99c8f49..2bfc74ffa02790 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/_index.scss +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/_index.scss @@ -1,2 +1,3 @@ @import 'no_results'; -@import 'histogram'; \ No newline at end of file +@import 'histogram'; +@import './collapsible_sidebar/index'; diff --git a/src/legacy/ui/public/collapsible_sidebar/_collapsible_sidebar.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_collapsible_sidebar.scss similarity index 100% rename from src/legacy/ui/public/collapsible_sidebar/_collapsible_sidebar.scss rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_collapsible_sidebar.scss diff --git a/src/legacy/ui/public/styles/_legacy/_depth.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_depth.scss similarity index 100% rename from src/legacy/ui/public/styles/_legacy/_depth.scss rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_depth.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_index.scss b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_index.scss new file mode 100644 index 00000000000000..1409920d11aa7f --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/_index.scss @@ -0,0 +1,2 @@ +@import 'depth'; +@import 'collapsible_sidebar'; diff --git a/src/legacy/ui/public/collapsible_sidebar/collapsible_sidebar.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar.ts similarity index 91% rename from src/legacy/ui/public/collapsible_sidebar/collapsible_sidebar.js rename to src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar.ts index 98b8f310bb82f0..5b6de7f16d4446 100644 --- a/src/legacy/ui/public/collapsible_sidebar/collapsible_sidebar.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/collapsible_sidebar/collapsible_sidebar.ts @@ -19,7 +19,11 @@ import _ from 'lodash'; import $ from 'jquery'; -import { uiModules } from '../modules'; +import { IScope } from 'angular'; + +interface LazyScope extends IScope { + [key: string]: any; +} export function CollapsibleSidebarProvider() { // simply a list of all of all of angulars .col-md-* classes except 12 @@ -29,7 +33,7 @@ export function CollapsibleSidebarProvider() { return { restrict: 'C', - link: function($scope, $elem) { + link: ($scope: LazyScope, $elem: any) => { let isCollapsed = false; const $collapser = $( `

- -

- - -

- -

-
- - - - - APM automatically collects in-depth performance metrics and errors from inside your applications. -
- } - footer={ - - - - } - icon={ + + + - } - title="APM" - /> - - - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. -
- } - footer={ - + + - - - } - icon={ - + + + + + + + + + + APM automatically collects in-depth performance metrics and errors from inside your applications. + + } + footer={ + + + + } + textAlign="left" + title="APM" + titleSize="xs" /> - } - title="Logs" - /> + + + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + + } + footer={ + + + + } + textAlign="left" + title="Logs" + titleSize="xs" + /> + + + + Collect metrics from the operating system and services running on your servers. + + } + footer={ + + + + } + textAlign="left" + title="Metrics" + titleSize="xs" + /> + + - - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - icon={ + + - } - title="Metrics" + + + +

+ +

+
+
+
+ -
- } - icon={ - - } + textAlign="left" title="SIEM" + titleSize="xs" /> @@ -257,109 +300,159 @@ exports[`isNewKibanaInstance 1`] = ` - - - -

- -

-
- -

- -

-
-
-
- - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - - - - } - icon={ + + + - } - title="Logs" - /> + + + +

+ +

+
+
+
+ + + + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + + } + footer={ + + + + } + textAlign="left" + title="Logs" + titleSize="xs" + /> + + + + Collect metrics from the operating system and services running on your servers. + + } + footer={ + + + + } + textAlign="left" + title="Metrics" + titleSize="xs" + /> + +
- - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - icon={ + + - } - title="Metrics" + + + +

+ +

+
+
+
+ -
- } - icon={ - - } + textAlign="left" title="SIEM" + titleSize="xs" />
@@ -476,143 +565,190 @@ exports[`mlEnabled 1`] = ` - - - -

- -

-
- -

- -

-
-
-
- - - APM automatically collects in-depth performance metrics and errors from inside your applications. - - } - footer={ - - - - } - icon={ + + + - } - title="APM" - /> - - - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - + + - - - } - icon={ - + + + + + + + + + + APM automatically collects in-depth performance metrics and errors from inside your applications. + + } + footer={ + + + + } + textAlign="left" + title="APM" + titleSize="xs" /> - } - title="Logs" - /> + + + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + + } + footer={ + + + + } + textAlign="left" + title="Logs" + titleSize="xs" + /> + + + + Collect metrics from the operating system and services running on your servers. + + } + footer={ + + + + } + textAlign="left" + title="Metrics" + titleSize="xs" + /> + + - - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - icon={ + + - } - title="Metrics" + + + +

+ +

+
+
+
+ -
- } - icon={ - - } + textAlign="left" title="SIEM" + titleSize="xs" />
@@ -765,109 +897,159 @@ exports[`render 1`] = ` - - - -

- -

-
- -

- -

-
-
-
- - - Ingest logs from popular data sources and easily visualize in preconfigured dashboards. - - } - footer={ - - - - } - icon={ + + + - } - title="Logs" - /> + + + +

+ +

+
+
+
+ + + + + Ingest logs from popular data sources and easily visualize in preconfigured dashboards. + + } + footer={ + + + + } + textAlign="left" + title="Logs" + titleSize="xs" + /> + + + + Collect metrics from the operating system and services running on your servers. + + } + footer={ + + + + } + textAlign="left" + title="Metrics" + titleSize="xs" + /> + +
- - Collect metrics from the operating system and services running on your servers. - - } - footer={ - - - - } - icon={ + + - } - title="Metrics" + + + +

+ +

+
+
+
+ -
- } - icon={ - - } + textAlign="left" title="SIEM" + titleSize="xs" />
diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap index 34cc0eb9265ffa..525cc5bdda9d42 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/__snapshots__/synopsis.test.js.snap @@ -15,6 +15,7 @@ exports[`props iconType 1`] = ` } layout="horizontal" title="Great tutorial" + titleSize="xs" /> `; @@ -34,6 +35,7 @@ exports[`props iconUrl 1`] = ` } layout="horizontal" title="Great tutorial" + titleSize="xs" /> `; @@ -46,6 +48,7 @@ exports[`props isBeta 1`] = ` href="link_to_item" layout="horizontal" title="Great tutorial" + titleSize="xs" /> `; @@ -58,5 +61,6 @@ exports[`render 1`] = ` href="link_to_item" layout="horizontal" title="Great tutorial" + titleSize="xs" /> `; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/_add_data.scss b/src/legacy/core_plugins/kibana/public/home/np_ready/components/_add_data.scss index 2b87ddbcf4673a..836b34227a37c2 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/_add_data.scss +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/_add_data.scss @@ -1,9 +1,22 @@ .homAddData__card { - max-width: 250px; border: none; box-shadow: none; } +.homAddData__cardDivider { + position: relative; + + &:after { + position: absolute; + content: ''; + width: 1px; + right: -$euiSizeS; + top: 0; + bottom: 0; + background: $euiBorderColor; + } +} + .homAddData__icon { width: $euiSizeXL * 2; height: $euiSizeXL * 2; @@ -18,10 +31,8 @@ text-align: center; } -@include euiBreakpoint('xs') { - .homAddData__card { - max-width: none; - } +.homAddData__logo { + margin-left: $euiSize; } @include euiBreakpoint('xs', 's') { @@ -30,6 +41,21 @@ } } +@include euiBreakpoint('xs', 's', 'm') { + .homAddDat__flexTablet { + flex-direction: column; + } + + .homAddData__cardDivider:after { + display: none; + } + + .homAddData__cardDivider { + flex-grow: 0 !important; + flex-basis: 100% !important; + } +} + @include euiBreakpoint('l', 'xl') { .homeAddData__flexGroup { flex-wrap: nowrap; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/add_data.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/add_data.js index a49620be2d229e..6d980114f96475 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/add_data.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/add_data.js @@ -91,10 +91,11 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { }; const getApmCard = () => ( - + } + titleSize="xs" title={apmData.title} description={{apmData.description}} footer={ @@ -115,60 +116,110 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { return ( - {apmUiEnabled !== false && getApmCard()} + + + + + + + + +

+ +

+
+
+
+ + + {apmUiEnabled !== false && getApmCard()} - - } - title={loggingData.title} - description={{loggingData.description}} - footer={ - - - - } - /> - + + {loggingData.description} + } + footer={ + + + + } + /> + - - } - title={metricsData.title} - description={{metricsData.description}} - footer={ - - - - } - /> + + {metricsData.description} + } + footer={ + + + + } + /> + +
- + + + + + + + +

+ +

+
+
+
+ } title={siemData.title} description={{siemData.description}} footer={ @@ -179,7 +230,7 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { > } @@ -195,29 +246,6 @@ const AddDataUi = ({ apmUiEnabled, isNewKibanaInstance, intl, mlEnabled }) => { return ( - - - -

- -

-
- -

- -

-
-
-
- - - {renderCards()} diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js index 5c32a463da1153..0c09c6c3c74fcb 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.js @@ -48,7 +48,7 @@ export class Home extends Component { super(props); const isWelcomeEnabled = !( - getServices().getInjected('disableWelcomeScreen') || + getServices().homeConfig.disableWelcomeScreen || props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' ); const currentOptInStatus = this.props.getOptInStatus(); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js index 27d4f1a8b1c1fa..d25a1f81dae5a5 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home.test.js @@ -30,6 +30,7 @@ jest.mock('../../kibana_services', () => ({ getServices: () => ({ getBasePath: () => 'path', getInjected: () => '', + homeConfig: { disableWelcomeScreen: false }, }), })); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js index e49f00b949da5b..f6c91b412381cd 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/home_app.js @@ -30,7 +30,7 @@ import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../../kibana_services'; export function HomeApp({ directories }) { const { - getInjected, + config, savedObjectsClient, getBasePath, addBasePath, @@ -41,7 +41,7 @@ export function HomeApp({ directories }) { const mlEnabled = environment.ml; const apmUiEnabled = environment.apmUi; - const defaultAppId = getInjected('kbnDefaultAppId', 'discover'); + const defaultAppId = config.defaultAppId || 'discover'; const renderTutorialDirectory = props => { return ( diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js b/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js index 38dff429f6d357..968b8eb64def5c 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/synopsis.js @@ -55,6 +55,7 @@ export function Synopsis({ className={classes} layout="horizontal" icon={optionalImg} + titleSize="xs" title={title} description={description} onClick={onClick} diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/__snapshots__/instruction_set.test.js.snap b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/__snapshots__/instruction_set.test.js.snap index 0d962ac514137f..9409963ff95aa7 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/__snapshots__/instruction_set.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/tutorial/__snapshots__/instruction_set.test.js.snap @@ -22,11 +22,7 @@ exports[`render 1`] = ` grow={false} />
- + - + - + - + - + - + - <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fillRule="evenodd" diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 502c8f45646cf0..aec3835dc075d9 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -27,6 +27,7 @@ import { Environment, FeatureCatalogueEntry, HomePublicPluginStart, + HomePublicPluginSetup, } from '../../../../../plugins/home/public'; export interface LegacyAngularInjectedDependencies { @@ -59,6 +60,7 @@ export interface HomePluginSetupDependencies { }; usageCollection: UsageCollectionSetup; kibana_legacy: KibanaLegacySetup; + home: HomePublicPluginSetup; } export class HomePlugin implements Plugin { @@ -69,6 +71,7 @@ export class HomePlugin implements Plugin { setup( core: CoreSetup, { + home, kibana_legacy, usageCollection, __LEGACY: { getAngularDependencies, ...legacyServices }, @@ -95,6 +98,8 @@ export class HomePlugin implements Plugin { getBasePath: core.http.basePath.get, indexPatternService: this.dataStart!.indexPatterns, environment: this.environment!, + config: kibana_legacy.config, + homeConfig: home.config, ...angularDependencies, }); const { renderApp } = await import('./np_ready/application'); diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 50f1702a2a6d0e..f2868da947a75d 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -20,7 +20,6 @@ // autoloading // preloading (for faster webpack builds) -import chrome from 'ui/chrome'; import routes from 'ui/routes'; import { uiModules } from 'ui/modules'; import { npSetup } from 'ui/new_platform'; @@ -64,8 +63,9 @@ localApplicationService.attachToAngular(routes); routes.enable(); +const { config } = npSetup.plugins.kibana_legacy; routes.otherwise({ - redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}`, + redirectTo: `/${config.defaultAppId || 'discover'}`, }); uiModules.get('kibana').run(showAppRedirectNotification); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap index 4ba0fe480ac423..2c0a5d8f6b8f19 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/__snapshots__/objects_table.test.js.snap @@ -78,7 +78,6 @@ exports[`ObjectsTable delete should show a confirm modal 1`] = ` exports[`ObjectsTable export should allow the user to choose when exporting all 1`] = ` <EuiModal - maxWidth={true} onClose={[Function]} > <EuiModalHeader> diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index f7fd19e8288e73..15e9c73a39effe 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -34,6 +34,7 @@ import { VisualizationsStart } from '../../../visualizations/public'; import { SavedVisualizations } from './np_ready/types'; import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { Chrome } from './legacy_imports'; +import { KibanaLegacyStart } from '../../../../../plugins/kibana_legacy/public'; export interface VisualizeKibanaServices { addBasePath: (url: string) => string; @@ -52,6 +53,7 @@ export interface VisualizeKibanaServices { savedVisualizations: SavedVisualizations; share: SharePluginStart; uiSettings: IUiSettingsClient; + config: KibanaLegacyStart['config']; visualizeCapabilities: any; visualizations: VisualizationsStart; usageCollection?: UsageCollectionSetup; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js index d99771ccc912dd..24055b9a2d9ed9 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/legacy_app.js @@ -173,7 +173,7 @@ export function initVisualizeApp(app, deps) { }, }) .when(`visualize/:tail*?`, { - redirectTo: `/${deps.core.injectedMetadata.getInjectedVar('kbnDefaultAppId')}`, + redirectTo: `/${deps.config.defaultAppId}`, }); }); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap index c75fd2096feab7..2dcf3ed0ea06f1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -442,7 +442,6 @@ exports[`NewVisModal filter for visualization types should render as expected 1` <EuiModal aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further." className="visNewVisDialog" - maxWidth={true} onClose={[Function]} role="menu" > @@ -2179,7 +2178,6 @@ exports[`NewVisModal should render as expected 1`] = ` <EuiModal aria-label="Start creating your visualization by selecting a type for that visualization. Hit escape to close this modal. Hit Tab key to go further." className="visNewVisDialog" - maxWidth={true} onClose={[Function]} role="menu" > @@ -2897,7 +2895,6 @@ exports[`NewVisModal should render as expected 1`] = ` value="" > <EuiFormControlLayout - clear={null} compressed={false} fullWidth={true} icon="search" @@ -2922,7 +2919,6 @@ exports[`NewVisModal should render as expected 1`] = ` /> </EuiValidatableControl> <EuiFormControlLayoutIcons - clear={null} icon="search" isLoading={false} > diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 26c6691a3613f1..8e7487fee55f69 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -108,6 +108,7 @@ export class VisualizePlugin implements Plugin { share, toastNotifications: contextCore.notifications.toasts, uiSettings: contextCore.uiSettings, + config: kibana_legacy.config, visualizeCapabilities: contextCore.application.capabilities.visualize, visualizations, usageCollection, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index 9bf7ee3d594013..056849a2926570 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -39,6 +39,7 @@ import { createGoalVisTypeDefinition, } from './vis_type_vislib_vis_types'; import { ChartsPluginSetup } from '../../../../plugins/charts/public'; +import { ConfigSchema as VisTypeXyConfigSchema } from '../../vis_type_xy'; export interface VisTypeVislibDependencies { uiSettings: IUiSettingsClient; @@ -72,11 +73,7 @@ export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> { uiSettings: core.uiSettings, charts, }; - - expressions.registerFunction(createVisTypeVislibVisFn); - expressions.registerFunction(createPieVisFn); - - [ + const vislibTypes = [ createHistogramVisTypeDefinition, createLineVisTypeDefinition, createPieVisTypeDefinition, @@ -85,7 +82,30 @@ export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> { createHorizontalBarVisTypeDefinition, createGaugeVisTypeDefinition, createGoalVisTypeDefinition, - ].forEach(vis => visualizations.types.createBaseVisualization(vis(visualizationDependencies))); + ]; + const vislibFns = [createVisTypeVislibVisFn, createPieVisFn]; + + const visTypeXy = core.injectedMetadata.getInjectedVar('visTypeXy') as + | VisTypeXyConfigSchema['visTypeXy'] + | undefined; + + // if visTypeXy plugin is disabled it's config will be undefined + if (!visTypeXy || !visTypeXy.enabled) { + const convertedTypes: any[] = []; + const convertedFns: any[] = []; + + // Register legacy vislib types that have been converted + convertedFns.forEach(expressions.registerFunction); + convertedTypes.forEach(vis => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + // Register non-converted types + vislibFns.forEach(expressions.registerFunction); + vislibTypes.forEach(vis => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); } public start(core: CoreStart, deps: VisTypeVislibPluginStartDependencies) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap index 9b2b633fd0e163..c2bac1c1e706a4 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`VisLegend Component Legend closed should match the snapshot 1`] = `"<div class=\\"visLegend\\"><button type=\\"button\\" class=\\"visLegend__toggle kbn-resetFocusState\\" aria-label=\\"Toggle legend\\" aria-expanded=\\"false\\" aria-controls=\\"legendId\\" data-test-subj=\\"vislibToggleLegend\\" title=\\"Toggle legend\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon--text euiIcon-isLoaded\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>"`; +exports[`VisLegend Component Legend closed should match the snapshot 1`] = `"
"`; -exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
"`; +exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
"`; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 6f0a5a3784b070..e66dff01b6bf21 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -34,8 +34,8 @@ jest.mock('@elastic/eui', () => ({ jest.mock('../../../legacy_imports', () => ({ getTableAggs: jest.fn(), })); -jest.mock('../../../../../visualizations/public', () => ({ - createFiltersFromEvent: jest.fn().mockReturnValue(['yes']), +jest.mock('../../../../../data/public/actions/filters/create_filters_from_event', () => ({ + createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), })); const vis = { @@ -95,8 +95,8 @@ const uiState = { setSilent: jest.fn(), }; -const getWrapper = (props?: Partial) => - mount( +const getWrapper = async (props?: Partial) => { + const wrapper = mount( ) => ); + await (wrapper.find(VisLegend).instance() as VisLegend).refresh(); + wrapper.update(); + return wrapper; +}; + const getLegendItems = (wrapper: ReactWrapper) => wrapper.find('.visLegend__button'); describe('VisLegend Component', () => { @@ -120,9 +125,9 @@ describe('VisLegend Component', () => { }); describe('Legend open', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -131,9 +136,9 @@ describe('VisLegend Component', () => { }); describe('Legend closed', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -142,25 +147,26 @@ describe('VisLegend Component', () => { }); describe('Highlighting', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); - it('should call highlight handler when legend item is focused', () => { + it('should call highlight handler when legend item is focused', async () => { const first = getLegendItems(wrapper).first(); + first.simulate('focus'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call highlight handler when legend item is hovered', () => { + it('should call highlight handler when legend item is hovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is blurred', () => { + it('should call unHighlight handler when legend item is blurred', async () => { let first = getLegendItems(wrapper).first(); first.simulate('focus'); first = getLegendItems(wrapper).first(); @@ -169,7 +175,7 @@ describe('VisLegend Component', () => { expect(vislibVis.handler.unHighlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is unhovered', () => { + it('should call unHighlight handler when legend item is unhovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); @@ -187,8 +193,8 @@ describe('VisLegend Component', () => { }, }; - expect(() => { - wrapper = getWrapper({ vis: newVis }); + expect(async () => { + wrapper = await getWrapper({ vis: newVis }); const first = getLegendItems(wrapper).first(); first.simulate('focus'); first.simulate('blur'); @@ -197,8 +203,8 @@ describe('VisLegend Component', () => { }); describe('Filtering', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should filter out when clicked', () => { @@ -223,8 +229,8 @@ describe('VisLegend Component', () => { }); describe('Toggles details', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should show details when clicked', () => { @@ -236,8 +242,8 @@ describe('VisLegend Component', () => { }); describe('setColor', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('sets the color in the UI state', () => { @@ -255,18 +261,18 @@ describe('VisLegend Component', () => { }); describe('toggleLegend function', () => { - it('click should show legend once toggled from hidden', () => { + it('click should show legend once toggled from hidden', async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); expect(wrapper.exists('.visLegend__list')).toBe(true); }); - it('click should hide legend once toggled from shown', () => { + it('click should hide legend once toggled from shown', async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 0eec557dd334ee..a170af33583df0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -24,7 +24,8 @@ import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; // @ts-ignore -import { createFiltersFromEvent } from '../../../../../visualizations/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createFiltersFromEvent } from '../../../../../data/public/actions/filters/create_filters_from_event'; import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; import { VisLegendItem } from './legend_item'; import { getPieNames } from './pie_utils'; @@ -94,11 +95,11 @@ export class VisLegend extends PureComponent { this.props.vis.API.events.filter({ data, negate }); }; - canFilter = (item: LegendItem): boolean => { + canFilter = async (item: LegendItem): Promise => { if (CUSTOM_LEGEND_VIS_TYPES.includes(this.props.vislibVis.visConfigArgs.type)) { return false; } - const filters = createFiltersFromEvent({ aggConfigs: this.state.tableAggs, data: item.values }); + const filters = await createFiltersFromEvent({ data: item.values }); return Boolean(filters.length); }; @@ -123,16 +124,39 @@ export class VisLegend extends PureComponent { }; // Most of these functions were moved directly from the old Legend class. Not a fan of this. - getLabels = (data: any, type: string) => { - if (!data) return []; - data = data.columns || data.rows || [data]; + setLabels = (data: any, type: string): Promise => + new Promise(async resolve => { + let labels = []; + if (CUSTOM_LEGEND_VIS_TYPES.includes(type)) { + const legendLabels = this.props.vislibVis.getLegendLabels(); + if (legendLabels) { + labels = map(legendLabels, label => { + return { label }; + }); + } + } else { + if (!data) return []; + data = data.columns || data.rows || [data]; - if (type === 'pie') return getPieNames(data); + labels = type === 'pie' ? getPieNames(data) : this.getSeriesLabels(data); + } - return this.getSeriesLabels(data); - }; + const labelsConfig = await Promise.all( + labels.map(async label => ({ + ...label, + canFilter: await this.canFilter(label), + })) + ); + + this.setState( + { + labels: labelsConfig, + }, + resolve + ); + }); - refresh = () => { + refresh = async () => { const vislibVis = this.props.vislibVis; if (!vislibVis || !vislibVis.visConfig) { this.setState({ @@ -154,24 +178,12 @@ export class VisLegend extends PureComponent { this.setState({ open: this.props.vis.params.addLegend }); } - if (CUSTOM_LEGEND_VIS_TYPES.includes(vislibVis.visConfigArgs.type)) { - const legendLabels = this.props.vislibVis.getLegendLabels(); - if (legendLabels) { - this.setState({ - labels: map(legendLabels, label => { - return { label }; - }), - }); - } - } else { - this.setState({ labels: this.getLabels(this.props.visData, vislibVis.visConfigArgs.type) }); - } - if (vislibVis.visConfig) { this.getColor = this.props.vislibVis.visConfig.data.getColorFunc(); } this.setState({ tableAggs: getTableAggs(this.props.vis) }); + await this.setLabels(this.props.visData, vislibVis.visConfigArgs.type); }; highlight = (event: BaseSyntheticEvent) => { @@ -219,7 +231,7 @@ export class VisLegend extends PureComponent { key={item.label} anchorPosition={anchorPosition} selected={this.state.selectedLabel === item.label} - canFilter={this.canFilter(item)} + canFilter={item.canFilter} onFilter={this.filter} onSelect={this.toggleDetails} legendId={this.legendId} diff --git a/src/legacy/core_plugins/vis_type_xy/index.ts b/src/legacy/core_plugins/vis_type_xy/index.ts new file mode 100644 index 00000000000000..975399f891503d --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/index.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { Legacy } from 'kibana'; + +import { LegacyPluginApi, LegacyPluginInitializer } from '../../types'; + +export interface ConfigSchema { + visTypeXy: { + enabled: boolean; + }; +} + +const visTypeXyPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => + new Plugin({ + id: 'visTypeXy', + require: ['kibana', 'elasticsearch', 'visualizations', 'interpreter', 'data'], + publicDir: resolve(__dirname, 'public'), + uiExports: { + hacks: [resolve(__dirname, 'public/legacy')], + injectDefaultVars(server): ConfigSchema { + const config = server.config(); + + return { + visTypeXy: { + enabled: config.get('visTypeXy.enabled') as boolean, + }, + }; + }, + }, + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(false), + }).default(); + }, + } as Legacy.PluginSpecOptions); + +// eslint-disable-next-line import/no-default-export +export default visTypeXyPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_xy/package.json b/src/legacy/core_plugins/vis_type_xy/package.json new file mode 100644 index 00000000000000..920f7dcb44e87c --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/package.json @@ -0,0 +1,4 @@ +{ + "name": "visTypeXy", + "version": "kibana" +} diff --git a/src/legacy/core_plugins/vis_type_xy/public/index.ts b/src/legacy/core_plugins/vis_type_xy/public/index.ts new file mode 100644 index 00000000000000..218dc8aa8a6831 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from '../../../../core/public'; +import { VisTypeXyPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/vis_type_xy/public/legacy.ts b/src/legacy/core_plugins/vis_type_xy/public/legacy.ts new file mode 100644 index 00000000000000..e1cee9c30804a2 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/legacy.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { npSetup, npStart } from 'ui/new_platform'; +import { PluginInitializerContext } from 'kibana/public'; + +import { plugin } from '.'; +import { VisTypeXyPluginSetupDependencies, VisTypeXyPluginStartDependencies } from './plugin'; +import { + setup as visualizationsSetup, + start as visualizationsStart, +} from '../../visualizations/public/np_ready/public/legacy'; + +const setupPlugins: Readonly = { + expressions: npSetup.plugins.expressions, + visualizations: visualizationsSetup, + charts: npSetup.plugins.charts, +}; + +const startPlugins: Readonly = { + expressions: npStart.plugins.expressions, + visualizations: visualizationsStart, +}; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, setupPlugins); +export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/vis_type_xy/public/plugin.ts b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts new file mode 100644 index 00000000000000..59bb64b337256c --- /dev/null +++ b/src/legacy/core_plugins/vis_type_xy/public/plugin.ts @@ -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 { + CoreSetup, + CoreStart, + Plugin, + IUiSettingsClient, + PluginInitializerContext, +} from 'kibana/public'; + +import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../../../plugins/charts/public'; + +export interface VisTypeXyDependencies { + uiSettings: IUiSettingsClient; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeXyPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeXyPluginStartDependencies { + expressions: ReturnType; + visualizations: VisualizationsStart; +} + +type VisTypeXyCoreSetup = CoreSetup; + +/** @internal */ +export class VisTypeXyPlugin implements Plugin, void> { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: VisTypeXyCoreSetup, + { expressions, visualizations, charts }: VisTypeXyPluginSetupDependencies + ) { + // eslint-disable-next-line no-console + console.warn( + 'The visTypeXy plugin is enabled\n\n', + 'This may negatively alter existing vislib visualization configurations if saved.' + ); + const visualizationDependencies: Readonly = { + uiSettings: core.uiSettings, + charts, + }; + + const visTypeDefinitions: any[] = []; + const visFunctions: any = []; + + visFunctions.forEach((fn: any) => expressions.registerFunction(fn)); + visTypeDefinitions.forEach((vis: any) => + visualizations.types.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + public start(core: CoreStart, deps: VisTypeXyPluginStartDependencies) { + // nothing to do here + } +} diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 2af468ff77de6e..d3badcc6bdc3fc 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -17,13 +17,12 @@ * under the License. */ -import _, { forEach } from 'lodash'; +import _ from 'lodash'; import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/types'; -import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; @@ -34,7 +33,6 @@ import { Query, onlyDisabledFiltersChanged, esFilters, - mapAndFlattenFilters, ISearchSource, } from '../../../../../plugins/data/public'; import { @@ -42,7 +40,8 @@ import { EmbeddableOutput, Embeddable, Container, - APPLY_FILTER_TRIGGER, + VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, } from '../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; @@ -105,7 +104,6 @@ export class VisualizeEmbeddable extends Embeddable { - if (event.disabled || !eventName) { - return; - } else { - this.actions[eventName] = event.defaultAction; - } - }); - // This is a hack to give maps visualizations access to data in the // globalState, since they can no longer access it via searchSource. // TODO: Remove this as a part of elastic/kibana#30593 @@ -301,18 +290,13 @@ export class VisualizeEmbeddable extends Embeddable { - if (this.actions[event.name]) { - event.data.aggConfigs = getTableAggs(this.vis); - const filters: esFilters.Filter[] = this.actions[event.name](event.data) || []; - const mappedFilters = mapAndFlattenFilters(filters); - const timeFieldName = this.vis.indexPattern.timeFieldName; - - npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, - filters: mappedFilters, - timeFieldName, - }); - } + const eventName = event.name === 'brush' ? SELECT_RANGE_TRIGGER : VALUE_CLICK_TRIGGER; + + npStart.plugins.uiActions.executeTriggerActions(eventName, { + embeddable: this, + timeFieldName: this.vis.indexPattern.timeFieldName, + data: event.data, + }); }) ); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 4dffcb8ce995e3..3c4a1c1449d475 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -44,7 +44,6 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public static code */ export { Vis, VisParams, VisState } from './vis'; -export * from './filters'; export { TypesService } from './types/types_service'; export { Status } from './legacy/update_status'; @@ -53,6 +52,4 @@ export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/bui // @ts-ignore export { updateOldState } from './legacy/vis_update_state'; export { calculateObjectHash } from './legacy/calculate_object_hash'; -// @ts-ignore -export { createFiltersFromEvent } from './filters/vis_filters'; export { createSavedVisLoader } from '../../saved_visualizations/saved_visualizations'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js index f62b3a0b393aca..351acc48e26766 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js @@ -19,7 +19,6 @@ import _ from 'lodash'; -import { createFiltersFromEvent, onBrushEvent } from '../filters'; import { DefaultEditorController } from '../../../../../vis_default_editor/public'; export class BaseVisType { @@ -60,15 +59,6 @@ export class BaseVisType { showIndexSelection: true, hierarchicalData: false, // we should get rid of this i guess ? }, - events: { - filterBucket: { - defaultAction: createFiltersFromEvent, - }, - brush: { - defaultAction: onBrushEvent, - disabled: true, - }, - }, stage: 'production', feedbackMessage: '', hidden: false, diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 88a794445870c2..a24ffcbaaa49f0 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -103,6 +103,10 @@ export default () => logging: Joi.object() .keys({ + appenders: HANDLED_IN_NEW_PLATFORM, + loggers: HANDLED_IN_NEW_PLATFORM, + root: HANDLED_IN_NEW_PLATFORM, + silent: Joi.boolean().default(false), quiet: Joi.boolean().when('silent', { @@ -257,9 +261,9 @@ export default () => manifestServiceUrl: Joi.string() .default('') .allow(''), - emsFileApiUrl: Joi.string().default('https://vector-staging.maps.elastic.co'), + emsFileApiUrl: Joi.string().default('https://vector.maps.elastic.co'), emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'), - emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.4'), + emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.6'), emsFontLibraryUrl: Joi.string().default( 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf' ), diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index e4e58019dda691..e990ba2a46de53 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -10,7 +10,6 @@ @import './accessibility/index'; @import './chrome/index'; -@import './collapsible_sidebar/index'; @import './directives/index'; @import './error_auto_create_index/index'; @import './error_url_overflow/index'; diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index 3f88c540be1641..17a8b14b57d020 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -63,7 +63,7 @@ const unknownSchema: Schema = { const getTypeFromRegistry = (type: string): AggType => { // We need to inline require here, since we're having a cyclic dependency // from somewhere inside agg_types back to AggConfig. - const aggTypes = require('../agg_types').aggTypes; + const aggTypes = require('./agg_types').aggTypes; const registeredType = aggTypes.metrics.find((agg: AggType) => agg.name === type) || aggTypes.buckets.find((agg: AggType) => agg.name === type); diff --git a/src/legacy/ui/public/agg_types/agg_types.ts b/src/legacy/ui/public/agg_types/agg_types.ts new file mode 100644 index 00000000000000..1b05f5926ebfc9 --- /dev/null +++ b/src/legacy/ui/public/agg_types/agg_types.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. + */ + +import { countMetricAgg } from './metrics/count'; +import { avgMetricAgg } from './metrics/avg'; +import { sumMetricAgg } from './metrics/sum'; +import { medianMetricAgg } from './metrics/median'; +import { minMetricAgg } from './metrics/min'; +import { maxMetricAgg } from './metrics/max'; +import { topHitMetricAgg } from './metrics/top_hit'; +import { stdDeviationMetricAgg } from './metrics/std_deviation'; +import { cardinalityMetricAgg } from './metrics/cardinality'; +import { percentilesMetricAgg } from './metrics/percentiles'; +import { geoBoundsMetricAgg } from './metrics/geo_bounds'; +import { geoCentroidMetricAgg } from './metrics/geo_centroid'; +import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; +import { derivativeMetricAgg } from './metrics/derivative'; +import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; +import { movingAvgMetricAgg } from './metrics/moving_avg'; +import { serialDiffMetricAgg } from './metrics/serial_diff'; +import { dateHistogramBucketAgg } from './buckets/date_histogram'; +import { histogramBucketAgg } from './buckets/histogram'; +import { rangeBucketAgg } from './buckets/range'; +import { dateRangeBucketAgg } from './buckets/date_range'; +import { ipRangeBucketAgg } from './buckets/ip_range'; +import { termsBucketAgg } from './buckets/terms'; +import { filterBucketAgg } from './buckets/filter'; +import { filtersBucketAgg } from './buckets/filters'; +import { significantTermsBucketAgg } from './buckets/significant_terms'; +import { geoHashBucketAgg } from './buckets/geo_hash'; +import { geoTileBucketAgg } from './buckets/geo_tile'; +import { bucketSumMetricAgg } from './metrics/bucket_sum'; +import { bucketAvgMetricAgg } from './metrics/bucket_avg'; +import { bucketMinMetricAgg } from './metrics/bucket_min'; +import { bucketMaxMetricAgg } from './metrics/bucket_max'; + +export { AggType } from './agg_type'; + +export const aggTypes = { + metrics: [ + countMetricAgg, + avgMetricAgg, + sumMetricAgg, + medianMetricAgg, + minMetricAgg, + maxMetricAgg, + stdDeviationMetricAgg, + cardinalityMetricAgg, + percentilesMetricAgg, + percentileRanksMetricAgg, + topHitMetricAgg, + derivativeMetricAgg, + cumulativeSumMetricAgg, + movingAvgMetricAgg, + serialDiffMetricAgg, + bucketAvgMetricAgg, + bucketSumMetricAgg, + bucketMinMetricAgg, + bucketMaxMetricAgg, + geoBoundsMetricAgg, + geoCentroidMetricAgg, + ], + buckets: [ + dateHistogramBucketAgg, + histogramBucketAgg, + rangeBucketAgg, + dateRangeBucketAgg, + ipRangeBucketAgg, + termsBucketAgg, + filterBucketAgg, + filtersBucketAgg, + significantTermsBucketAgg, + geoHashBucketAgg, + geoTileBucketAgg, + ], +}; diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts index ca7c2f82023c94..cf2733b9a9f36a 100644 --- a/src/legacy/ui/public/agg_types/index.ts +++ b/src/legacy/ui/public/agg_types/index.ts @@ -17,80 +17,7 @@ * under the License. */ -import { countMetricAgg } from './metrics/count'; -import { avgMetricAgg } from './metrics/avg'; -import { sumMetricAgg } from './metrics/sum'; -import { medianMetricAgg } from './metrics/median'; -import { minMetricAgg } from './metrics/min'; -import { maxMetricAgg } from './metrics/max'; -import { topHitMetricAgg } from './metrics/top_hit'; -import { stdDeviationMetricAgg } from './metrics/std_deviation'; -import { cardinalityMetricAgg } from './metrics/cardinality'; -import { percentilesMetricAgg } from './metrics/percentiles'; -import { geoBoundsMetricAgg } from './metrics/geo_bounds'; -import { geoCentroidMetricAgg } from './metrics/geo_centroid'; -import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; -import { derivativeMetricAgg } from './metrics/derivative'; -import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; -import { movingAvgMetricAgg } from './metrics/moving_avg'; -import { serialDiffMetricAgg } from './metrics/serial_diff'; -import { dateHistogramBucketAgg, setBounds } from './buckets/date_histogram'; -import { histogramBucketAgg } from './buckets/histogram'; -import { rangeBucketAgg } from './buckets/range'; -import { dateRangeBucketAgg } from './buckets/date_range'; -import { ipRangeBucketAgg } from './buckets/ip_range'; -import { termsBucketAgg, termsAggFilter } from './buckets/terms'; -import { filterBucketAgg } from './buckets/filter'; -import { filtersBucketAgg } from './buckets/filters'; -import { significantTermsBucketAgg } from './buckets/significant_terms'; -import { geoHashBucketAgg } from './buckets/geo_hash'; -import { geoTileBucketAgg } from './buckets/geo_tile'; -import { bucketSumMetricAgg } from './metrics/bucket_sum'; -import { bucketAvgMetricAgg } from './metrics/bucket_avg'; -import { bucketMinMetricAgg } from './metrics/bucket_min'; -import { bucketMaxMetricAgg } from './metrics/bucket_max'; - -export { AggType } from './agg_type'; - -export const aggTypes = { - metrics: [ - countMetricAgg, - avgMetricAgg, - sumMetricAgg, - medianMetricAgg, - minMetricAgg, - maxMetricAgg, - stdDeviationMetricAgg, - cardinalityMetricAgg, - percentilesMetricAgg, - percentileRanksMetricAgg, - topHitMetricAgg, - derivativeMetricAgg, - cumulativeSumMetricAgg, - movingAvgMetricAgg, - serialDiffMetricAgg, - bucketAvgMetricAgg, - bucketSumMetricAgg, - bucketMinMetricAgg, - bucketMaxMetricAgg, - geoBoundsMetricAgg, - geoCentroidMetricAgg, - ], - buckets: [ - dateHistogramBucketAgg, - histogramBucketAgg, - rangeBucketAgg, - dateRangeBucketAgg, - ipRangeBucketAgg, - termsBucketAgg, - filterBucketAgg, - filtersBucketAgg, - significantTermsBucketAgg, - geoHashBucketAgg, - geoTileBucketAgg, - ], -}; - +export { aggTypes } from './agg_types'; export { AggParam } from './agg_params'; export { AggConfig } from './agg_config'; export { AggConfigs } from './agg_configs'; @@ -99,5 +26,6 @@ export { FieldParamType } from './param_types'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; - -export { setBounds, termsAggFilter }; +export { AggType } from './agg_type'; +export { setBounds } from './buckets/date_histogram'; +export { termsAggFilter } from './buckets/terms'; diff --git a/src/legacy/ui/public/agg_types/param_types/field.ts b/src/legacy/ui/public/agg_types/param_types/field.ts index 4ce5bb29f8ff60..d01e059c6c616b 100644 --- a/src/legacy/ui/public/agg_types/param_types/field.ts +++ b/src/legacy/ui/public/agg_types/param_types/field.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { AggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; -import { toastNotifications } from '../../notify'; +import { npStart } from '../../new_platform'; import { propFilter } from '../filter'; import { Field, IFieldList } from '../../../../../plugins/data/public'; import { isNestedField } from '../../../../../plugins/data/public'; @@ -89,7 +89,7 @@ export class FieldParamType extends BaseParamType { (f: any) => f.name === fieldName ); if (!validField) { - toastNotifications.addDanger( + npStart.core.notifications.toasts.addDanger( i18n.translate( 'common.ui.aggTypes.paramTypes.field.invalidSavedFieldParameterErrorMessage', { diff --git a/src/legacy/ui/public/agg_types/utils.ts b/src/legacy/ui/public/agg_types/utils.ts index fd405d49625eda..e382f821b31a90 100644 --- a/src/legacy/ui/public/agg_types/utils.ts +++ b/src/legacy/ui/public/agg_types/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { isValidEsInterval } from '../../../core_plugins/data/public'; +import { isValidEsInterval } from '../../../core_plugins/data/common/parse_es_interval/is_valid_es_interval'; import { leastCommonInterval } from '../vis/lib/least_common_interval'; /** diff --git a/src/legacy/ui/public/collapsible_sidebar/_index.scss b/src/legacy/ui/public/collapsible_sidebar/_index.scss deleted file mode 100644 index 84b759f66720ed..00000000000000 --- a/src/legacy/ui/public/collapsible_sidebar/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../styles/_legacy/depth'; - -@import './collapsible_sidebar'; diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts index c89ae9f8b3c9b1..439ac9b5713dfa 100644 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts @@ -27,6 +27,7 @@ import { inspectorPluginMock } from '../../../../../plugins/inspector/public/moc import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks'; import { managementPluginMock } from '../../../../../plugins/management/public/mocks'; import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks'; +import { kibanaLegacyPluginMock } from '../../../../../plugins/kibana_legacy/public/mocks'; import { chartPluginMock } from '../../../../../plugins/charts/public/mocks'; /* eslint-enable @kbn/eslint/no-restricted-paths */ @@ -40,6 +41,7 @@ export const pluginsMock = { expressions: expressionsPluginMock.createSetupContract(), uiActions: uiActionsPluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), + kibana_legacy: kibanaLegacyPluginMock.createSetupContract(), }), createStart: () => ({ data: dataPluginMock.createStartContract(), @@ -50,6 +52,7 @@ export const pluginsMock = { expressions: expressionsPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), management: managementPluginMock.createStartContract(), + kibana_legacy: kibanaLegacyPluginMock.createStartContract(), }), }; diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f98b8801d52663..c2c8b5a0fae7a7 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -119,6 +119,9 @@ export const npSetup = { kibana_legacy: { registerLegacyApp: () => {}, forwardApp: () => {}, + config: { + defaultAppId: 'home', + }, }, inspector: { registerView: () => undefined, @@ -140,6 +143,9 @@ export const npSetup = { environment: { update: sinon.fake(), }, + config: { + disableWelcomeScreen: false, + }, }, charts: { theme: { @@ -196,6 +202,9 @@ export const npStart = { kibana_legacy: { getApps: () => [], getForwards: () => [], + config: { + defaultAppId: 'home', + }, }, data: { autocomplete: { @@ -297,6 +306,9 @@ export const npStart = { environment: { get: sinon.fake(), }, + config: { + disableWelcomeScreen: false, + }, }, navigation: { ui: { diff --git a/src/legacy/ui/public/styles/_legacy/_index.scss b/src/legacy/ui/public/styles/_legacy/_index.scss index 106730fecd2b8e..a0b1a98b09b7dd 100644 --- a/src/legacy/ui/public/styles/_legacy/_index.scss +++ b/src/legacy/ui/public/styles/_legacy/_index.scss @@ -1,6 +1,5 @@ // // // // KIBANA THEME -@import './depth'; @import './base'; @import './mixins'; diff --git a/src/legacy/ui/public/vis/lib/least_common_interval.ts b/src/legacy/ui/public/vis/lib/least_common_interval.ts index 244bc1d0111e3b..72426855f70af5 100644 --- a/src/legacy/ui/public/vis/lib/least_common_interval.ts +++ b/src/legacy/ui/public/vis/lib/least_common_interval.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { leastCommonMultiple } from './least_common_multiple'; -import { parseEsInterval } from '../../../../core_plugins/data/public'; +import { parseEsInterval } from '../../../../core_plugins/data/common/parse_es_interval/parse_es_interval'; /** * Finds the lowest common interval between two given ES date histogram intervals diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts index a57caad1d34bf8..c294c79542bbe9 100644 --- a/src/legacy/utils/index.d.ts +++ b/src/legacy/utils/index.d.ts @@ -18,3 +18,16 @@ */ export function unset(object: object, rawPath: string): void; + +export { + concatStreamProviders, + createConcatStream, + createFilterStream, + createIntersperseStream, + createListStream, + createMapStream, + createPromiseFromStreams, + createReduceStream, + createReplaceStream, + createSplitStream, +} from './streams'; diff --git a/src/legacy/utils/streams/index.d.ts b/src/legacy/utils/streams/index.d.ts index b8d4c67050b2da..5ef39b292c6858 100644 --- a/src/legacy/utils/streams/index.d.ts +++ b/src/legacy/utils/streams/index.d.ts @@ -20,17 +20,17 @@ import { Readable, Transform, Writable, TransformOptions } from 'stream'; export function concatStreamProviders( - sourceProviders: Readable[], + sourceProviders: Array<() => Readable>, options: TransformOptions ): Transform; export function createIntersperseStream(intersperseChunk: string | Buffer): Transform; export function createSplitStream(splitChunk: T): Transform; -export function createListStream(items: any[]): Readable; +export function createListStream(items: any | any[]): Readable; export function createReduceStream(reducer: (value: any, chunk: T, enc: string) => T): Transform; export function createPromiseFromStreams([first, ...rest]: [Readable, ...Writable[]]): Promise< T >; -export function createConcatStream(initial: any): Transform; +export function createConcatStream(initial?: any): Transform; export function createMapStream(fn: (value: T, i: number) => void): Transform; export function createReplaceStream(toReplace: string, replacement: string | Buffer): Transform; export function createFilterStream(fn: (obj: T) => boolean): Transform; diff --git a/src/plugins/data/public/search/search_source/types.ts b/src/plugins/data/public/search/search_source/types.ts index 17337c905db877..268d24aaa6df11 100644 --- a/src/plugins/data/public/search/search_source/types.ts +++ b/src/plugins/data/public/search/search_source/types.ts @@ -26,7 +26,12 @@ export enum SortDirection { desc = 'desc', } -export type EsQuerySortValue = Record; +export interface SortDirectionNumeric { + order: SortDirection; + numeric_type?: 'double' | 'long' | 'date' | 'date_nanos'; +} + +export type EsQuerySortValue = Record; export interface SearchSourceFields { type?: string; diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 801329a4a79af9..1e0e7dfdb09331 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -23,6 +23,8 @@ import { APPLY_FILTER_TRIGGER, createFilterAction, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, } from './lib'; /** @@ -50,11 +52,25 @@ export const bootstrap = (uiActions: IUiActionsSetup) => { description: 'Actions appear in title bar when an embeddable loads in a panel', actionIds: [], }; + const selectRangeTrigger = { + id: SELECT_RANGE_TRIGGER, + title: 'Select range', + description: 'Applies a range filter', + actionIds: [], + }; + const valueClickTrigger = { + id: VALUE_CLICK_TRIGGER, + title: 'Value clicked', + description: 'Value was clicked', + actionIds: [], + }; const actionApplyFilter = createFilterAction(); uiActions.registerTrigger(triggerContext); uiActions.registerTrigger(triggerFilter); uiActions.registerAction(actionApplyFilter); uiActions.registerTrigger(triggerBadge); + uiActions.registerTrigger(selectRangeTrigger); + uiActions.registerTrigger(valueClickTrigger); // uiActions.attachAction(triggerFilter.id, actionApplyFilter.id); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 583b21ddfa4338..ec71a1e724c7d8 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -25,6 +25,8 @@ export { APPLY_FILTER_ACTION, APPLY_FILTER_TRIGGER, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, Adapters, AddPanelAction, CONTEXT_MENU_TRIGGER, diff --git a/src/plugins/embeddable/public/lib/triggers/index.ts b/src/plugins/embeddable/public/lib/triggers/index.ts index ffa7f6d0c0f448..72565b3f527adc 100644 --- a/src/plugins/embeddable/public/lib/triggers/index.ts +++ b/src/plugins/embeddable/public/lib/triggers/index.ts @@ -19,4 +19,6 @@ export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; +export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; +export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; diff --git a/src/plugins/expressions/common/expression_types/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/kibana_datatable.ts index c360a2be8c7f7b..38227d2ed6207b 100644 --- a/src/plugins/expressions/common/expression_types/kibana_datatable.ts +++ b/src/plugins/expressions/common/expression_types/kibana_datatable.ts @@ -23,9 +23,16 @@ import { Datatable, PointSeries } from '.'; const name = 'kibana_datatable'; +export interface KibanaDatatableColumnMeta { + type: string; + indexPatternId?: string; + aggConfigParams?: Record; +} + export interface KibanaDatatableColumn { id: string; name: string; + meta?: KibanaDatatableColumnMeta; formatHint?: SerializedFieldFormat; } diff --git a/src/plugins/home/config.ts b/src/plugins/home/config.ts new file mode 100644 index 00000000000000..149723a7ee5ae2 --- /dev/null +++ b/src/plugins/home/config.ts @@ -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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + disableWelcomeScreen: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index ca05c8b5f760e7..114d442b40943b 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -17,6 +17,8 @@ * under the License. */ +import { PluginInitializerContext } from 'kibana/public'; + export { FeatureCatalogueSetup, FeatureCatalogueStart, @@ -26,4 +28,5 @@ export { export { FeatureCatalogueEntry, FeatureCatalogueCategory, Environment } from './services'; import { HomePublicPlugin } from './plugin'; -export const plugin = () => new HomePublicPlugin(); +export const plugin = (initializerContext: PluginInitializerContext) => + new HomePublicPlugin(initializerContext); diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 34502d7d2c6cd8..fa44a110c63b70 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -19,6 +19,9 @@ import { registryMock, environmentMock } from './plugin.test.mocks'; import { HomePublicPlugin } from './plugin'; +import { coreMock } from '../../../core/public/mocks'; + +const mockInitializerContext = coreMock.createPluginInitializerContext(); describe('HomePublicPlugin', () => { beforeEach(() => { @@ -30,13 +33,13 @@ describe('HomePublicPlugin', () => { describe('setup', () => { test('wires up and returns registry', async () => { - const setup = await new HomePublicPlugin().setup(); + const setup = await new HomePublicPlugin(mockInitializerContext).setup(); expect(setup).toHaveProperty('featureCatalogue'); expect(setup.featureCatalogue).toHaveProperty('register'); }); test('wires up and returns environment service', async () => { - const setup = await new HomePublicPlugin().setup(); + const setup = await new HomePublicPlugin(mockInitializerContext).setup(); expect(setup).toHaveProperty('environment'); expect(setup.environment).toHaveProperty('update'); }); @@ -44,7 +47,7 @@ describe('HomePublicPlugin', () => { describe('start', () => { test('wires up and returns registry', async () => { - const service = new HomePublicPlugin(); + const service = new HomePublicPlugin(mockInitializerContext); await service.setup(); const core = { application: { capabilities: { catalogue: {} } } } as any; const start = await service.start(core); @@ -55,7 +58,7 @@ describe('HomePublicPlugin', () => { }); test('wires up and returns environment service', async () => { - const service = new HomePublicPlugin(); + const service = new HomePublicPlugin(mockInitializerContext); await service.setup(); const start = await service.start({ application: { capabilities: { catalogue: {} } }, diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index 39a7f23826900b..fe68dbc3e7e490 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -17,7 +17,8 @@ * under the License. */ -import { CoreStart, Plugin } from 'src/core/public'; +import { CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; + import { EnvironmentService, EnvironmentServiceSetup, @@ -26,19 +27,23 @@ import { FeatureCatalogueRegistrySetup, FeatureCatalogueRegistryStart, } from './services'; +import { ConfigSchema } from '../config'; export class HomePublicPlugin implements Plugin { private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry(); private readonly environmentService = new EnvironmentService(); - public async setup() { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(): HomePublicPluginSetup { return { featureCatalogue: { ...this.featuresCatalogueRegistry.setup() }, environment: { ...this.environmentService.setup() }, + config: this.initializerContext.config.get(), }; } - public async start(core: CoreStart) { + public start(core: CoreStart): HomePublicPluginStart { return { featureCatalogue: { ...this.featuresCatalogueRegistry.start({ @@ -71,6 +76,7 @@ export interface HomePublicPluginSetup { * @deprecated */ environment: EnvironmentSetup; + config: ConfigSchema; } /** @public */ diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts index 0961c729698b9d..02f4c91a414cc9 100644 --- a/src/plugins/home/server/index.ts +++ b/src/plugins/home/server/index.ts @@ -20,8 +20,19 @@ export { HomeServerPluginSetup, HomeServerPluginStart } from './plugin'; export { TutorialProvider } from './services'; export { SampleDatasetProvider, SampleDataRegistrySetup } from './services'; -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { HomeServerPlugin } from './plugin'; +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + disableWelcomeScreen: true, + }, + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('kibana.disableWelcomeScreen', 'home.disableWelcomeScreen'), + ], +}; export const plugin = (initContext: PluginInitializerContext) => new HomeServerPlugin(initContext); diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index 23c236764cddcf..d2f2d7041024e3 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { TutorialsRegistry, TutorialsRegistrySetup, diff --git a/src/legacy/ui/public/collapsible_sidebar/index.js b/src/plugins/kibana_legacy/config.ts similarity index 78% rename from src/legacy/ui/public/collapsible_sidebar/index.js rename to src/plugins/kibana_legacy/config.ts index 9f818922f71360..291f8813ecfb91 100644 --- a/src/legacy/ui/public/collapsible_sidebar/index.js +++ b/src/plugins/kibana_legacy/config.ts @@ -17,4 +17,10 @@ * under the License. */ -import './collapsible_sidebar'; +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + defaultAppId: schema.string({ defaultValue: 'home' }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json index 26ee6db3ba06ae..b6d11309a4f964 100644 --- a/src/plugins/kibana_legacy/kibana.json +++ b/src/plugins/kibana_legacy/kibana.json @@ -1,6 +1,6 @@ { "id": "kibana_legacy", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 4cb30be8917acd..de8788808e74c6 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -20,8 +20,7 @@ import { PluginInitializerContext } from 'kibana/public'; import { KibanaLegacyPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new KibanaLegacyPlugin(); -} +export const plugin = (initializerContext: PluginInitializerContext) => + new KibanaLegacyPlugin(initializerContext); export * from './plugin'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts new file mode 100644 index 00000000000000..b6287dd9d9a556 --- /dev/null +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KibanaLegacyPlugin } from './plugin'; + +export type Setup = jest.Mocked>; +export type Start = jest.Mocked>; + +const createSetupContract = (): Setup => ({ + forwardApp: jest.fn(), + registerLegacyApp: jest.fn(), + config: { + defaultAppId: 'home', + }, +}); + +const createStartContract = (): Start => ({ + getApps: jest.fn(), + getForwards: jest.fn(), + config: { + defaultAppId: 'home', + }, +}); + +export const kibanaLegacyPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index cb95088320d7b3..b9a61a1c9b2000 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -17,7 +17,9 @@ * under the License. */ -import { App } from 'kibana/public'; +import { App, PluginInitializerContext } from 'kibana/public'; + +import { ConfigSchema } from '../config'; interface ForwardDefinition { legacyAppId: string; @@ -29,6 +31,8 @@ export class KibanaLegacyPlugin { private apps: App[] = []; private forwards: ForwardDefinition[] = []; + constructor(private readonly initializerContext: PluginInitializerContext) {} + public setup() { return { /** @@ -77,6 +81,8 @@ export class KibanaLegacyPlugin { ) => { this.forwards.push({ legacyAppId, newAppId, ...options }); }, + + config: this.initializerContext.config.get(), }; } @@ -92,6 +98,7 @@ export class KibanaLegacyPlugin { * Just exported for wiring up with legacy platform, should not be used. */ getForwards: () => this.forwards, + config: this.initializerContext.config.get(), }; } } diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts new file mode 100644 index 00000000000000..4d0fe8364a66c5 --- /dev/null +++ b/src/plugins/kibana_legacy/server/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { CoreSetup, CoreStart, PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + defaultAppId: true, + }, + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + // TODO: Remove deprecation once defaultAppId is deleted + renameFromRoot('kibana.defaultAppId', 'kibana_legacy.defaultAppId', true), + ], +}; + +class Plugin { + public setup(core: CoreSetup) {} + + public start(core: CoreStart) {} +} + +export const plugin = () => new Plugin(); 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 18f84f41d5d998..307c0760de7ba4 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 @@ -8,7 +8,6 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` diff --git a/src/plugins/kibana_utils/public/core/create_getter_setter.ts b/src/plugins/kibana_utils/common/create_getter_setter.ts similarity index 100% rename from src/plugins/kibana_utils/public/core/create_getter_setter.ts rename to src/plugins/kibana_utils/common/create_getter_setter.ts diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index bfb45b88964d89..444c10194a8e3e 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -19,4 +19,6 @@ export * from './defer'; export * from './of'; +export * from './state_containers'; +export { createGetterSetter, Get, Set } from './create_getter_setter'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.test.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container.test.ts diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts similarity index 86% rename from src/plugins/kibana_utils/public/state_containers/create_state_container.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container.ts index d420aec30f068c..78bfc0c3e90906 100644 --- a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts @@ -19,7 +19,6 @@ import { BehaviorSubject } from 'rxjs'; import { skip } from 'rxjs/operators'; -import { RecursiveReadonly } from '@kbn/utility-types'; import deepFreeze from 'deep-freeze-strict'; import { PureTransitionsToTransitions, @@ -32,14 +31,18 @@ import { const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable'; const $$setActionType = '@@SET'; -const freeze: (value: T) => RecursiveReadonly = - process.env.NODE_ENV !== 'production' - ? (value: T): RecursiveReadonly => { - const isFreezable = value !== null && typeof value === 'object'; - if (isFreezable) return deepFreeze(value) as RecursiveReadonly; - return value as RecursiveReadonly; - } - : (value: T) => value as RecursiveReadonly; +const isProduction = + typeof window === 'object' + ? process.env.NODE_ENV === 'production' + : !process.env.NODE_ENV || process.env.NODE_ENV === 'production'; + +const freeze: (value: T) => T = isProduction + ? (value: T) => value as T + : (value: T): T => { + const isFreezable = value !== null && typeof value === 'object'; + if (isFreezable) return deepFreeze(value) as T; + return value as T; + }; export function createStateContainer( defaultState: State @@ -66,7 +69,7 @@ export function createStateContainer< pureTransitions: PureTransitions = {} as PureTransitions, pureSelectors: PureSelectors = {} as PureSelectors ): ReduxLikeStateContainer { - const data$ = new BehaviorSubject>(freeze(defaultState)); + const data$ = new BehaviorSubject(freeze(defaultState)); const state$ = data$.pipe(skip(1)); const get = () => data$.getValue(); const container: ReduxLikeStateContainer = { @@ -101,7 +104,7 @@ export function createStateContainer< ), addMiddleware: middleware => (container.dispatch = middleware(container as any)(container.dispatch)), - subscribe: (listener: (state: RecursiveReadonly) => void) => { + subscribe: (listener: (state: State) => void) => { const subscription = state$.subscribe(listener); return () => subscription.unsubscribe(); }, diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx rename to src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts diff --git a/src/plugins/kibana_utils/public/state_containers/index.ts b/src/plugins/kibana_utils/common/state_containers/index.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/index.ts rename to src/plugins/kibana_utils/common/state_containers/index.ts diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/common/state_containers/types.ts similarity index 88% rename from src/plugins/kibana_utils/public/state_containers/types.ts rename to src/plugins/kibana_utils/common/state_containers/types.ts index 5f27a3d2c1dcad..26a29bc470e8ac 100644 --- a/src/plugins/kibana_utils/public/state_containers/types.ts +++ b/src/plugins/kibana_utils/common/state_containers/types.ts @@ -18,7 +18,7 @@ */ import { Observable } from 'rxjs'; -import { Ensure, RecursiveReadonly } from '@kbn/utility-types'; +import { Ensure } from '@kbn/utility-types'; export type BaseState = object; export interface TransitionDescription { @@ -27,7 +27,7 @@ export interface TransitionDescription = (...args: Args) => State; export type PureTransition = ( - state: RecursiveReadonly + state: State ) => Transition; export type EnsurePureTransition = Ensure>; export type PureTransitionToTransition> = ReturnType; @@ -36,9 +36,9 @@ export type PureTransitionsToTransitions = { }; export interface BaseStateContainer { - get: () => RecursiveReadonly; + get: () => State; set: (state: State) => void; - state$: Observable>; + state$: Observable; } export interface StateContainer< @@ -55,12 +55,12 @@ export interface ReduxLikeStateContainer< PureTransitions extends object = {}, PureSelectors extends object = {} > extends StateContainer { - getState: () => RecursiveReadonly; - reducer: Reducer>; - replaceReducer: (nextReducer: Reducer>) => void; + getState: () => State; + reducer: Reducer; + replaceReducer: (nextReducer: Reducer) => void; dispatch: (action: TransitionDescription) => void; - addMiddleware: (middleware: Middleware>) => void; - subscribe: (listener: (state: RecursiveReadonly) => void) => () => void; + addMiddleware: (middleware: Middleware) => void; + subscribe: (listener: (state: State) => void) => () => void; } export type Dispatch = (action: T) => void; diff --git a/src/plugins/kibana_utils/demos/state_containers/counter.ts b/src/plugins/kibana_utils/demos/state_containers/counter.ts index 4ddf532c1506db..0484a906a60d3b 100644 --- a/src/plugins/kibana_utils/demos/state_containers/counter.ts +++ b/src/plugins/kibana_utils/demos/state_containers/counter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createStateContainer } from '../../public/state_containers'; +import { createStateContainer } from '../../common/state_containers'; interface State { count: number; diff --git a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts index e807783a56f319..0a07d721479b3b 100644 --- a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts +++ b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createStateContainer, PureTransition } from '../../public/state_containers'; +import { createStateContainer, PureTransition } from '../../common/state_containers'; export interface TodoItem { text: string; diff --git a/src/plugins/kibana_utils/demos/state_sync/url.ts b/src/plugins/kibana_utils/demos/state_sync/url.ts index 2c426cae6733a4..80c016950d2242 100644 --- a/src/plugins/kibana_utils/demos/state_sync/url.ts +++ b/src/plugins/kibana_utils/demos/state_sync/url.ts @@ -18,7 +18,7 @@ */ import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc'; -import { BaseState, BaseStateContainer, createStateContainer } from '../../public/state_containers'; +import { BaseState, BaseStateContainer, createStateContainer } from '../../common/state_containers'; import { createKbnUrlStateStorage, syncState, diff --git a/src/plugins/kibana_utils/public/core/create_kibana_utils_core.ts b/src/plugins/kibana_utils/public/core/create_kibana_utils_core.ts index 84ecffa1da6342..c528c68f29edb6 100644 --- a/src/plugins/kibana_utils/public/core/create_kibana_utils_core.ts +++ b/src/plugins/kibana_utils/public/core/create_kibana_utils_core.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter, Get, Set } from './create_getter_setter'; +import { createGetterSetter, Get, Set } from '../../common'; import { CoreStart } from '../../../../core/public'; import { KUSavedObjectClient, createSavedObjectsClient } from './saved_objects_client'; diff --git a/src/plugins/kibana_utils/public/core/index.ts b/src/plugins/kibana_utils/public/core/index.ts index 7e8dff7191fe8e..3f08d591300a2e 100644 --- a/src/plugins/kibana_utils/public/core/index.ts +++ b/src/plugins/kibana_utils/public/core/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './create_getter_setter'; export * from './create_kibana_utils_core'; diff --git a/src/plugins/kibana_utils/public/core/saved_objects_client.ts b/src/plugins/kibana_utils/public/core/saved_objects_client.ts index 40407fea5d189f..5262755a6a3abe 100644 --- a/src/plugins/kibana_utils/public/core/saved_objects_client.ts +++ b/src/plugins/kibana_utils/public/core/saved_objects_client.ts @@ -18,7 +18,7 @@ */ import { CoreStart } from '../../../../core/public'; -import { Get } from './create_getter_setter'; +import { Get } from '../../common'; type CoreSavedObjectClient = CoreStart['savedObjects']['client']; diff --git a/src/plugins/kibana_utils/public/core/state.ts b/src/plugins/kibana_utils/public/core/state.ts index 8ac6e4e0e58e6d..52c4d166d737ef 100644 --- a/src/plugins/kibana_utils/public/core/state.ts +++ b/src/plugins/kibana_utils/public/core/state.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from './create_getter_setter'; +import { createGetterSetter } from '../../common'; import { CoreStart } from '../../../../core/public'; export const [getCoreStart, setCoreStart] = createGetterSetter('CoreStart'); diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 00c1c95028b4dc..5b6d304e14c2e0 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -export { defer, Defer, of } from '../common'; +export { defer, Defer, of, createGetterSetter, Get, Set } from '../common'; export * from './core'; export * from './errors'; export * from './field_mapping'; @@ -25,7 +25,7 @@ export * from './field_wildcard'; export * from './parse'; export * from './render_complete'; export * from './resize_checker'; -export * from './state_containers'; +export * from '../common/state_containers'; export * from './storage'; export { hashedItemStore, HashedItemStore } from './storage/hashed_item_store'; export { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index 17f41483a0a21e..c55c60f9b0f890 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BaseState, BaseStateContainer, createStateContainer } from '../state_containers'; +import { BaseState, BaseStateContainer, createStateContainer } from '../../common/state_containers'; import { defaultState, pureTransitions, diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts index 28d133829e07c4..ed57723f8f2b7b 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -23,7 +23,7 @@ import defaultComparator from 'fast-deep-equal'; import { IStateSyncConfig } from './types'; import { IStateStorage } from './state_sync_state_storage'; import { distinctUntilChangedWithInitialValue } from '../../common'; -import { BaseState } from '../state_containers'; +import { BaseState } from '../../common/state_containers'; import { applyDiff } from '../state_management/utils/diff_object'; /** diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts index 3009c1d161a532..2acb466d92e920 100644 --- a/src/plugins/kibana_utils/public/state_sync/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BaseState, BaseStateContainer } from '../state_containers/types'; +import { BaseState, BaseStateContainer } from '../../common/state_containers/types'; import { IStateStorage } from './state_sync_state_storage'; export interface INullableBaseStateContainer diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts b/src/plugins/kibana_utils/server/index.ts similarity index 93% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts rename to src/plugins/kibana_utils/server/index.ts index 4558621dc6615a..f8b79a1b8b339d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts +++ b/src/plugins/kibana_utils/server/index.ts @@ -17,5 +17,4 @@ * under the License. */ -// @ts-ignore -export * from './vis_filters'; +export { Get, Set, createGetterSetter } from '../common'; diff --git a/src/plugins/management/public/management_service.test.ts b/src/plugins/management/public/management_service.test.ts index 854406a10335be..b34e76474cec23 100644 --- a/src/plugins/management/public/management_service.test.ts +++ b/src/plugins/management/public/management_service.test.ts @@ -19,12 +19,13 @@ import { ManagementService } from './management_service'; import { coreMock } from '../../../core/public/mocks'; +import { npSetup } from '../../../legacy/ui/public/new_platform/__mocks__'; -const mockKibanaLegacy = { registerLegacyApp: () => {}, forwardApp: () => {} }; +jest.mock('ui/new_platform'); test('Provides default sections', () => { const service = new ManagementService().setup( - mockKibanaLegacy, + npSetup.plugins.kibana_legacy, () => {}, coreMock.createSetup().getStartServices ); @@ -36,7 +37,7 @@ test('Provides default sections', () => { test('Register section, enable and disable', () => { const service = new ManagementService().setup( - mockKibanaLegacy, + npSetup.plugins.kibana_legacy, () => {}, coreMock.createSetup().getStartServices ); diff --git a/test/common/config.js b/test/common/config.js index 29d4bbf10a6cee..faf8cef0271709 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -54,7 +54,7 @@ export default function() { `--elasticsearch.hosts=${formatUrl(servers.elasticsearch)}`, `--elasticsearch.username=${kibanaServerTestUser.username}`, `--elasticsearch.password=${kibanaServerTestUser.password}`, - `--kibana.disableWelcomeScreen=true`, + `--home.disableWelcomeScreen=true`, '--telemetry.banner=false', `--server.maxPayloadBytes=1679958`, // newsfeed mock service diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index d96febee7b06d2..d320b57ee59e66 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0", "react-dom": "^16.12.0" } diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts index 657d8d5150c3a8..a700727d87299d 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/public/np_ready/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public/core'; +import { createGetterSetter } from '../../../../../../src/plugins/kibana_utils/public'; import { ExpressionsStart } from './types'; export const [getExpressions, setExpressions] = createGetterSetter('Expressions'); diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 87f3173d560247..56c055b8b1cff8 100644 Binary files a/test/interpreter_functional/screenshots/baseline/combined_test.png and b/test/interpreter_functional/screenshots/baseline/combined_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index d860bb73521cea..753ab2c2c6e949 100644 Binary files a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png and b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_all_data.png b/test/interpreter_functional/screenshots/baseline/metric_all_data.png index 43943b1e6c46ce..44226877bdc5af 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_all_data.png and b/test/interpreter_functional/screenshots/baseline/metric_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index 795f2f7c832f33..e0cffd065fc4ad 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index 580889bb7deaf3..14457f0a4d0ab7 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png and b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png differ diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index 916a284433874a..c4fc4d3979152a 100644 Binary files a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png and b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_1.png b/test/interpreter_functional/screenshots/baseline/partial_test_1.png index 9815f25d00b165..51998d019c66c0 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_1.png and b/test/interpreter_functional/screenshots/baseline/partial_test_1.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_2.png b/test/interpreter_functional/screenshots/baseline/partial_test_2.png index 87f3173d560247..56c055b8b1cff8 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_2.png and b/test/interpreter_functional/screenshots/baseline/partial_test_2.png differ diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index ee9182a654d1e8..7b96f3ec43c7ed 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_3.png and b/test/interpreter_functional/screenshots/baseline/partial_test_3.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index 03ffc7ac7b1a58..a7088de3849a54 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 3a7315df405dfe..8f93ba81ad2adc 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png index 795f2f7c832f33..e0cffd065fc4ad 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_invalid_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 5cdce692966736..98890b9687ac95 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png differ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index 394b5585097a24..479280f598aef7 100644 Binary files a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png and b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png differ diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index 26f111e9edcf92..9b0122c1574812 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 7f64f978451915..2d6e756a7f0a3a 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index e171a65be8bab5..37c6885d76cb08 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index ed8b0b258fd90f..60a0e450906a27 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 8a349aa5df0600..6b2f93b47c0b28 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index c1e429508c37ff..4241d6f208bfd4 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 1325c7fbed03ea..ae1e817424cb10 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 2b063b518665af..c0da4794728807 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 6152fd406961fc..c5fbcd63b06854 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index e4c6b09a264ddb..b67b0744494034 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index 26f111e9edcf92..9b0122c1574812 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 7f64f978451915..2d6e756a7f0a3a 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index e171a65be8bab5..37c6885d76cb08 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index ed8b0b258fd90f..60a0e450906a27 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 8a349aa5df0600..6b2f93b47c0b28 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index c1e429508c37ff..4241d6f208bfd4 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 1325c7fbed03ea..ae1e817424cb10 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index 2b063b518665af..c0da4794728807 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 6152fd406961fc..c5fbcd63b06854 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index e4c6b09a264ddb..b67b0744494034 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 170cc77ca37cc1..27a8c1fab6c8e8 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 85c76071d1e944..51bb7240dd7c44 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index ade93c9f500990..9ee0e3de51d8b5 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 32ff4908629ebe..50348a74f07254 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -18,7 +18,7 @@ "xpack.idxMgmt": "legacy/plugins/index_management", "xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management", "xpack.infra": "legacy/plugins/infra", - "xpack.kueryAutocomplete": "legacy/plugins/kuery_autocomplete", + "xpack.data": "plugins/data_enhanced", "xpack.lens": "legacy/plugins/lens", "xpack.licenseMgmt": "legacy/plugins/license_management", "xpack.licensing": "plugins/licensing", diff --git a/x-pack/index.js b/x-pack/index.js index 9ab6e2ee322d90..e2d25ce87b26b4 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -23,7 +23,6 @@ import { licenseManagement } from './legacy/plugins/license_management'; import { indexManagement } from './legacy/plugins/index_management'; import { indexLifecycleManagement } from './legacy/plugins/index_lifecycle_management'; import { spaces } from './legacy/plugins/spaces'; -import { kueryAutocompleteInitializer } from './legacy/plugins/kuery_autocomplete'; import { canvas } from './legacy/plugins/canvas'; import { infra } from './legacy/plugins/infra'; import { taskManager } from './legacy/plugins/task_manager'; @@ -65,7 +64,6 @@ module.exports = function(kibana) { licenseManagement(kibana), indexManagement(kibana), indexLifecycleManagement(kibana), - kueryAutocompleteInitializer(kibana), infra(kibana), taskManager(kibana), rollup(kibana), diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts index 9b8cfe9b69ed04..a5d9f6fb6e6a60 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.test.ts @@ -1436,97 +1436,120 @@ describe('find()', () => { }); describe('delete()', () => { - test('successfully removes an alert', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', - attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - scheduledTaskId: 'task-123', - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], + let alertsClient: AlertsClient; + const existingAlert = { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, }, - references: [ + scheduledTaskId: 'task-123', + actions: [ { - name: 'action_0', - type: 'action', - id: '1', + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, }, ], - }); - savedObjectsClient.delete.mockResolvedValueOnce({ + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }; + + beforeEach(() => { + alertsClient = new AlertsClient(alertsClientParams); + savedObjectsClient.get.mockResolvedValue(existingAlert); + savedObjectsClient.delete.mockResolvedValue({ success: true, }); + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + }); + }); + + test('successfully removes an alert', async () => { const result = await alertsClient.delete({ id: '1' }); expect(result).toEqual({ success: true }); - expect(savedObjectsClient.delete).toHaveBeenCalledTimes(1); - expect(savedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "alert", - "1", - ] - `); - expect(taskManager.remove).toHaveBeenCalledTimes(1); - expect(taskManager.remove.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "task-123", - ] - `); + expect(savedObjectsClient.delete).toHaveBeenCalledWith('alert', '1'); + expect(taskManager.remove).toHaveBeenCalledWith('task-123'); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); }); - test('swallows error when invalidate API key throws', async () => { - const alertsClient = new AlertsClient(alertsClientParams); - alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); - encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ - id: '1', - type: 'alert', + test(`doesn't remove a task when scheduledTaskId is null`, async () => { + savedObjectsClient.get.mockResolvedValue({ + ...existingAlert, attributes: { - alertTypeId: '123', - schedule: { interval: '10s' }, - params: { - bar: true, - }, - apiKey: Buffer.from('123:abc').toString('base64'), - scheduledTaskId: 'task-123', - actions: [ - { - group: 'default', - actionRef: 'action_0', - params: { - foo: true, - }, - }, - ], + ...existingAlert.attributes, + scheduledTaskId: null, }, - references: [ - { - name: 'action_0', - type: 'action', - id: '1', - }, - ], }); - savedObjectsClient.delete.mockResolvedValueOnce({ - success: true, + + await alertsClient.delete({ id: '1' }); + expect(taskManager.remove).not.toHaveBeenCalled(); + }); + + test(`doesn't invalidate API key when apiKey is null`, async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({ + ...existingAlert, + attributes: { + ...existingAlert.attributes, + apiKey: null, + }, }); await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + }); + + test('swallows error when invalidate API key throws', async () => { + alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail')); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' }); expect(alertsClientParams.logger.error).toHaveBeenCalledWith( 'Failed to invalidate API Key: Fail' ); }); + + test('swallows error when getDecryptedAsInternalUser throws an error', async () => { + encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); + + await alertsClient.delete({ id: '1' }); + expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled(); + expect(alertsClientParams.logger.error).toHaveBeenCalledWith( + 'delete(): Failed to load API key to invalidate on alert 1: Fail' + ); + }); + + test('throws error when savedObjectsClient.get throws an error', async () => { + savedObjectsClient.get.mockRejectedValue(new Error('SOC Fail')); + + await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"SOC Fail"` + ); + }); + + test('throws error when taskManager.remove throws an error', async () => { + taskManager.remove.mockRejectedValue(new Error('TM Fail')); + + await expect(alertsClient.delete({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"TM Fail"` + ); + }); }); describe('update()', () => { diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index a6ba936b76570e..12d60b162d58e2 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -29,7 +29,7 @@ import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; type NormalizedAlertAction = Omit; @@ -45,7 +45,7 @@ interface ConstructorOptions { taskManager: TaskManagerStartContract; savedObjectsClient: SavedObjectsClientContract; alertTypeRegistry: AlertTypeRegistry; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceId?: string; namespace?: string; getUserName: () => Promise; @@ -120,7 +120,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor({ alertTypeRegistry, @@ -226,14 +226,29 @@ export class AlertsClient { } public async delete({ id }: { id: string }) { - const decryptedAlertSavedObject = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser< - RawAlert - >('alert', id, { namespace: this.namespace }); + const [taskIdToRemove, apiKeyToInvalidate] = await Promise.all([ + this.savedObjectsClient + .get('alert', id) + .then(result => result.attributes.scheduledTaskId), + // We'll try and load the decrypted saved object but if this fails we'll only log + // and skip invalidating the API key. + this.encryptedSavedObjectsPlugin + .getDecryptedAsInternalUser('alert', id, { namespace: this.namespace }) + .then(result => result.attributes.apiKey) + .catch(e => + this.logger.error( + `delete(): Failed to load API key to invalidate on alert ${id}: ${e.message}` + ) + ), + ]); + const removeResult = await this.savedObjectsClient.delete('alert', id); - if (decryptedAlertSavedObject.attributes.scheduledTaskId) { - await this.taskManager.remove(decryptedAlertSavedObject.attributes.scheduledTaskId); - } - await this.invalidateApiKey({ apiKey: decryptedAlertSavedObject.attributes.apiKey }); + + await Promise.all([ + taskIdToRemove && this.taskManager.remove(taskIdToRemove), + apiKeyToInvalidate && this.invalidateApiKey({ apiKey: apiKeyToInvalidate }), + ]); + return removeResult; } diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts index eab1cc3ce627b4..de789fba0ac388 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts @@ -11,7 +11,7 @@ import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { SecurityPluginStartContract } from './shim'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { InvalidateAPIKeyParams } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; export interface ConstructorOpts { @@ -21,7 +21,7 @@ export interface ConstructorOpts { securityPluginSetup?: SecurityPluginStartContract; getSpaceId: (request: Hapi.Request) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; } export class AlertsClientFactory { @@ -31,7 +31,7 @@ export class AlertsClientFactory { private readonly securityPluginSetup?: SecurityPluginStartContract; private readonly getSpaceId: (request: Hapi.Request) => string | undefined; private readonly spaceIdToNamespace: SpaceIdToNamespaceFunction; - private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor(options: ConstructorOpts) { this.logger = options.logger; diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index 80d01ea722926f..bc8b0eb863634c 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -15,10 +15,10 @@ import { getTaskManagerSetup, getTaskManagerStart } from '../../task_manager/ser import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import KbnServer from '../../../../../src/legacy/server/kbn_server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { CoreSetup, LoggerFactory, @@ -44,8 +44,8 @@ export interface Server extends Legacy.Server { /** * Shim what we're thinking setup and start contracts will look like */ -export type SecurityPluginSetupContract = Pick; -export type SecurityPluginStartContract = Pick; +export type SecurityPluginSetupContract = Pick; +export type SecurityPluginStartContract = Pick; export type XPackMainPluginSetupContract = Pick; /** @@ -71,14 +71,14 @@ export interface AlertingPluginsSetup { taskManager: TaskManagerSetupContract; actions: ActionsPluginSetupContract; xpack_main: XPackMainPluginSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; } export interface AlertingPluginsStart { actions: ActionsPluginStartContract; security?: SecurityPluginStartContract; spaces: () => SpacesPluginStartContract | undefined; - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } @@ -120,7 +120,7 @@ export function shim( actions: newPlatform.setup.plugins.actions as ActionsPluginSetupContract, xpack_main: server.plugins.xpack_main, encryptedSavedObjects: newPlatform.setup.plugins - .encryptedSavedObjects as EncryptedSavedObjectsSetupContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup, licensing: newPlatform.setup.plugins.licensing as LicensingPluginSetup, }; @@ -131,7 +131,7 @@ export function shim( // initializes after this function is called spaces: () => server.plugins.spaces, encryptedSavedObjects: newPlatform.start.plugins - .encryptedSavedObjects as EncryptedSavedObjectsStartContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart, taskManager: getTaskManagerStart(server)!, }; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts index 67fef33b69c6dd..d2ecfb64c8a813 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -5,7 +5,7 @@ */ import { Logger } from '../../../../../../src/core/server'; import { RunContext } from '../../../../../plugins/task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../plugins/encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../../../plugins/actions/server'; import { AlertType, @@ -19,7 +19,7 @@ export interface TaskRunnerContext { logger: Logger; getServices: GetServicesFunction; executeAction: ActionsPluginStartContract['execute']; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap index b2c503806c3854..260d7de3aefd4b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap @@ -62,11 +62,7 @@ exports[`DetailView should render TabContent 1`] = ` `; exports[`DetailView should render tabs 1`] = ` - + List should render empty state 1`] = ` } initialPageSize={25} initialSortDirection="desc" - initialSortField="latestOccurrenceAt" + initialSortField="occurrenceCount" items={Array []} noItemsMessage="No errors were found" sortItems={false} @@ -190,7 +190,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` List should render empty state 1`] = ` } >
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap index 8a8799207ace81..73d7599a603590 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap @@ -65,7 +65,6 @@ exports[` can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -85,7 +84,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -110,7 +108,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -130,7 +127,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -219,7 +215,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -239,7 +234,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -264,7 +258,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -284,7 +277,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -317,7 +309,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z" fill-rule="nonzero" @@ -366,7 +357,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z" /> @@ -381,7 +371,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fill-rule="evenodd" @@ -561,7 +550,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -581,7 +569,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -606,7 +593,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -626,7 +612,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -715,7 +700,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -735,7 +719,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -760,7 +743,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -780,7 +762,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -813,7 +794,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z" fill-rule="nonzero" @@ -871,7 +851,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z" /> @@ -886,7 +865,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fill-rule="evenodd" @@ -927,4 +905,4 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = </div> `; -exports[`<Settings /> can navigate Toolbar Settings, closes when activated 3`] = `"<div><div><div data-focus-guard=\\"true\\" tabindex=\\"-1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"-1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"disabled\\"><div class=\\"euiPanel euiPopover__panel euiPopover__panel--top euiPopover__panel-withTitle\\" aria-live=\\"assertive\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-describedby=\\"generated-id\\" style=\\"top: -16px; left: -22px; z-index: 2000;\\"><div class=\\"euiPopover__panelArrow euiPopover__panelArrow--top\\" style=\\"left: 10px; top: 0px;\\"></div><div><div class=\\"euiContextMenu\\" style=\\"height: 0px;\\"><div class=\\"euiContextMenuPanel euiContextMenu__panel euiContextMenuPanel-txOutLeft\\" tabindex=\\"0\\"><div class=\\"euiPopoverTitle\\"><span class=\\"euiContextMenu__itemLayout\\">Settings</span></div><div><div><button class=\\"euiContextMenuItem\\" type=\\"button\\"><span class=\\"euiContextMenu__itemLayout\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoaded euiContextMenu__icon\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>Auto Play
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index ebae49f9947239..4215f96c8de4a4 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -55,10 +55,12 @@ chrome.setRootController('kibana', function() { uiModules.get('kibana').run(showAppRedirectNotification); -// If there is a configured kbnDefaultAppId, and it is a dashboard ID, we'll -// show that dashboard, otherwise, we'll show the default dasbhoard landing page. +/** + * If there is a configured `kibana.defaultAppId`, and it is a dashboard ID, we'll + * show that dashboard, otherwise, we'll show the default dasbhoard landing page. + */ function defaultUrl() { - const defaultAppId = chrome.getInjected('kbnDefaultAppId', ''); + const defaultAppId = npStart.plugins.kibana_legacy.config.defaultAppId || ''; const isDashboardId = defaultAppId.startsWith(dashboardAppIdPrefix()); return isDashboardId ? `/${defaultAppId}` : DashboardConstants.LANDING_PAGE_PATH; } diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts index 69058a7a33f59f..ce343dba006cfc 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts @@ -6,7 +6,7 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; -import { PluginSetupContract } from '../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../../plugins/encrypted_saved_objects/server'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; @@ -29,7 +29,7 @@ export const encryptedSavedObjects = (kibana: { init(server: Legacy.Server) { const encryptedSavedObjectsPlugin = (server.newPlatform.setup.plugins - .encryptedSavedObjects as unknown) as PluginSetupContract; + .encryptedSavedObjects as unknown) as EncryptedSavedObjectsPluginSetup; if (!encryptedSavedObjectsPlugin) { throw new Error('New Platform XPack EncryptedSavedObjects plugin is not available.'); } diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js index e71820a346f918..7534f3cd0934ee 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; export function checkLicense(xpackLicenseInfo) { - const pluginName = 'Index Management'; + const pluginName = 'Index Lifecycle Policies'; // If, for some reason, we cannot get the license information // from Elasticsearch, assume worst case and disable diff --git a/x-pack/legacy/plugins/kuery_autocomplete/index.ts b/x-pack/legacy/plugins/kuery_autocomplete/index.ts deleted file mode 100644 index 5f9d74ed799e6e..00000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/index.ts +++ /dev/null @@ -1,22 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -export const kueryAutocompleteInitializer: LegacyPluginInitializer = ({ - Plugin, -}: LegacyPluginApi) => - new Plugin({ - id: 'kuery_autocomplete', - publicDir: resolve(__dirname, 'public'), - uiExports: { - hacks: [resolve(__dirname, 'public/legacy')], - }, - init: (server: Legacy.Server) => ({}), - } as Legacy.PluginSpecOptions); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts deleted file mode 100644 index 303fe8c557fbd2..00000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/legacy.ts +++ /dev/null @@ -1,27 +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 { PluginInitializerContext } from 'src/core/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { plugin } from './index'; -import { - KueryAutocompletePluginSetupDependencies, - KueryAutocompletePluginStartDependencies, -} from './plugin'; - -const pluginsSetup: Readonly = { - data: npSetup.plugins.data, -}; - -const pluginsStart: Readonly = { - data: npStart.plugins.data, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, pluginsSetup); -export const start = pluginInstance.start(npStart.core, pluginsStart); diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts b/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts deleted file mode 100644 index 81737c46365327..00000000000000 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/plugin.ts +++ /dev/null @@ -1,41 +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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { Plugin as DataPublicPlugin } from '../../../../../src/plugins/data/public'; -import { setAutocompleteService } from './services'; -import { setupKqlQuerySuggestionProvider } from './kql_query_suggestion'; - -/** @internal */ -export interface KueryAutocompletePluginSetupDependencies { - data: ReturnType; -} - -/** @internal */ -export interface KueryAutocompletePluginStartDependencies { - data: ReturnType; -} - -const KUERY_LANGUAGE_NAME = 'kuery'; - -/** @internal */ -export class KueryAutocompletePlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public async setup(core: CoreSetup, plugins: KueryAutocompletePluginSetupDependencies) { - const kueryProvider = setupKqlQuerySuggestionProvider(core); - - plugins.data.autocomplete.addQuerySuggestionProvider(KUERY_LANGUAGE_NAME, kueryProvider); - } - - public start(core: CoreStart, plugins: KueryAutocompletePluginStartDependencies) { - setAutocompleteService(plugins.data.autocomplete); - } -} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 80a7ceb61c324c..99926c646da223 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -60,10 +60,14 @@ function createMockFilterManager() { return unsubscribe; }, }), - setFilters: (newFilters: unknown[]) => { + setFilters: jest.fn((newFilters: unknown[]) => { filters = newFilters; - subscriber(); - }, + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), getFilters: () => filters, getGlobalFilters: () => { // @ts-ignore @@ -189,6 +193,13 @@ describe('Lens App', () => { `); }); + it('clears app filters on load', () => { + const defaultArgs = makeDefaultArgs(); + mount(); + + expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); + }); + it('sets breadcrumbs when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); const instance = mount(); @@ -226,7 +237,7 @@ describe('Lens App', () => { expect(args.docStorage.load).not.toHaveBeenCalled(); }); - it('loads a document and uses query if there is a document id', async () => { + it('loads a document and uses query and filters if there is a document id', async () => { const args = makeDefaultArgs(); args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ @@ -234,6 +245,7 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, }); @@ -245,6 +257,9 @@ describe('Lens App', () => { expect(args.docStorage.load).toHaveBeenCalledWith('1234'); expect(args.data.indexPatterns.get).toHaveBeenCalledWith('1'); + expect(args.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ + { query: { match_phrase: { src: 'test' } } }, + ]); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', @@ -260,6 +275,7 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, }, diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 35e45af6a3d689..6d2ebee1d88dbe 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -83,6 +83,10 @@ export function App({ const { lastKnownDoc } = state; useEffect(() => { + // Clear app-specific filters when navigating to Lens. Necessary because Lens + // can be loaded without a full page refresh + data.query.filterManager.setAppFilters([]); + const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ next: () => { setState(s => ({ ...s, filters: data.query.filterManager.getFilters() })); @@ -123,13 +127,14 @@ export function App({ core.notifications ) .then(indexPatterns => { + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters(doc.state.filters); setState(s => ({ ...s, isLoading: false, persistedDoc: doc, lastKnownDoc: doc, query: doc.state.query, - filters: doc.state.filters, indexPatternsForTopNav: indexPatterns, })); }) diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9256bee4e756b5..353dc58e6d4011 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -296,7 +296,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem } > @@ -1401,7 +1399,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1 width={16} xmlns="http://www.w3.org/2000/svg" > - <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> @@ -1871,7 +1868,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> @@ -2806,7 +2802,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index 2dc355513ece24..c62b07a89e7a3e 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -33,6 +33,7 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -110,6 +111,7 @@ exports[`should not show "within" relation when filter geometry is not closed 1` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -214,6 +216,7 @@ exports[`should render error message 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -294,6 +297,7 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap index 9a55e46b40aea3..9d07b9c641e0f9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap @@ -15,6 +15,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index 2e5ea299b6f679..757750dbb0813d 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -69,7 +69,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_FILES_API_PATH}/${EMS_FILES_DEFAULT_JSON_PATH}`, handler: async request => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.query.id) { server.log('warning', 'Must supply id parameters to retrieve EMS file'); @@ -96,7 +97,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_TILE_PATH}`, handler: async (request, h) => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if ( !request.query.id || @@ -127,8 +129,9 @@ export function initRoutes(server, licenseUid) { server.route({ method: 'GET', path: `${ROOT}/${EMS_CATALOGUE_PATH}`, - handler: async () => { - checkEMSProxyConfig(); + handler: async request => { + const { server } = request; + checkEMSProxyConfig(server); const main = await emsClient.getMainManifest(); const proxiedManifest = { @@ -157,8 +160,9 @@ export function initRoutes(server, licenseUid) { server.route({ method: 'GET', path: `${ROOT}/${EMS_FILES_CATALOGUE_PATH}/{emsVersion}/manifest`, - handler: async () => { - checkEMSProxyConfig(); + handler: async request => { + const { server } = request; + checkEMSProxyConfig(server); const file = await emsClient.getDefaultFileManifest(); const layers = file.layers.map(layer => { @@ -181,8 +185,9 @@ export function initRoutes(server, licenseUid) { server.route({ method: 'GET', path: `${ROOT}/${EMS_TILES_CATALOGUE_PATH}/{emsVersion}/manifest`, - handler: async () => { - checkEMSProxyConfig(); + handler: async request => { + const { server } = request; + checkEMSProxyConfig(server); const tilesManifest = await emsClient.getDefaultTMSManifest(); const newServices = tilesManifest.services.map(service => { @@ -220,7 +225,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_RASTER_STYLE_PATH}`, handler: async request => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.query.id) { server.log('warning', 'Must supply id parameter to retrieve EMS raster style'); @@ -246,7 +252,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_STYLE_PATH}`, handler: async request => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.query.id) { server.log('warning', 'Must supply id parameter to retrieve EMS vector style'); @@ -285,7 +292,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_SOURCE_PATH}`, handler: async request => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.query.id || !request.query.sourceId) { server.log( @@ -316,7 +324,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_TILES_VECTOR_TILE_PATH}`, handler: async (request, h) => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if ( !request.query.id || @@ -352,7 +361,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_GLYPHS_PATH}/{fontstack}/{range}`, handler: async (request, h) => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); const url = mapConfig.emsFontLibraryUrl .replace('{fontstack}', request.params.fontstack) .replace('{range}', request.params.range); @@ -365,7 +375,8 @@ export function initRoutes(server, licenseUid) { method: 'GET', path: `${ROOT}/${EMS_TILES_API_PATH}/${EMS_SPRITES_PATH}/{id}/sprite{scaling?}.{extension}`, handler: async (request, h) => { - checkEMSProxyConfig(); + const { server } = request; + checkEMSProxyConfig(server); if (!request.params.id) { server.log('warning', 'Must supply id parameter to retrieve EMS vector source sprite'); @@ -443,7 +454,7 @@ export function initRoutes(server, licenseUid) { }, }); - function checkEMSProxyConfig() { + function checkEMSProxyConfig(server) { if (!mapConfig.proxyElasticMapsServiceInMaps) { server.log( 'warning', diff --git a/x-pack/legacy/plugins/ml/.gitignore b/x-pack/legacy/plugins/ml/.gitignore new file mode 100644 index 00000000000000..708c5b199467b9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/.gitignore @@ -0,0 +1 @@ +routes_doc diff --git a/x-pack/legacy/plugins/ml/server/routes/README.md b/x-pack/legacy/plugins/ml/server/routes/README.md new file mode 100644 index 00000000000000..1d08335af3d2e9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/README.md @@ -0,0 +1,16 @@ +# ML Kibana API routes + +This folder contains ML API routes in Kibana. + +Each route handler requires [apiDoc](https://github.com/apidoc/apidoc) annotations in order +to generate documentation. +The [apidoc-markdown](https://github.com/rigwild/apidoc-markdown) package is also required in order to generate the markdown. + +For now the process is pretty manual. You need to make sure the packages mentioned above are installed globally +to execute the following command from the directory in which this README file is located. +``` +apidoc -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md +``` + +It will create a new directory `routes_doc` (next to the `routes` folder) which contains the documentation in HTML format +as well as `ML_API.md` file. \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json new file mode 100644 index 00000000000000..8292e946cd344d --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -0,0 +1,21 @@ +{ + "name": "ml_kibana_api", + "version": "0.1.0", + "description": "ML Kibana API", + "title": "ML Kibana API", + "url" : "/api/ml/", + "order": [ + "DataFrameAnalytics", + "GetDataFrameAnalytics", + "GetDataFrameAnalyticsById", + "GetDataFrameAnalyticsStats", + "GetDataFrameAnalyticsStatsById", + "UpdateDataFrameAnalytics", + "EvaluateDataFrameAnalytics", + "ExplainDataFrameAnalytics", + "DeleteDataFrameAnalytics", + "StartDataFrameAnalyticsJob", + "StopsDataFrameAnalyticsJob", + "GetDataFrameAnalyticsMessages" + ] +} diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts index 7b855e5f87cbf6..67fa2fba46f1a9 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts @@ -15,7 +15,20 @@ import { dataAnalyticsExplainSchema, } from '../new_platform/data_analytics_schema'; +/** + * Routes for the data frame analytics + */ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics Get analytics data + * @apiName GetDataFrameAnalytics + * @apiDescription Returns the list of data frame analytics jobs. + * + * @apiSuccess {Number} count + * @apiSuccess {Object[]} data_frame_analytics + */ router.get( { path: '/api/ml/data_frame/analytics', @@ -35,6 +48,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId Get analytics data by id + * @apiName GetDataFrameAnalyticsById + * @apiDescription Returns the data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -57,6 +79,13 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/_stats Get analytics stats + * @apiName GetDataFrameAnalyticsStats + * @apiDescription Returns data frame analytics jobs statistics. + */ router.get( { path: '/api/ml/data_frame/analytics/_stats', @@ -76,6 +105,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId/_stats Get stats for requested analytics job + * @apiName GetDataFrameAnalyticsStatsById + * @apiDescription Returns data frame analytics job statistics. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stats', @@ -101,6 +139,16 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {put} /api/ml/data_frame/analytics/:analyticsId Instantiate a data frame analytics job + * @apiName UpdateDataFrameAnalytics + * @apiDescription This API creates a data frame analytics job that performs an analysis + * on the source index and stores the outcome in a destination index. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.put( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -130,6 +178,13 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/_evaluate Evaluate the data frame analytics for an annotated index + * @apiName EvaluateDataFrameAnalytics + * @apiDescription Evaluates the data frame analytics for an annotated index. + */ router.post( { path: '/api/ml/data_frame/_evaluate', @@ -154,6 +209,22 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/_explain Explain a data frame analytics config + * @apiName ExplainDataFrameAnalytics + * @apiDescription This API provides explanations for a data frame analytics config + * that either exists already or one that has not been created yet. + * + * @apiParam {String} [description] + * @apiParam {Object} [dest] + * @apiParam {Object} source + * @apiParam {String} source.index + * @apiParam {Object} analysis + * @apiParam {Object} [analyzed_fields] + * @apiParam {String} [model_memory_limit] + */ router.post( { path: '/api/ml/data_frame/analytics/_explain', @@ -178,6 +249,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {delete} /api/ml/data_frame/analytics/:analyticsId Delete specified analytics job + * @apiName DeleteDataFrameAnalytics + * @apiDescription Deletes specified data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.delete( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -205,6 +285,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_start Start specified analytics job + * @apiName StartDataFrameAnalyticsJob + * @apiDescription Starts a data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_start', @@ -229,6 +318,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_stop Stop specified analytics job + * @apiName StopsDataFrameAnalyticsJob + * @apiDescription Stops a data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stop', @@ -263,6 +361,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId/messages Get analytics job messages + * @apiName GetDataFrameAnalyticsMessages + * @apiDescription Returns the list of audit messages for data frame analytics jobs. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/messages', diff --git a/x-pack/legacy/plugins/monitoring/common/constants.js b/x-pack/legacy/plugins/monitoring/common/constants.ts similarity index 85% rename from x-pack/legacy/plugins/monitoring/common/constants.js rename to x-pack/legacy/plugins/monitoring/common/constants.ts index ff16b0e9c5167f..53764f592dc151 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.js +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -233,3 +233,45 @@ export const REPORTING_SYSTEM_ID = 'reporting'; * @type {Number} */ export const TELEMETRY_COLLECTION_INTERVAL = 86400000; + +/** + * We want to slowly rollout the migration from watcher-based cluster alerts to + * kibana alerts and we only want to enable the kibana alerts once all + * watcher-based cluster alerts have been migrated so this flag will serve + * as the only way to see the new UI and actually run Kibana alerts. It will + * be false until all alerts have been migrated, then it will be removed + */ +export const KIBANA_ALERTING_ENABLED = false; + +/** + * The prefix for all alert types used by monitoring + */ +export const ALERT_TYPE_PREFIX = 'monitoring_'; + +/** + * This is the alert type id for the license expiration alert + */ +export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; + +/** + * A listing of all alert types + */ +export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION]; + +/** + * Matches the id for the built-in in email action type + * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts + */ +export const ALERT_ACTION_TYPE_EMAIL = '.email'; + +/** + * The number of alerts that have been migrated + */ +export const NUMBER_OF_MIGRATED_ALERTS = 1; + +/** + * The advanced settings config name for the email address + */ +export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress'; + +export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/legacy/plugins/monitoring/deprecations.js b/x-pack/legacy/plugins/monitoring/deprecations.js index 6e35e86dd9d718..ae8650fd3b26a6 100644 --- a/x-pack/legacy/plugins/monitoring/deprecations.js +++ b/x-pack/legacy/plugins/monitoring/deprecations.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from './common/constants'; +import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY, KIBANA_ALERTING_ENABLED } from './common/constants'; /** * Re-writes deprecated user-defined config settings and logs warnings as a @@ -21,10 +21,20 @@ export const deprecations = () => { const clusterAlertsEnabled = get(settings, 'cluster_alerts.enabled'); const emailNotificationsEnabled = clusterAlertsEnabled && get(settings, 'cluster_alerts.email_notifications.enabled'); - if (emailNotificationsEnabled && !get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { - log( - `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."` - ); + if (emailNotificationsEnabled) { + if (KIBANA_ALERTING_ENABLED) { + if (get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log( + `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" is deprecated. Please configure the email adddress through the Stack Monitoring UI instead."` + ); + } + } else { + if (!get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log( + `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."` + ); + } + } } }, (settings, log) => { diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index ca595836133c2c..ade172f527dabd 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -10,15 +10,20 @@ import { deprecations } from './deprecations'; import { getUiExports } from './ui_exports'; import { Plugin } from './server/plugin'; import { initInfraSource } from './server/lib/logs/init_infra_source'; +import { KIBANA_ALERTING_ENABLED } from './common/constants'; /** * Invokes plugin modules to instantiate the Monitoring plugin for Kibana * @param kibana {Object} Kibana plugin instance * @return {Object} Monitoring UI Kibana plugin object */ +const deps = ['kibana', 'elasticsearch', 'xpack_main']; +if (KIBANA_ALERTING_ENABLED) { + deps.push(...['alerting', 'actions']); +} export const monitoring = kibana => new kibana.Plugin({ - require: ['kibana', 'elasticsearch', 'xpack_main'], + require: deps, id: 'monitoring', configPrefix: 'monitoring', publicDir: resolve(__dirname, 'public'), @@ -59,6 +64,7 @@ export const monitoring = kibana => }), injectUiAppVars: server.injectUiAppVars, log: (...args) => server.log(...args), + logger: server.newPlatform.coreContext.logger, getOSInfo: server.getOSInfo, events: { on: (...args) => server.events.on(...args), @@ -73,11 +79,13 @@ export const monitoring = kibana => xpack_main: server.plugins.xpack_main, elasticsearch: server.plugins.elasticsearch, infra: server.plugins.infra, + alerting: server.plugins.alerting, usageCollection, licensing, }; - new Plugin().setup(serverFacade, plugins); + const plugin = new Plugin(); + plugin.setup(serverFacade, plugins); }, config, deprecations, diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap new file mode 100644 index 00000000000000..4cf1f4df2eb2e6 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Status should render a flyout when clicking the link 1`] = ` +<EuiFlyout + aria-labelledby="flyoutTitle" + closeButtonAriaLabel="Closes this dialog" + hideCloseButton={false} + maxWidth={false} + onClose={[Function]} + ownFocus={false} + size="m" +> + <EuiFlyoutHeader + hasBorder={true} + > + <EuiTitle + size="m" + > + <h2> + Monitoring alerts + </h2> + </EuiTitle> + <EuiText> + <p> + Configure an email server and email address to receive alerts. + </p> + </EuiText> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <AlertsConfiguration + emailAddress="test@elastic.co" + onDone={[Function]} + /> + </EuiFlyoutBody> +</EuiFlyout> +`; + +exports[`Status should render a success message if all alerts have been migrated and in setup mode 1`] = ` +<EuiCallOut + color="success" + iconType="flag" + title="Kibana alerting is up to date!" +> + <p> + <EuiLink + onClick={[Function]} + > + Want to make changes? Click here. + </EuiLink> + </p> +</EuiCallOut> +`; + +exports[`Status should render without setup mode 1`] = ` +<Fragment> + <EuiCallOut + color="warning" + title="Hey! We made alerting better!" + > + <p> + <EuiLink + onClick={[Function]} + > + Migrate cluster alerts to our new alerting platform. + </EuiLink> + </p> + </EuiCallOut> + <EuiSpacer /> +</Fragment> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap new file mode 100644 index 00000000000000..429d19fbb887ec --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Configuration shallow view should render step 1 1`] = ` +<Fragment> + <EuiSuperSelect + compressed={false} + fullWidth={false} + hasDividers={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + options={ + Array [ + Object { + "dropdownDisplay": <EuiText> + Create new email action... + </EuiText>, + "inputDisplay": <EuiText> + Create new email action... + </EuiText>, + "value": "__new__", + }, + ] + } + valueOfSelected="" + /> +</Fragment> +`; + +exports[`Configuration shallow view should render step 2 1`] = ` +<EuiForm + isInvalid={false} +> + <EuiFormRow + describedByIds={Array []} + display="row" + error={null} + fullWidth={false} + hasChildLabel={true} + hasEmptyLabelSpace={false} + isInvalid={false} + label="Email address" + labelType="label" + > + <EuiFieldText + disabled={true} + onChange={[Function]} + value="test@elastic.co" + /> + </EuiFormRow> +</EuiForm> +`; + +exports[`Configuration shallow view should render step 3 1`] = ` +<Fragment> + <EuiButton + isDisabled={true} + isLoading={false} + onClick={[Function]} + > + Save + </EuiButton> +</Fragment> +`; + +exports[`Configuration should render high level steps 1`] = ` +<div + className="euiSteps" +> + <EuiStep + headingElement="p" + key="0" + status="incomplete" + step={1} + title="Create email action" + > + <Step1 + editAction={null} + emailActions={Array []} + emailAddress="test@elastic.co" + onActionDone={[Function]} + selectedEmailActionId="" + setEditAction={[Function]} + setSelectedEmailActionId={[Function]} + /> + </EuiStep> + <EuiStep + headingElement="p" + key="1" + status="disabled" + step={2} + title="Set the email to receive alerts" + > + <Step2 + emailAddress="test@elastic.co" + formErrors={ + Object { + "email": null, + } + } + isDisabled={true} + setEmailAddress={[Function]} + showFormErrors={false} + /> + </EuiStep> + <EuiStep + headingElement="p" + key="2" + status="disabled" + step={3} + title="Confirm and save" + > + <Step3 + error="" + isDisabled={true} + isSaving={false} + save={[Function]} + /> + </EuiStep> +</div> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap new file mode 100644 index 00000000000000..94d951a94fe29a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap @@ -0,0 +1,300 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step1 creating should render a create form 1`] = ` +<Fragment> + <EuiPanel> + <ManageEmailAction + createEmailAction={[Function]} + isNew={true} + /> + </EuiPanel> +</Fragment> +`; + +exports[`Step1 editing should allow for editing 1`] = ` +<Fragment> + <EuiText> + <p> + Edit the action below. + </p> + </EuiText> + <EuiSpacer /> + <ManageEmailAction + action={ + Object { + "actionTypeId": "1abc", + "config": Object {}, + "id": "1", + "name": "Testing", + } + } + cancel={[Function]} + createEmailAction={[Function]} + isNew={false} + /> +</Fragment> +`; + +exports[`Step1 should render normally 1`] = ` +<Fragment> + <EuiSuperSelect + compressed={false} + fullWidth={false} + hasDividers={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + options={ + Array [ + Object { + "dropdownDisplay": <EuiText> + From: , Service: + </EuiText>, + "inputDisplay": <EuiText> + From: , Service: + </EuiText>, + "value": "1", + }, + Object { + "dropdownDisplay": <EuiText> + Create new email action... + </EuiText>, + "inputDisplay": <EuiText> + Create new email action... + </EuiText>, + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="pencil" + onClick={[Function]} + size="s" + > + Edit + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="play" + isDisabled={false} + isLoading={false} + onClick={[Function]} + size="s" + > + Test + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + color="danger" + iconType="trash" + isLoading={false} + onClick={[Function]} + size="s" + > + Delete + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> +</Fragment> +`; + +exports[`Step1 testing should should a tooltip if there is no email address 1`] = ` +<EuiToolTip + content="Please configure an email address below to test this action." + delay="regular" + position="top" +> + <EuiButton + iconType="play" + isDisabled={true} + isLoading={false} + onClick={[Function]} + size="s" + > + Test + </EuiButton> +</EuiToolTip> +`; + +exports[`Step1 testing should show a failed test error 1`] = ` +<Fragment> + <EuiSuperSelect + compressed={false} + fullWidth={false} + hasDividers={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + options={ + Array [ + Object { + "dropdownDisplay": <EuiText> + From: , Service: + </EuiText>, + "inputDisplay": <EuiText> + From: , Service: + </EuiText>, + "value": "1", + }, + Object { + "dropdownDisplay": <EuiText> + Create new email action... + </EuiText>, + "inputDisplay": <EuiText> + Create new email action... + </EuiText>, + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="pencil" + onClick={[Function]} + size="s" + > + Edit + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="play" + isDisabled={false} + isLoading={false} + onClick={[Function]} + size="s" + > + Test + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + color="danger" + iconType="trash" + isLoading={false} + onClick={[Function]} + size="s" + > + Delete + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + <EuiCallOut + color="danger" + iconType="alert" + title="Unable to send test email. Please double check your email configuration." + > + <p> + Very detailed error message + </p> + </EuiCallOut> +</Fragment> +`; + +exports[`Step1 testing should show a successful test 1`] = ` +<Fragment> + <EuiSuperSelect + compressed={false} + fullWidth={false} + hasDividers={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + options={ + Array [ + Object { + "dropdownDisplay": <EuiText> + From: , Service: + </EuiText>, + "inputDisplay": <EuiText> + From: , Service: + </EuiText>, + "value": "1", + }, + Object { + "dropdownDisplay": <EuiText> + Create new email action... + </EuiText>, + "inputDisplay": <EuiText> + Create new email action... + </EuiText>, + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="pencil" + onClick={[Function]} + size="s" + > + Edit + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + iconType="play" + isDisabled={false} + isLoading={false} + onClick={[Function]} + size="s" + > + Test + </EuiButton> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiButton + color="danger" + iconType="trash" + isLoading={false} + onClick={[Function]} + size="s" + > + Delete + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer /> + <EuiText + color="secondary" + > + <p> + Looks good on our end! + </p> + </EuiText> +</Fragment> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap new file mode 100644 index 00000000000000..bac183618b4912 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step2 should render normally 1`] = ` +<EuiForm + isInvalid={false} +> + <EuiFormRow + describedByIds={Array []} + display="row" + error={null} + fullWidth={false} + hasChildLabel={true} + hasEmptyLabelSpace={false} + isInvalid={false} + label="Email address" + labelType="label" + > + <EuiFieldText + disabled={false} + onChange={[Function]} + value="test@test.com" + /> + </EuiFormRow> +</EuiForm> +`; + +exports[`Step2 should show form errors 1`] = ` +<EuiForm + isInvalid={true} +> + <EuiFormRow + describedByIds={Array []} + display="row" + error="This is required" + fullWidth={false} + hasChildLabel={true} + hasEmptyLabelSpace={false} + isInvalid={true} + label="Email address" + labelType="label" + > + <EuiFieldText + disabled={false} + onChange={[Function]} + value="test@test.com" + /> + </EuiFormRow> +</EuiForm> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap new file mode 100644 index 00000000000000..ed15ae9a9cff7f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step3 should render normally 1`] = ` +<Fragment> + <EuiButton + isDisabled={false} + isLoading={false} + onClick={[MockFunction]} + > + Save + </EuiButton> +</Fragment> +`; + +exports[`Step3 should show a disabled state 1`] = ` +<Fragment> + <EuiButton + isDisabled={true} + isLoading={false} + onClick={ + [MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + } + } + > + Save + </EuiButton> +</Fragment> +`; + +exports[`Step3 should show a saving state 1`] = ` +<Fragment> + <EuiButton + isDisabled={false} + isLoading={true} + onClick={ + [MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + } + } + > + Save + </EuiButton> +</Fragment> +`; + +exports[`Step3 should show an error 1`] = ` +<Fragment> + <EuiCallOut + color="danger" + iconType="alert" + title="Unable to save" + > + <p> + Test error + </p> + </EuiCallOut> + <EuiSpacer /> + <EuiButton + isDisabled={false} + isLoading={false} + onClick={ + [MockFunction] { + "calls": Array [ + Array [], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + } + } + > + Save + </EuiButton> +</Fragment> +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx new file mode 100644 index 00000000000000..6b7e2391e0301c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx @@ -0,0 +1,147 @@ +/* + * 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 { mockUseEffects } from '../../../jest.helpers'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { kfetch } from 'ui/kfetch'; +import { AlertsConfiguration, AlertsConfigurationProps } from './configuration'; + +jest.mock('ui/kfetch', () => ({ + kfetch: jest.fn(), +})); + +const defaultProps: AlertsConfigurationProps = { + emailAddress: 'test@elastic.co', + onDone: jest.fn(), +}; + +describe('Configuration', () => { + it('should render high level steps', () => { + const component = shallow(<AlertsConfiguration {...defaultProps} />); + expect(component.find('EuiSteps').shallow()).toMatchSnapshot(); + }); + + function getStep(component: ShallowWrapper, index: number) { + return component + .find('EuiSteps') + .shallow() + .find('EuiStep') + .at(index) + .children() + .shallow(); + } + + describe('shallow view', () => { + it('should render step 1', () => { + const component = shallow(<AlertsConfiguration {...defaultProps} />); + const stepOne = getStep(component, 0); + expect(stepOne).toMatchSnapshot(); + }); + + it('should render step 2', () => { + const component = shallow(<AlertsConfiguration {...defaultProps} />); + const stepTwo = getStep(component, 1); + expect(stepTwo).toMatchSnapshot(); + }); + + it('should render step 3', () => { + const component = shallow(<AlertsConfiguration {...defaultProps} />); + const stepThree = getStep(component, 2); + expect(stepThree).toMatchSnapshot(); + }); + }); + + describe('selected action', () => { + const actionId = 'a123b'; + let component: ShallowWrapper; + beforeEach(async () => { + mockUseEffects(2); + + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [ + { + actionTypeId: '.email', + id: actionId, + config: {}, + }, + ], + }; + }); + + component = shallow(<AlertsConfiguration {...defaultProps} />); + }); + + it('reflect in Step1', async () => { + const steps = component.find('EuiSteps').dive(); + expect( + steps + .find('EuiStep') + .at(0) + .prop('title') + ).toBe('Select email action'); + expect(steps.find('Step1').prop('selectedEmailActionId')).toBe(actionId); + }); + + it('should enable Step2', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step2').prop('isDisabled')).toBe(false); + }); + + it('should enable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(false); + }); + }); + + describe('edit action', () => { + let component: ShallowWrapper; + beforeEach(async () => { + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [], + }; + }); + + component = shallow(<AlertsConfiguration {...defaultProps} />); + }); + + it('disable Step2', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step2').prop('isDisabled')).toBe(true); + }); + + it('disable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(true); + }); + }); + + describe('no email address', () => { + let component: ShallowWrapper; + beforeEach(async () => { + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [ + { + actionTypeId: '.email', + id: 'actionId', + config: {}, + }, + ], + }; + }); + + component = shallow(<AlertsConfiguration {...defaultProps} emailAddress="" />); + }); + + it('should disable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx new file mode 100644 index 00000000000000..0933cd22db7c9f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx @@ -0,0 +1,193 @@ +/* + * 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, { ReactNode } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { EuiSteps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; +import { getMissingFieldErrors } from '../../../lib/form_validation'; +import { Step1 } from './step1'; +import { Step2 } from './step2'; +import { Step3 } from './step3'; + +export interface AlertsConfigurationProps { + emailAddress: string; + onDone: Function; +} + +export interface StepResult { + title: string; + children: ReactNode; + status: any; +} + +export interface AlertsConfigurationForm { + email: string | null; +} + +export const NEW_ACTION_ID = '__new__'; + +export const AlertsConfiguration: React.FC<AlertsConfigurationProps> = ( + props: AlertsConfigurationProps +) => { + const { onDone } = props; + + const [emailActions, setEmailActions] = React.useState<ActionResult[]>([]); + const [selectedEmailActionId, setSelectedEmailActionId] = React.useState(''); + const [editAction, setEditAction] = React.useState<ActionResult | null>(null); + const [emailAddress, setEmailAddress] = React.useState(props.emailAddress); + const [formErrors, setFormErrors] = React.useState<AlertsConfigurationForm>({ email: null }); + const [showFormErrors, setShowFormErrors] = React.useState(false); + const [isSaving, setIsSaving] = React.useState(false); + const [saveError, setSaveError] = React.useState(''); + + React.useEffect(() => { + async function fetchData() { + await fetchEmailActions(); + } + + fetchData(); + }, []); + + React.useEffect(() => { + setFormErrors(getMissingFieldErrors({ email: emailAddress }, { email: '' })); + }, [emailAddress]); + + async function fetchEmailActions() { + const kibanaActions = await kfetch({ + method: 'GET', + pathname: `/api/action/_find`, + }); + + const actions = kibanaActions.data.filter( + (action: ActionResult) => action.actionTypeId === ALERT_ACTION_TYPE_EMAIL + ); + if (actions.length > 0) { + setSelectedEmailActionId(actions[0].id); + } else { + setSelectedEmailActionId(NEW_ACTION_ID); + } + setEmailActions(actions); + } + + async function save() { + if (emailAddress.length === 0) { + setShowFormErrors(true); + return; + } + setIsSaving(true); + setShowFormErrors(false); + + try { + await kfetch({ + method: 'POST', + pathname: `/api/monitoring/v1/alerts`, + body: JSON.stringify({ selectedEmailActionId, emailAddress }), + }); + } catch (err) { + setIsSaving(false); + setSaveError( + err?.body?.message || + i18n.translate('xpack.monitoring.alerts.configuration.unknownError', { + defaultMessage: 'Something went wrong. Please consult the server logs.', + }) + ); + return; + } + + onDone(); + } + + function isStep2Disabled() { + return isStep2AndStep3Disabled(); + } + + function isStep3Disabled() { + return isStep2AndStep3Disabled() || !emailAddress || emailAddress.length === 0; + } + + function isStep2AndStep3Disabled() { + return !!editAction || !selectedEmailActionId || selectedEmailActionId === NEW_ACTION_ID; + } + + function getStep2Status() { + const isDisabled = isStep2AndStep3Disabled(); + + if (isDisabled) { + return 'disabled' as const; + } + + if (emailAddress && emailAddress.length) { + return 'complete' as const; + } + + return 'incomplete' as const; + } + + function getStep1Status() { + if (editAction) { + return 'incomplete' as const; + } + + return selectedEmailActionId ? ('complete' as const) : ('incomplete' as const); + } + + const steps = [ + { + title: emailActions.length + ? i18n.translate('xpack.monitoring.alerts.configuration.selectEmailAction', { + defaultMessage: 'Select email action', + }) + : i18n.translate('xpack.monitoring.alerts.configuration.createEmailAction', { + defaultMessage: 'Create email action', + }), + children: ( + <Step1 + onActionDone={async () => await fetchEmailActions()} + emailActions={emailActions} + selectedEmailActionId={selectedEmailActionId} + setSelectedEmailActionId={setSelectedEmailActionId} + emailAddress={emailAddress} + editAction={editAction} + setEditAction={setEditAction} + /> + ), + status: getStep1Status(), + }, + { + title: i18n.translate('xpack.monitoring.alerts.configuration.setEmailAddress', { + defaultMessage: 'Set the email to receive alerts', + }), + status: getStep2Status(), + children: ( + <Step2 + emailAddress={emailAddress} + setEmailAddress={setEmailAddress} + showFormErrors={showFormErrors} + formErrors={formErrors} + isDisabled={isStep2Disabled()} + /> + ), + }, + { + title: i18n.translate('xpack.monitoring.alerts.configuration.confirm', { + defaultMessage: 'Confirm and save', + }), + status: getStep2Status(), + children: ( + <Step3 isSaving={isSaving} save={save} isDisabled={isStep3Disabled()} error={saveError} /> + ), + }, + ]; + + return ( + <div> + <EuiSteps steps={steps} /> + </div> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts new file mode 100644 index 00000000000000..7a96c6e324ab36 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { AlertsConfiguration } from './configuration'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx new file mode 100644 index 00000000000000..650294c29e9a50 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx @@ -0,0 +1,338 @@ +/* + * 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 { omit, pick } from 'lodash'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { GetStep1Props } from './step1'; +import { EmailActionData } from '../manage_email_action'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; + +let Step1: React.FC<GetStep1Props>; +let NEW_ACTION_ID: string; + +function setModules() { + Step1 = require('./step1').Step1; + NEW_ACTION_ID = require('./configuration').NEW_ACTION_ID; +} + +describe('Step1', () => { + const emailActions = [ + { + id: '1', + actionTypeId: '1abc', + name: 'Testing', + config: {}, + }, + ]; + const selectedEmailActionId = emailActions[0].id; + const setSelectedEmailActionId = jest.fn(); + const emailAddress = 'test@test.com'; + const editAction = null; + const setEditAction = jest.fn(); + const onActionDone = jest.fn(); + + const defaultProps: GetStep1Props = { + onActionDone, + emailActions, + selectedEmailActionId, + setSelectedEmailActionId, + emailAddress, + editAction, + setEditAction, + }; + + beforeEach(() => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: () => { + return {}; + }, + })); + setModules(); + }); + }); + + it('should render normally', () => { + const component = shallow(<Step1 {...defaultProps} />); + + expect(component).toMatchSnapshot(); + }); + + describe('creating', () => { + it('should render a create form', () => { + const customProps = { + emailActions: [], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + expect(component).toMatchSnapshot(); + }); + + it('should render the select box if at least one action exists', () => { + const customProps = { + emailActions: [ + { + id: 'foo', + actionTypeId: '.email', + name: '', + config: {}, + }, + ], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + expect(component.find('EuiSuperSelect').exists()).toBe(true); + }); + + it('should send up the create to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + emailActions: [], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + const data: EmailActionData = { + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + from: 'test@test.com', + user: 'user@user.com', + password: 'password', + }; + + const createEmailAction: (data: EmailActionData) => void = component + .find('ManageEmailAction') + .prop('createEmailAction'); + createEmailAction(data); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'POST', + pathname: `/api/action`, + body: JSON.stringify({ + name: 'Email action for Stack Monitoring alerts', + actionTypeId: ALERT_ACTION_TYPE_EMAIL, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + }); + }); + + describe('editing', () => { + it('should allow for editing', () => { + const customProps = { + editAction: emailActions[0], + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + expect(component).toMatchSnapshot(); + }); + + it('should send up the edit to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + editAction: emailActions[0], + }; + + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + const data: EmailActionData = { + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + from: 'test@test.com', + user: 'user@user.com', + password: 'password', + }; + + const createEmailAction: (data: EmailActionData) => void = component + .find('ManageEmailAction') + .prop('createEmailAction'); + createEmailAction(data); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'PUT', + pathname: `/api/action/${emailActions[0].id}`, + body: JSON.stringify({ + name: emailActions[0].name, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + }); + }); + + describe('testing', () => { + it('should allow for testing', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: jest.fn().mockImplementation(arg => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }), + })); + setModules(); + }); + + const component = shallow(<Step1 {...defaultProps} />); + + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(false); + component + .find('EuiButton') + .at(1) + .simulate('click'); + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(true); + await component.update(); + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(false); + }); + + it('should show a successful test', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }, + })); + setModules(); + }); + + const component = shallow(<Step1 {...defaultProps} />); + + component + .find('EuiButton') + .at(1) + .simulate('click'); + await component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should show a failed test error', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { message: 'Very detailed error message' }; + } + return {}; + }, + })); + setModules(); + }); + + const component = shallow(<Step1 {...defaultProps} />); + + component + .find('EuiButton') + .at(1) + .simulate('click'); + await component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should not allow testing if there is no email address', () => { + const customProps = { + emailAddress: '', + }; + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + expect( + component + .find('EuiButton') + .at(1) + .prop('isDisabled') + ).toBe(true); + }); + + it('should should a tooltip if there is no email address', () => { + const customProps = { + emailAddress: '', + }; + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + expect(component.find('EuiToolTip')).toMatchSnapshot(); + }); + }); + + describe('deleting', () => { + it('should send up the delete to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + setSelectedEmailActionId: jest.fn(), + onActionDone: jest.fn(), + }; + const component = shallow(<Step1 {...defaultProps} {...customProps} />); + + await component + .find('EuiButton') + .at(2) + .simulate('click'); + await component.update(); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'DELETE', + pathname: `/api/action/${emailActions[0].id}`, + }); + + expect(customProps.setSelectedEmailActionId).toHaveBeenCalledWith(''); + expect(customProps.onActionDone).toHaveBeenCalled(); + expect( + component + .find('EuiButton') + .at(2) + .prop('isLoading') + ).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx new file mode 100644 index 00000000000000..fc051a68e29f35 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx @@ -0,0 +1,334 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiText, + EuiSpacer, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSuperSelect, + EuiToolTip, + EuiCallOut, +} from '@elastic/eui'; +import { kfetch } from 'ui/kfetch'; +import { omit, pick } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { ManageEmailAction, EmailActionData } from '../manage_email_action'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; +import { NEW_ACTION_ID } from './configuration'; + +export interface GetStep1Props { + onActionDone: () => Promise<void>; + emailActions: ActionResult[]; + selectedEmailActionId: string; + setSelectedEmailActionId: (id: string) => void; + emailAddress: string; + editAction: ActionResult | null; + setEditAction: (action: ActionResult | null) => void; +} + +export const Step1: React.FC<GetStep1Props> = (props: GetStep1Props) => { + const [isTesting, setIsTesting] = React.useState(false); + const [isDeleting, setIsDeleting] = React.useState(false); + const [testingStatus, setTestingStatus] = React.useState<string | boolean | null>(null); + const [fullTestingError, setFullTestingError] = React.useState(''); + + async function createEmailAction(data: EmailActionData) { + if (props.editAction) { + await kfetch({ + method: 'PUT', + pathname: `/api/action/${props.editAction.id}`, + body: JSON.stringify({ + name: props.editAction.name, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + props.setEditAction(null); + } else { + await kfetch({ + method: 'POST', + pathname: '/api/action', + body: JSON.stringify({ + name: i18n.translate('xpack.monitoring.alerts.configuration.emailAction.name', { + defaultMessage: 'Email action for Stack Monitoring alerts', + }), + actionTypeId: ALERT_ACTION_TYPE_EMAIL, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + } + + await props.onActionDone(); + } + + async function deleteEmailAction(id: string) { + setIsDeleting(true); + + await kfetch({ + method: 'DELETE', + pathname: `/api/action/${id}`, + }); + + if (props.editAction && props.editAction.id === id) { + props.setEditAction(null); + } + if (props.selectedEmailActionId === id) { + props.setSelectedEmailActionId(''); + } + await props.onActionDone(); + setIsDeleting(false); + setTestingStatus(null); + } + + async function testEmailAction() { + setIsTesting(true); + setTestingStatus(null); + + const params = { + subject: 'Kibana alerting test configuration', + message: `This is a test for the configured email action for Kibana alerting.`, + to: [props.emailAddress], + }; + + const result = await kfetch({ + method: 'POST', + pathname: `/api/action/${props.selectedEmailActionId}/_execute`, + body: JSON.stringify({ params }), + }); + if (result.status === 'ok') { + setTestingStatus(true); + } else { + setTestingStatus(false); + setFullTestingError(result.message); + } + setIsTesting(false); + } + + function getTestButton() { + const isTestingDisabled = !props.emailAddress || props.emailAddress.length === 0; + const testBtn = ( + <EuiButton + size="s" + iconType="play" + onClick={testEmailAction} + isLoading={isTesting} + isDisabled={isTestingDisabled} + > + {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.buttonText', { + defaultMessage: 'Test', + })} + </EuiButton> + ); + + if (isTestingDisabled) { + return ( + <EuiToolTip + position="top" + content={i18n.translate( + 'xpack.monitoring.alerts.configuration.testConfiguration.disabledTooltipText', + { + defaultMessage: 'Please configure an email address below to test this action.', + } + )} + > + {testBtn} + </EuiToolTip> + ); + } + + return testBtn; + } + + if (props.editAction) { + return ( + <Fragment> + <EuiText> + <p> + {i18n.translate('xpack.monitoring.alerts.configuration.step1.editAction', { + defaultMessage: 'Edit the action below.', + })} + </p> + </EuiText> + <EuiSpacer /> + <ManageEmailAction + createEmailAction={async (data: EmailActionData) => await createEmailAction(data)} + cancel={() => props.setEditAction(null)} + isNew={false} + action={props.editAction} + /> + </Fragment> + ); + } + + const newAction = ( + <EuiText> + {i18n.translate('xpack.monitoring.alerts.configuration.newActionDropdownDisplay', { + defaultMessage: 'Create new email action...', + })} + </EuiText> + ); + + const options = [ + ...props.emailActions.map(action => { + const actionLabel = i18n.translate( + 'xpack.monitoring.alerts.configuration.selectAction.inputDisplay', + { + defaultMessage: 'From: {from}, Service: {service}', + values: { + service: action.config.service, + from: action.config.from, + }, + } + ); + + return { + value: action.id, + inputDisplay: <EuiText>{actionLabel}</EuiText>, + dropdownDisplay: <EuiText>{actionLabel}</EuiText>, + }; + }), + { + value: NEW_ACTION_ID, + inputDisplay: newAction, + dropdownDisplay: newAction, + }, + ]; + + let selectBox: React.ReactNode | null = ( + <EuiSuperSelect + options={options} + valueOfSelected={props.selectedEmailActionId} + onChange={id => props.setSelectedEmailActionId(id)} + hasDividers + /> + ); + let createNew = null; + if (props.selectedEmailActionId === NEW_ACTION_ID) { + createNew = ( + <EuiPanel> + <ManageEmailAction + createEmailAction={async (data: EmailActionData) => await createEmailAction(data)} + isNew={true} + /> + </EuiPanel> + ); + + // If there are no actions, do not show the select box as there are no choices + if (props.emailActions.length === 0) { + selectBox = null; + } else { + // Otherwise, add a spacer + selectBox = ( + <Fragment> + {selectBox} + <EuiSpacer /> + </Fragment> + ); + } + } + + let manageConfiguration = null; + const selectedEmailAction = props.emailActions.find( + action => action.id === props.selectedEmailActionId + ); + + if ( + props.selectedEmailActionId !== NEW_ACTION_ID && + props.selectedEmailActionId && + selectedEmailAction + ) { + let testingStatusUi = null; + if (testingStatus === true) { + testingStatusUi = ( + <Fragment> + <EuiSpacer /> + <EuiText color="secondary"> + <p> + {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.success', { + defaultMessage: 'Looks good on our end!', + })} + </p> + </EuiText> + </Fragment> + ); + } else if (testingStatus === false) { + testingStatusUi = ( + <Fragment> + <EuiSpacer /> + <EuiCallOut + title={i18n.translate('xpack.monitoring.alerts.configuration.step1.testingError', { + defaultMessage: + 'Unable to send test email. Please double check your email configuration.', + })} + color="danger" + iconType="alert" + > + <p>{fullTestingError}</p> + </EuiCallOut> + </Fragment> + ); + } + + manageConfiguration = ( + <Fragment> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton + size="s" + iconType="pencil" + onClick={() => { + const editAction = + props.emailActions.find(action => action.id === props.selectedEmailActionId) || + null; + props.setEditAction(editAction); + }} + > + {i18n.translate( + 'xpack.monitoring.alerts.configuration.editConfiguration.buttonText', + { + defaultMessage: 'Edit', + } + )} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}>{getTestButton()}</EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + size="s" + color="danger" + iconType="trash" + onClick={() => deleteEmailAction(props.selectedEmailActionId)} + isLoading={isDeleting} + > + {i18n.translate( + 'xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText', + { + defaultMessage: 'Delete', + } + )} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + {testingStatusUi} + </Fragment> + ); + } + + return ( + <Fragment> + {selectBox} + {manageConfiguration} + {createNew} + </Fragment> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx new file mode 100644 index 00000000000000..14e3cb078f9cca --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { Step2, GetStep2Props } from './step2'; + +describe('Step2', () => { + const defaultProps: GetStep2Props = { + emailAddress: 'test@test.com', + setEmailAddress: jest.fn(), + showFormErrors: false, + formErrors: { email: null }, + isDisabled: false, + }; + + it('should render normally', () => { + const component = shallow(<Step2 {...defaultProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should set the email address properly', () => { + const newEmail = 'email@email.com'; + const component = shallow(<Step2 {...defaultProps} />); + component.find('EuiFieldText').simulate('change', { target: { value: newEmail } }); + expect(defaultProps.setEmailAddress).toHaveBeenCalledWith(newEmail); + }); + + it('should show form errors', () => { + const customProps = { + showFormErrors: true, + formErrors: { + email: 'This is required', + }, + }; + const component = shallow(<Step2 {...defaultProps} {...customProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should disable properly', () => { + const customProps = { + isDisabled: true, + }; + const component = shallow(<Step2 {...defaultProps} {...customProps} />); + expect(component.find('EuiFieldText').prop('disabled')).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx new file mode 100644 index 00000000000000..974dd8513d231d --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AlertsConfigurationForm } from './configuration'; + +export interface GetStep2Props { + emailAddress: string; + setEmailAddress: (email: string) => void; + showFormErrors: boolean; + formErrors: AlertsConfigurationForm; + isDisabled: boolean; +} + +export const Step2: React.FC<GetStep2Props> = (props: GetStep2Props) => { + return ( + <EuiForm isInvalid={props.showFormErrors}> + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.configuration.emailAddressLabel', { + defaultMessage: 'Email address', + })} + error={props.formErrors.email} + isInvalid={props.showFormErrors && !!props.formErrors.email} + > + <EuiFieldText + value={props.emailAddress} + disabled={props.isDisabled} + onChange={e => props.setEmailAddress(e.target.value)} + /> + </EuiFormRow> + </EuiForm> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx new file mode 100644 index 00000000000000..9b1304c42a507d --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { Step3 } from './step3'; + +describe('Step3', () => { + const defaultProps = { + isSaving: false, + isDisabled: false, + save: jest.fn(), + error: null, + }; + + it('should render normally', () => { + const component = shallow(<Step3 {...defaultProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should save properly', () => { + const component = shallow(<Step3 {...defaultProps} />); + component.find('EuiButton').simulate('click'); + expect(defaultProps.save).toHaveBeenCalledWith(); + }); + + it('should show a saving state', () => { + const customProps = { isSaving: true }; + const component = shallow(<Step3 {...defaultProps} {...customProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should show a disabled state', () => { + const customProps = { isDisabled: true }; + const component = shallow(<Step3 {...defaultProps} {...customProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should show an error', () => { + const customProps = { error: 'Test error' }; + const component = shallow(<Step3 {...defaultProps} {...customProps} />); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx new file mode 100644 index 00000000000000..80acb8992cbc16 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx @@ -0,0 +1,47 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiButton, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface GetStep3Props { + isSaving: boolean; + isDisabled: boolean; + save: () => void; + error: string | null; +} + +export const Step3: React.FC<GetStep3Props> = (props: GetStep3Props) => { + let errorUi = null; + if (props.error) { + errorUi = ( + <Fragment> + <EuiCallOut + title={i18n.translate('xpack.monitoring.alerts.configuration.step3.saveError', { + defaultMessage: 'Unable to save', + })} + color="danger" + iconType="alert" + > + <p>{props.error}</p> + </EuiCallOut> + <EuiSpacer /> + </Fragment> + ); + } + + return ( + <Fragment> + {errorUi} + <EuiButton isLoading={props.isSaving} isDisabled={props.isDisabled} onClick={props.save}> + {i18n.translate('xpack.monitoring.alerts.configuration.save', { + defaultMessage: 'Save', + })} + </EuiButton> + </Fragment> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx new file mode 100644 index 00000000000000..2bd9804795cb56 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx @@ -0,0 +1,301 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiLink, + EuiSpacer, + EuiFieldNumber, + EuiFieldPassword, + EuiSwitch, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../plugins/actions/common'; +import { getMissingFieldErrors, hasErrors, getRequiredFieldError } from '../../lib/form_validation'; +import { ALERT_EMAIL_SERVICES } from '../../../common/constants'; + +export interface EmailActionData { + service: string; + host: string; + port?: number; + secure: boolean; + from: string; + user: string; + password: string; +} + +interface ManageActionModalProps { + createEmailAction: (handler: EmailActionData) => void; + cancel?: () => void; + isNew: boolean; + action?: ActionResult | null; +} + +const DEFAULT_DATA: EmailActionData = { + service: '', + host: '', + port: 0, + secure: false, + from: '', + user: '', + password: '', +}; + +const CREATE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.createLabel', { + defaultMessage: 'Create email action', +}); +const SAVE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.saveLabel', { + defaultMessage: 'Save email action', +}); +const CANCEL_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.cancelLabel', { + defaultMessage: 'Cancel', +}); + +const NEW_SERVICE_ID = '__new__'; + +export const ManageEmailAction: React.FC<ManageActionModalProps> = ( + props: ManageActionModalProps +) => { + const { createEmailAction, cancel, isNew, action } = props; + + const defaultData = Object.assign({}, DEFAULT_DATA, action ? action.config : {}); + const [isSaving, setIsSaving] = React.useState(false); + const [showErrors, setShowErrors] = React.useState(false); + const [errors, setErrors] = React.useState<EmailActionData | any>( + getMissingFieldErrors(defaultData, DEFAULT_DATA) + ); + const [data, setData] = React.useState(defaultData); + const [createNewService, setCreateNewService] = React.useState(false); + const [newService, setNewService] = React.useState(''); + + React.useEffect(() => { + const missingFieldErrors = getMissingFieldErrors(data, DEFAULT_DATA); + if (!missingFieldErrors.service) { + if (data.service === NEW_SERVICE_ID && !newService) { + missingFieldErrors.service = getRequiredFieldError('service'); + } + } + setErrors(missingFieldErrors); + }, [data, newService]); + + async function saveEmailAction() { + setShowErrors(true); + if (!hasErrors(errors)) { + setShowErrors(false); + setIsSaving(true); + const mergedData = { + ...data, + service: data.service === NEW_SERVICE_ID ? newService : data.service, + }; + try { + await createEmailAction(mergedData); + } catch (err) { + setErrors({ + general: err.body.message, + }); + } + } + } + + const serviceOptions = ALERT_EMAIL_SERVICES.map(service => ({ + value: service, + inputDisplay: <EuiText>{service}</EuiText>, + dropdownDisplay: <EuiText>{service}</EuiText>, + })); + + serviceOptions.push({ + value: NEW_SERVICE_ID, + inputDisplay: ( + <EuiText> + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText', { + defaultMessage: 'Adding new service...', + })} + </EuiText> + ), + dropdownDisplay: ( + <EuiText> + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addNewServiceText', { + defaultMessage: 'Add new service...', + })} + </EuiText> + ), + }); + + let addNewServiceUi = null; + if (createNewService) { + addNewServiceUi = ( + <Fragment> + <EuiSpacer /> + <EuiFieldText + value={newService} + onChange={e => setNewService(e.target.value)} + isInvalid={showErrors} + /> + </Fragment> + ); + } + + return ( + <EuiForm isInvalid={showErrors} error={Object.values(errors)}> + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceText', { + defaultMessage: 'Service', + })} + helpText={ + <EuiLink external target="_blank" href="https://nodemailer.com/smtp/well-known/"> + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceHelpText', { + defaultMessage: 'Find out more', + })} + </EuiLink> + } + error={errors.service} + isInvalid={showErrors && !!errors.service} + > + <Fragment> + <EuiSuperSelect + options={serviceOptions} + valueOfSelected={data.service} + onChange={id => { + if (id === NEW_SERVICE_ID) { + setCreateNewService(true); + setData({ ...data, service: NEW_SERVICE_ID }); + } else { + setCreateNewService(false); + setData({ ...data, service: id }); + } + }} + hasDividers + isInvalid={showErrors && !!errors.service} + /> + {addNewServiceUi} + </Fragment> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.hostText', { + defaultMessage: 'Host', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.hostHelpText', { + defaultMessage: 'Host name of the service provider', + })} + error={errors.host} + isInvalid={showErrors && !!errors.host} + > + <EuiFieldText + value={data.host} + onChange={e => setData({ ...data, host: e.target.value })} + isInvalid={showErrors && !!errors.host} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.portText', { + defaultMessage: 'Port', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.portHelpText', { + defaultMessage: 'Port number of the service provider', + })} + error={errors.port} + isInvalid={showErrors && !!errors.port} + > + <EuiFieldNumber + value={data.port} + onChange={e => setData({ ...data, port: parseInt(e.target.value, 10) })} + isInvalid={showErrors && !!errors.port} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.secureText', { + defaultMessage: 'Secure', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.secureHelpText', { + defaultMessage: 'Whether to use TLS with the service provider', + })} + > + <EuiSwitch + label="" + checked={data.secure} + onChange={e => setData({ ...data, secure: e.target.checked })} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.fromText', { + defaultMessage: 'From', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.fromHelpText', { + defaultMessage: 'The from email address for alerts', + })} + error={errors.from} + isInvalid={showErrors && !!errors.from} + > + <EuiFieldText + value={data.from} + onChange={e => setData({ ...data, from: e.target.value })} + isInvalid={showErrors && !!errors.from} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.userText', { + defaultMessage: 'User', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.userHelpText', { + defaultMessage: 'The user to use with the service provider', + })} + error={errors.user} + isInvalid={showErrors && !!errors.user} + > + <EuiFieldText + value={data.user} + onChange={e => setData({ ...data, user: e.target.value })} + isInvalid={showErrors && !!errors.user} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.passwordText', { + defaultMessage: 'Password', + })} + helpText={i18n.translate('xpack.monitoring.alerts.migrate.manageAction.passwordHelpText', { + defaultMessage: 'The password to use with the service provider', + })} + error={errors.password} + isInvalid={showErrors && !!errors.password} + > + <EuiFieldPassword + value={data.password} + onChange={e => setData({ ...data, password: e.target.value })} + isInvalid={showErrors && !!errors.password} + /> + </EuiFormRow> + + <EuiSpacer /> + + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton type="submit" fill onClick={saveEmailAction} isLoading={isSaving}> + {isNew ? CREATE_LABEL : SAVE_LABEL} + </EuiButton> + </EuiFlexItem> + {!action || isNew ? null : ( + <EuiFlexItem grow={false}> + <EuiButton onClick={cancel}>{CANCEL_LABEL}</EuiButton> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiForm> + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx new file mode 100644 index 00000000000000..258a5b68db372e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; +import { kfetch } from 'ui/kfetch'; +import { AlertsStatus, AlertsStatusProps } from './status'; +import { ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { getSetupModeState } from '../../lib/setup_mode'; +import { mockUseEffects } from '../../jest.helpers'; + +jest.mock('../../lib/setup_mode', () => ({ + getSetupModeState: jest.fn(), + addSetupModeCallback: jest.fn(), + toggleSetupMode: jest.fn(), +})); + +jest.mock('ui/kfetch', () => ({ + kfetch: jest.fn(), +})); + +const defaultProps: AlertsStatusProps = { + clusterUuid: '1adsb23', + emailAddress: 'test@elastic.co', +}; + +describe('Status', () => { + beforeEach(() => { + mockUseEffects(2); + + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: false, + }); + + (kfetch as jest.Mock).mockImplementation(({ pathname }) => { + if (pathname === '/internal/security/api_key/privileges') { + return { areApiKeysEnabled: true }; + } + return { + data: [], + }; + }); + }); + + it('should render without setup mode', () => { + const component = shallow(<AlertsStatus {...defaultProps} />); + expect(component).toMatchSnapshot(); + }); + + it('should render a flyout when clicking the link', async () => { + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: true, + }); + + const component = shallow(<AlertsStatus {...defaultProps} />); + component.find('EuiLink').simulate('click'); + await component.update(); + expect(component.find('EuiFlyout')).toMatchSnapshot(); + }); + + it('should render a success message if all alerts have been migrated and in setup mode', async () => { + (kfetch as jest.Mock).mockReturnValue({ + data: [ + { + alertTypeId: ALERT_TYPE_PREFIX, + }, + ], + }); + + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: true, + }); + + const component = shallow(<AlertsStatus {...defaultProps} />); + await component.update(); + expect(component.find('EuiCallOut')).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx new file mode 100644 index 00000000000000..0ee0015ed39a70 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx @@ -0,0 +1,203 @@ +/* + * 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, { Fragment } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { + EuiSpacer, + EuiCallOut, + EuiTitle, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Alert } from '../../../../alerting/server/types'; +import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; +import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { AlertsConfiguration } from './configuration'; + +export interface AlertsStatusProps { + clusterUuid: string; + emailAddress: string; +} + +export const AlertsStatus: React.FC<AlertsStatusProps> = (props: AlertsStatusProps) => { + const { emailAddress } = props; + + const [setupModeEnabled, setSetupModeEnabled] = React.useState(getSetupModeState().enabled); + const [kibanaAlerts, setKibanaAlerts] = React.useState<Alert[]>([]); + const [showMigrationFlyout, setShowMigrationFlyout] = React.useState(false); + const [isSecurityConfigured, setIsSecurityConfigured] = React.useState(false); + + React.useEffect(() => { + async function fetchAlertsStatus() { + const alerts = await kfetch({ method: 'GET', pathname: `/api/alert/_find` }); + const monitoringAlerts = alerts.data.filter((alert: Alert) => + alert.alertTypeId.startsWith(ALERT_TYPE_PREFIX) + ); + setKibanaAlerts(monitoringAlerts); + } + + fetchAlertsStatus(); + fetchSecurityConfigured(); + }, [setupModeEnabled, showMigrationFlyout]); + + React.useEffect(() => { + if (!setupModeEnabled && showMigrationFlyout) { + setShowMigrationFlyout(false); + } + }, [setupModeEnabled, showMigrationFlyout]); + + async function fetchSecurityConfigured() { + const response = await kfetch({ pathname: '/internal/security/api_key/privileges' }); + setIsSecurityConfigured(response.areApiKeysEnabled); + } + + addSetupModeCallback(() => setSetupModeEnabled(getSetupModeState().enabled)); + + function enterSetupModeAndOpenFlyout() { + toggleSetupMode(true); + setShowMigrationFlyout(true); + } + + function getSecurityConfigurationErrorUi() { + if (isSecurityConfigured) { + return null; + } + + const link = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/security-settings.html#api-key-service-settings`; + return ( + <Fragment> + <EuiSpacer /> + <EuiCallOut + title={i18n.translate( + 'xpack.monitoring.alerts.configuration.securityConfigurationErrorTitle', + { + defaultMessage: 'API keys are not enabled in Elasticsearch', + } + )} + color="danger" + iconType="alert" + > + <p> + <FormattedMessage + id="xpack.monitoring.alerts.configuration.securityConfigurationErrorMessage" + defaultMessage="Refer to the {link} to enable API keys." + values={{ + link: ( + <EuiLink href={link} target="_blank"> + {i18n.translate( + 'xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel', + { + defaultMessage: 'docs', + } + )} + </EuiLink> + ), + }} + /> + </p> + </EuiCallOut> + </Fragment> + ); + } + + function renderContent() { + let flyout = null; + if (showMigrationFlyout) { + flyout = ( + <EuiFlyout onClose={() => setShowMigrationFlyout(false)} aria-labelledby="flyoutTitle"> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2> + {i18n.translate('xpack.monitoring.alerts.status.flyoutTitle', { + defaultMessage: 'Monitoring alerts', + })} + </h2> + </EuiTitle> + <EuiText> + <p> + {i18n.translate('xpack.monitoring.alerts.status.flyoutSubtitle', { + defaultMessage: 'Configure an email server and email address to receive alerts.', + })} + </p> + </EuiText> + {getSecurityConfigurationErrorUi()} + </EuiFlyoutHeader> + <EuiFlyoutBody> + <AlertsConfiguration + emailAddress={emailAddress} + onDone={() => setShowMigrationFlyout(false)} + /> + </EuiFlyoutBody> + </EuiFlyout> + ); + } + + const allMigrated = kibanaAlerts.length === NUMBER_OF_MIGRATED_ALERTS; + if (allMigrated) { + if (setupModeEnabled) { + return ( + <Fragment> + <EuiCallOut + color="success" + title={i18n.translate('xpack.monitoring.alerts.status.upToDate', { + defaultMessage: 'Kibana alerting is up to date!', + })} + iconType="flag" + > + <p> + <EuiLink onClick={enterSetupModeAndOpenFlyout}> + {i18n.translate('xpack.monitoring.alerts.status.manage', { + defaultMessage: 'Want to make changes? Click here.', + })} + </EuiLink> + </p> + </EuiCallOut> + {flyout} + </Fragment> + ); + } + } else { + return ( + <Fragment> + <EuiCallOut + color="warning" + title={i18n.translate('xpack.monitoring.alerts.status.needToMigrateTitle', { + defaultMessage: 'Hey! We made alerting better!', + })} + > + <p> + <EuiLink onClick={enterSetupModeAndOpenFlyout}> + {i18n.translate('xpack.monitoring.alerts.status.needToMigrate', { + defaultMessage: 'Migrate cluster alerts to our new alerting platform.', + })} + </EuiLink> + </p> + </EuiCallOut> + {flyout} + </Fragment> + ); + } + } + + const content = renderContent(); + if (content) { + return ( + <Fragment> + {content} + <EuiSpacer /> + </Fragment> + ); + } + + return null; +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index 33b26c7ec56e00..a8001638f4399d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Fragment } from 'react'; +import moment from 'moment-timezone'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; -import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; +import { + CALCULATE_DURATION_SINCE, + KIBANA_ALERTING_ENABLED, + ALERT_TYPE_LICENSE_EXPIRATION, + CALCULATE_DURATION_UNTIL, +} from '../../../../common/constants'; import { formatDateTimeLocal } from '../../../../common/formatting'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -21,6 +27,7 @@ import { EuiText, EuiSpacer, EuiCallOut, + EuiLink, } from '@elastic/eui'; export function AlertsPanel({ alerts, changeUrl }) { @@ -82,9 +89,52 @@ export function AlertsPanel({ alerts, changeUrl }) { ); } - const topAlertItems = alerts.map((item, index) => ( - <TopAlertItem item={item} key={`top-alert-item-${index}`} index={index} /> - )); + const alertsList = KIBANA_ALERTING_ENABLED + ? alerts.map((alert, idx) => { + const callOutProps = mapSeverity(alert.severity); + let message = alert.message + // scan message prefix and replace relative times + // \w: Matches any alphanumeric character from the basic Latin alphabet, including the underscore. Equivalent to [A-Za-z0-9_]. + .replace( + '#relative', + formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL) + ) + .replace('#absolute', moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z')); + + if (!alert.isFiring) { + callOutProps.title = i18n.translate( + 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', + { + defaultMessage: '{severityIconTitle} (resolved {time} ago)', + values: { + severityIconTitle: callOutProps.title, + time: formatTimestampToDuration(alert.resolvedMS, CALCULATE_DURATION_SINCE), + }, + } + ); + callOutProps.color = 'success'; + callOutProps.iconType = 'check'; + } else { + if (alert.type === ALERT_TYPE_LICENSE_EXPIRATION) { + message = ( + <Fragment> + {message} +   + <EuiLink href="#license">Please update your license</EuiLink> + </Fragment> + ); + } + } + + return ( + <EuiCallOut key={idx} {...callOutProps}> + <p>{message}</p> + </EuiCallOut> + ); + }) + : alerts.map((item, index) => ( + <TopAlertItem item={item} key={`top-alert-item-${index}`} index={index} /> + )); return ( <div data-test-subj="clusterAlertsContainer"> @@ -109,7 +159,7 @@ export function AlertsPanel({ alerts, changeUrl }) { </EuiFlexItem> </EuiFlexGroup> <EuiSpacer size="m" /> - {topAlertItems} + {alertsList} <EuiSpacer size="xxl" /> </div> ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js index cad4bbf411c347..eee51c416d11eb 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js @@ -10,15 +10,22 @@ import { KibanaPanel } from './kibana_panel'; import { LogstashPanel } from './logstash_panel'; import { AlertsPanel } from './alerts_panel'; import { BeatsPanel } from './beats_panel'; - import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { ApmPanel } from './apm_panel'; -import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsStatus } from '../../alerts/status'; +import { + STANDALONE_CLUSTER_CLUSTER_UUID, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; export function Overview(props) { const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID; + const kibanaAlerts = KIBANA_ALERTING_ENABLED ? ( + <AlertsStatus emailAddress={props.emailAddress} /> + ) : null; + return ( <EuiPage> <EuiPageBody> @@ -30,6 +37,9 @@ export function Overview(props) { /> </h1> </EuiScreenReaderOnly> + + {kibanaAlerts} + <AlertsPanel alerts={props.cluster.alerts} changeUrl={props.changeUrl} /> {!isFromStandaloneCluster ? ( diff --git a/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts b/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts new file mode 100644 index 00000000000000..46ba603d301382 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts @@ -0,0 +1,36 @@ +/* + * 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'; + +/** + * Suppress React 16.8 act() warnings globally. + * The react teams fix won't be out of alpha until 16.9.0. + * https://github.com/facebook/react/issues/14769#issuecomment-514589856 + */ +const consoleError = console.error; // eslint-disable-line no-console +beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation((...args) => { + if (!args[0].includes('Warning: An update to %s inside a test was not wrapped in act')) { + consoleError(...args); + } + }); +}); + +export function mockUseEffects(count = 1) { + const spy = jest.spyOn(React, 'useEffect'); + for (let i = 0; i < count; i++) { + spy.mockImplementationOnce(f => f()); + } +} + +// export function mockUseEffectForDeps(deps, count = 1) { +// const spy = jest.spyOn(React, 'useEffect'); +// for (let i = 0; i < count; i++) { +// spy.mockImplementationOnce((f, depList) => { + +// }); +// } +// } diff --git a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js b/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx similarity index 94% rename from x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js rename to x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx index 9a51a885969264..22ce32103c208f 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx @@ -7,12 +7,13 @@ import React from 'react'; import { contains } from 'lodash'; import { toastNotifications } from 'ui/notify'; +// @ts-ignore import { formatMsg } from 'ui/notify/lib'; import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -export function formatMonitoringError(err) { +export function formatMonitoringError(err: any) { // TODO: We should stop using Boom for errors and instead write a custom handler to return richer error objects // then we can do better messages, such as highlighting the Cluster UUID instead of requiring it be part of the message if (err.status && err.status !== -1 && err.data) { @@ -33,10 +34,10 @@ export function formatMonitoringError(err) { return formatMsg(err); } -export function ajaxErrorHandlersProvider($injector) { +export function ajaxErrorHandlersProvider($injector: any) { const kbnUrl = $injector.get('kbnUrl'); - return err => { + return (err: any) => { if (err.status === 403) { // redirect to error message view kbnUrl.redirect('access-denied'); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts b/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts new file mode 100644 index 00000000000000..98d56f9790be47 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts @@ -0,0 +1,48 @@ +/* + * 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 { isString, isNumber, capitalize } from 'lodash'; + +export function getRequiredFieldError(field: string): string { + return i18n.translate('xpack.monitoring.alerts.migrate.manageAction.requiredFieldError', { + defaultMessage: '{field} is a required field.', + values: { + field: capitalize(field), + }, + }); +} + +export function getMissingFieldErrors(data: any, defaultData: any) { + const errors: any = {}; + + for (const key in data) { + if (!data.hasOwnProperty(key)) { + continue; + } + + if (isString(defaultData[key])) { + if (!data[key] || data[key].length === 0) { + errors[key] = getRequiredFieldError(key); + } + } else if (isNumber(defaultData[key])) { + if (isNaN(data[key]) || data[key] === 0) { + errors[key] = getRequiredFieldError(key); + } + } + } + + return errors; +} + +export function hasErrors(errors: any) { + for (const error in errors) { + if (error.length) { + return true; + } + } + return false; +} diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js index aa931368b34c28..4a2b470f04c729 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -90,7 +90,7 @@ describe('setup_mode', () => { } catch (err) { error = err; } - expect(error).toEqual( + expect(error.message).toEqual( 'Unable to interact with setup ' + 'mode because the angular injector was not previously set. This needs to be ' + 'set by calling `initSetupModeState`.' @@ -255,9 +255,9 @@ describe('setup_mode', () => { await toggleSetupMode(true); injectorModulesMock.$http.post.mockClear(); await updateSetupModeData(undefined, true); - expect( - injectorModulesMock.$http.post - ).toHaveBeenCalledWith('../api/monitoring/v1/setup/collection/cluster', { ccs: undefined }); + const url = '../api/monitoring/v1/setup/collection/cluster'; + const args = { ccs: undefined }; + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(url, args); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx similarity index 76% rename from x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js rename to x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx index 41aae013076178..d805c10247b2ed 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx @@ -6,31 +6,49 @@ import React from 'react'; import { render } from 'react-dom'; -import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { get, contains } from 'lodash'; import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; import { npSetup } from 'ui/new_platform'; +import { PluginsSetup } from 'ui/new_platform/new_platform'; +import { CloudSetup } from '../../../../../plugins/cloud/public'; +import { ajaxErrorHandlersProvider } from './ajax_error_handler'; +import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; + +interface PluginsSetupWithCloud extends PluginsSetup { + cloud: CloudSetup; +} -function isOnPage(hash) { +function isOnPage(hash: string) { return contains(window.location.hash, hash); } -const angularState = { +interface IAngularState { + injector: any; + scope: any; +} + +const angularState: IAngularState = { injector: null, scope: null, }; const checkAngularState = () => { if (!angularState.injector || !angularState.scope) { - throw 'Unable to interact with setup mode because the angular injector was not previously set.' + - ' This needs to be set by calling `initSetupModeState`.'; + throw new Error( + 'Unable to interact with setup mode because the angular injector was not previously set.' + + ' This needs to be set by calling `initSetupModeState`.' + ); } }; -const setupModeState = { +interface ISetupModeState { + enabled: boolean; + data: any; + callbacks: Function[]; +} +const setupModeState: ISetupModeState = { enabled: false, data: null, callbacks: [], @@ -38,7 +56,7 @@ const setupModeState = { export const getSetupModeState = () => setupModeState; -export const setNewlyDiscoveredClusterUuid = clusterUuid => { +export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => { const globalState = angularState.injector.get('globalState'); const executor = angularState.injector.get('$executor'); angularState.scope.$apply(() => { @@ -48,7 +66,7 @@ export const setNewlyDiscoveredClusterUuid = clusterUuid => { executor.run(); }; -export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) => { +export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => { checkAngularState(); const http = angularState.injector.get('$http'); @@ -75,19 +93,19 @@ export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) } }; -const notifySetupModeDataChange = oldData => { - setupModeState.callbacks.forEach(cb => cb(oldData)); +const notifySetupModeDataChange = (oldData?: any) => { + setupModeState.callbacks.forEach((cb: Function) => cb(oldData)); }; -export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) => { +export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - const { cloud } = npSetup.plugins; + const { cloud } = npSetup.plugins as PluginsSetupWithCloud; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const hasPermissions = get(data, '_meta.hasPermissions', false); if (isCloudEnabled || !hasPermissions) { - let text = null; + let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { defaultMessage: 'You do not have the necessary permissions to do this.', @@ -113,9 +131,9 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; if (!clusterUuid) { - const liveClusterUuid = get(data, '_meta.liveClusterUuid'); + const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( - node => node.isPartiallyMigrated || node.isFullyMigrated + (node: any) => node.isPartiallyMigrated || node.isFullyMigrated ); if (liveClusterUuid && migratedEsNodes.length > 0) { setNewlyDiscoveredClusterUuid(liveClusterUuid); @@ -140,7 +158,7 @@ export const disableElasticsearchInternalCollection = async () => { } }; -export const toggleSetupMode = inSetupMode => { +export const toggleSetupMode = (inSetupMode: boolean) => { checkAngularState(); const globalState = angularState.injector.get('globalState'); @@ -164,7 +182,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const { cloud } = npSetup.plugins; + const { cloud } = npSetup.plugins as PluginsSetupWithCloud; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const enabled = !globalState.inSetupMode && !isCloudEnabled; @@ -174,10 +192,14 @@ export const setSetupModeMenuItem = () => { ); }; -export const initSetupModeState = async ($scope, $injector, callback) => { +export const addSetupModeCallback = (callback: Function) => setupModeState.callbacks.push(callback); + +export const initSetupModeState = async ($scope: any, $injector: any, callback?: Function) => { angularState.scope = $scope; angularState.injector = $injector; - callback && setupModeState.callbacks.push(callback); + if (callback) { + setupModeState.callbacks.push(callback); + } const globalState = $injector.get('globalState'); if (globalState.inSetupMode) { diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js index 57a7850b6fd530..1bfc76b7664579 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js @@ -24,7 +24,7 @@ function getPageData($injector) { const globalState = $injector.get('globalState'); const $http = $injector.get('$http'); const Private = $injector.get('Private'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/alerts`; + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; const timeBounds = timefilter.getBounds(); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js index bec90f3230571e..e7107860d61fa8 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; +import { isEmpty } from 'lodash'; +import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; import uiRoutes from 'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; @@ -12,7 +14,11 @@ import { MonitoringViewBaseController } from '../../'; import { Overview } from 'plugins/monitoring/components/cluster/overview'; import { I18nContext } from 'ui/i18n'; import { SetupModeRenderer } from '../../../components/renderers'; -import { CODE_PATH_ALL } from '../../../../common/constants'; +import { + CODE_PATH_ALL, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -31,6 +37,7 @@ uiRoutes.when('/overview', { const monitoringClusters = $injector.get('monitoringClusters'); const globalState = $injector.get('globalState'); const showLicenseExpiration = $injector.get('showLicenseExpiration'); + const config = $injector.get('config'); super({ title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { @@ -58,7 +65,16 @@ uiRoutes.when('/overview', { $scope.$watch( () => this.data, - data => { + async data => { + if (isEmpty(data)) { + return; + } + + let emailAddress = chrome.getInjected('monitoringLegacyEmailAddress') || ''; + if (KIBANA_ALERTING_ENABLED) { + emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; + } + this.renderReact( <I18nContext> <SetupModeRenderer @@ -69,6 +85,7 @@ uiRoutes.when('/overview', { {flyoutComponent} <Overview cluster={data} + emailAddress={emailAddress} setupMode={setupMode} changeUrl={changeUrl} showLicenseExpiration={showLicenseExpiration} diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts new file mode 100644 index 00000000000000..2fec949f5692e8 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts @@ -0,0 +1,453 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment-timezone'; +import { getLicenseExpiration } from './license_expiration'; +import { + ALERT_TYPE_LICENSE_EXPIRATION, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, +} from '../../common/constants'; +import { Logger } from 'src/core/server'; +import { AlertServices } from '../../../alerting/server/types'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { AlertInstance } from '../../../alerting/server/alert_instance'; +import { + AlertState, + AlertClusterState, + AlertParams, + LicenseExpirationAlertExecutorOptions, +} from './types'; +import { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; + +function fillLicense(license: any, clusterUuid?: string) { + return { + hits: { + hits: [ + { + _source: { + license, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + }; +} + +const clusterUuid = 'a4545jhjb'; +const params: AlertParams = { + dateFormat: 'YYYY', + timezone: 'UTC', +}; + +interface MockServices { + callCluster: jest.Mock; + alertInstanceFactory: jest.Mock; + savedObjectsClient: jest.Mock; +} + +const alertExecutorOptions: LicenseExpirationAlertExecutorOptions = { + alertId: '', + startedAt: new Date(), + services: { + callCluster: (path: string, opts: any) => new Promise(resolve => resolve()), + alertInstanceFactory: (id: string) => new AlertInstance(), + savedObjectsClient: {} as jest.Mocked<SavedObjectsClientContract>, + }, + params: {}, + state: {}, + spaceId: '', + name: '', + tags: [], + createdBy: null, + updatedBy: null, +}; + +describe('getLicenseExpiration', () => { + const emailAddress = 'foo@foo.com'; + const server: any = { + newPlatform: { + __internals: { + uiSettings: { + asScopedToClient: (): any => ({ + get: () => new Promise(resolve => resolve(emailAddress)), + }), + }, + }, + }, + }; + const getMonitoringCluster: () => void = jest.fn(); + const logger: Logger = { + warn: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + info: jest.fn(), + get: jest.fn(), + }; + const getLogger = (): Logger => logger; + const ccrEnabled = false; + + afterEach(() => { + (logger.warn as jest.Mock).mockClear(); + }); + + it('should have the right id and actionGroups', () => { + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); + expect(alert.actionGroups).toEqual(['default']); + }); + + it('should return the state if no license is provided', async () => { + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const services: MockServices | AlertServices = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + const state = { foo: 1 }; + + const result = await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + }); + + expect(result).toEqual(state); + }); + + it('should log a warning if no email is provided', async () => { + const customServer: any = { + newPlatform: { + __internals: { + uiSettings: { + asScopedToClient: () => ({ + get: () => null, + }), + }, + }, + }, + }; + const alert = getLicenseExpiration(customServer, getMonitoringCluster, getLogger, ccrEnabled); + + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense({ + status: 'good', + type: 'basic', + expiry_date_in_millis: moment() + .add(7, 'days') + .valueOf(), + }) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + + const state = {}; + + await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + }); + + expect((logger.warn as jest.Mock).mock.calls.length).toBe(1); + expect(logger.warn).toHaveBeenCalledWith( + `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` + ); + }); + + it('should fire actions if going to expire', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject<SavedObjectAttributes> = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'gold', + expiry_date_in_millis: moment() + .add(7, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + + expect(newState.expiredCheckDateMS > 0).toBe(true); + expect(scheduleActions.mock.calls.length).toBe(1); + expect(scheduleActions.mock.calls[0][1].subject).toBe( + 'NEW X-Pack Monitoring: License Expiration' + ); + expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); + }); + + it('should fire actions if the user fixed their license', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject<SavedObjectAttributes> = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'gold', + expiry_date_in_millis: moment() + .add(120, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state: AlertState = { + [clusterUuid]: { + expiredCheckDateMS: moment() + .subtract(1, 'day') + .valueOf(), + ui: { isFiring: true, severity: 0, message: null, resolvedMS: 0, expirationTime: 0 }, + }, + }; + + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS).toBe(0); + expect(scheduleActions.mock.calls.length).toBe(1); + expect(scheduleActions.mock.calls[0][1].subject).toBe( + 'RESOLVED X-Pack Monitoring: License Expiration' + ); + expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); + }); + + it('should not fire actions for trial license that expire in more than 14 days', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject<SavedObjectAttributes> = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'trial', + expiry_date_in_millis: moment() + .add(15, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS).toBe(undefined); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should fire actions for trial license that in 14 days or less', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject<SavedObjectAttributes> = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise<any> => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'trial', + expiry_date_in_millis: moment() + .add(13, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS > 0).toBe(true); + expect(scheduleActions.mock.calls.length).toBe(1); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts new file mode 100644 index 00000000000000..197c5c9cdcbc72 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment-timezone'; +import { get } from 'lodash'; +import { Legacy } from 'kibana'; +import { Logger } from 'src/core/server'; +import { ALERT_TYPE_LICENSE_EXPIRATION, INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { AlertType } from '../../../alerting'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; +import { fetchDefaultEmailAddress } from '../lib/alerts/fetch_default_email_address'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; +import { + AlertLicense, + AlertState, + AlertClusterState, + AlertClusterUiState, + LicenseExpirationAlertExecutorOptions, +} from './types'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { executeActions, getUiMessage } from '../lib/alerts/license_expiration.lib'; + +const EXPIRES_DAYS = [60, 30, 14, 7]; + +export const getLicenseExpiration = ( + server: Legacy.Server, + getMonitoringCluster: any, + getLogger: (contexts: string[]) => Logger, + ccsEnabled: boolean +): AlertType => { + async function getCallCluster(services: any): Promise<any> { + const monitoringCluster = await getMonitoringCluster(); + if (!monitoringCluster) { + return services.callCluster; + } + + return monitoringCluster.callCluster; + } + + const logger = getLogger([ALERT_TYPE_LICENSE_EXPIRATION]); + return { + id: ALERT_TYPE_LICENSE_EXPIRATION, + name: 'Monitoring Alert - License Expiration', + actionGroups: ['default'], + async executor({ + services, + params, + state, + }: LicenseExpirationAlertExecutorOptions): Promise<any> { + logger.debug( + `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` + ); + + const callCluster = await getCallCluster(services); + + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (ccsEnabled) { + const availableCcs = await fetchAvailableCcs(callCluster); + if (availableCcs.length > 0) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + } + + const clusters = await fetchClusters(callCluster, esIndexPattern); + + // Fetch licensing information from cluster_stats documents + const licenses: AlertLicense[] = await fetchLicenses(callCluster, clusters, esIndexPattern); + if (licenses.length === 0) { + logger.warn(`No license found for ${ALERT_TYPE_LICENSE_EXPIRATION}.`); + return state; + } + + const uiSettings = server.newPlatform.__internals.uiSettings.asScopedToClient( + services.savedObjectsClient + ); + const dateFormat: string = await uiSettings.get<string>('dateFormat'); + const timezone: string = await uiSettings.get<string>('dateFormat:tz'); + const emailAddress = await fetchDefaultEmailAddress(uiSettings); + if (!emailAddress) { + // TODO: we can do more here + logger.warn( + `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` + ); + return; + } + + const result: AlertState = { ...state }; + + for (const license of licenses) { + const licenseState: AlertClusterState = state[license.clusterUuid] || {}; + const $expiry = moment.utc(license.expiryDateMS); + let isExpired = false; + let severity = 0; + + if (license.status !== 'active') { + isExpired = true; + severity = 2001; + } else if (license.expiryDateMS) { + for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { + if (license.type === 'trial' && i < 2) { + break; + } + + const $fromNow = moment.utc().add(EXPIRES_DAYS[i], 'days'); + if ($fromNow.isAfter($expiry)) { + isExpired = true; + severity = 1000 * i; + break; + } + } + } + + const ui: AlertClusterUiState = get<AlertClusterUiState>(licenseState, 'ui', { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + expirationTime: 0, + }); + let resolved = ui.resolvedMS; + let message = ui.message; + let expiredCheckDate = licenseState.expiredCheckDateMS; + const instance = services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION); + + if (isExpired) { + if (!licenseState.expiredCheckDateMS) { + logger.debug(`License will expire soon, sending email`); + executeActions(instance, license, $expiry, dateFormat, emailAddress); + expiredCheckDate = moment().valueOf(); + } + message = getUiMessage(license, timezone); + resolved = 0; + } else if (!isExpired && licenseState.expiredCheckDateMS) { + logger.debug(`License expiration has been resolved, sending email`); + executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + expiredCheckDate = 0; + message = getUiMessage(license, timezone, true); + resolved = moment().valueOf(); + } + + result[license.clusterUuid] = { + expiredCheckDateMS: expiredCheckDate, + ui: { + message, + expirationTime: license.expiryDateMS, + isFiring: expiredCheckDate > 0, + severity, + resolvedMS: resolved, + }, + }; + } + + return result; + }, + }; +}; diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts b/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts new file mode 100644 index 00000000000000..6346ca00dabbd2 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/types.d.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 { Moment } from 'moment'; +import { AlertExecutorOptions } from '../../../alerting'; + +export interface AlertLicense { + status: string; + type: string; + expiryDateMS: number; + clusterUuid: string; + clusterName: string; +} + +export interface AlertState { + [clusterUuid: string]: AlertClusterState; +} + +export interface AlertClusterState { + expiredCheckDateMS: number | Moment; + ui: AlertClusterUiState; +} + +export interface AlertClusterUiState { + isFiring: boolean; + severity: number; + message: string | null; + resolvedMS: number; + expirationTime: number; +} + +export interface AlertCluster { + clusterUuid: string; +} + +export interface LicenseExpirationAlertExecutorOptions extends AlertExecutorOptions { + state: AlertState; +} + +export interface AlertParams { + dateFormat: string; + timezone: string; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts new file mode 100644 index 00000000000000..4398b2dd675ec3 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { fetchAvailableCcs } from './fetch_available_ccs'; + +describe('fetchAvailableCcs', () => { + it('should call the `cluster.remoteInfo` api', async () => { + const callCluster = jest.fn(); + await fetchAvailableCcs(callCluster); + expect(callCluster).toHaveBeenCalledWith('cluster.remoteInfo'); + }); + + it('should return clusters that are connected', async () => { + const connectedRemote = 'myRemote'; + const callCluster = jest.fn().mockImplementation(() => ({ + [connectedRemote]: { + connected: true, + }, + })); + const result = await fetchAvailableCcs(callCluster); + expect(result).toEqual([connectedRemote]); + }); + + it('should not return clusters that are connected', async () => { + const disconnectedRemote = 'myRemote'; + const callCluster = jest.fn().mockImplementation(() => ({ + [disconnectedRemote]: { + connected: false, + }, + })); + const result = await fetchAvailableCcs(callCluster); + expect(result.length).toBe(0); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts new file mode 100644 index 00000000000000..34efaff93f34c7 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.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. + */ +export async function fetchAvailableCcs(callCluster: any): Promise<string[]> { + const availableCcs = []; + const response = await callCluster('cluster.remoteInfo'); + for (const remoteName in response) { + if (!response.hasOwnProperty(remoteName)) { + continue; + } + const remoteInfo = response[remoteName]; + if (remoteInfo.connected) { + availableCcs.push(remoteName); + } + } + return availableCcs; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts new file mode 100644 index 00000000000000..78eb9773df15fb --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { fetchClusters } from './fetch_clusters'; + +describe('fetchClusters', () => { + it('return a list of clusters', async () => { + const callCluster = jest.fn().mockImplementation(() => ({ + aggregations: { + clusters: { + buckets: [ + { + key: 'clusterA', + }, + ], + }, + }, + })); + const index = '.monitoring-es-*'; + const result = await fetchClusters(callCluster, index); + expect(result).toEqual([{ clusterUuid: 'clusterA' }]); + }); + + it('should limit the time period in the query', async () => { + const callCluster = jest.fn(); + const index = '.monitoring-es-*'; + await fetchClusters(callCluster, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[1].range.timestamp.gte).toBe('now-2m'); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts new file mode 100644 index 00000000000000..8ef7339618a2c0 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -0,0 +1,52 @@ +/* + * 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 { get } from 'lodash'; +import { AlertCluster } from '../../alerts/types'; + +interface AggregationResult { + key: string; +} + +export async function fetchClusters(callCluster: any, index: string): Promise<AlertCluster[]> { + const params = { + index, + filterPath: 'aggregations.clusters.buckets', + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size: 1000, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'aggregations.clusters.buckets', []).map((bucket: AggregationResult) => ({ + clusterUuid: bucket.key, + })); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts new file mode 100644 index 00000000000000..25b09b956038a0 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { fetchDefaultEmailAddress } from './fetch_default_email_address'; +import { uiSettingsServiceMock } from '../../../../../../../src/core/server/mocks'; + +describe('fetchDefaultEmailAddress', () => { + it('get the email address', async () => { + const email = 'test@test.com'; + const uiSettingsClient = uiSettingsServiceMock.createClient(); + uiSettingsClient.get.mockResolvedValue(email); + const result = await fetchDefaultEmailAddress(uiSettingsClient); + expect(result).toBe(email); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts new file mode 100644 index 00000000000000..88e4199a88256a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts @@ -0,0 +1,13 @@ +/* + * 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 { IUiSettingsClient } from 'src/core/server'; +import { MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS } from '../../../common/constants'; + +export async function fetchDefaultEmailAddress( + uiSettingsClient: IUiSettingsClient +): Promise<string> { + return await uiSettingsClient.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts new file mode 100644 index 00000000000000..dd6c074e68b1f9 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { fetchLicenses } from './fetch_licenses'; + +describe('fetchLicenses', () => { + it('return a list of licenses', async () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_name: clusterName, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + clusterName, + }, + ]); + }); + + it('should only search for the clusters provided', async () => { + const clusterUuid = 'clusterA'; + const callCluster = jest.fn(); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + }); + + it('should limit the time period in the query', async () => { + const clusterUuid = 'clusterA'; + const callCluster = jest.fn(); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + }); + + it('should give priority to the metadata name', async () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_name: 'fakeName', + cluster_uuid: clusterUuid, + cluster_settings: { + cluster: { + metadata: { + display_name: clusterName, + }, + }, + }, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + clusterName, + }, + ]); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts new file mode 100644 index 00000000000000..31a68e8aa9c3ee --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -0,0 +1,67 @@ +/* + * 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 { get } from 'lodash'; +import { AlertLicense, AlertCluster } from '../../alerts/types'; + +export async function fetchLicenses( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise<AlertLicense[]> { + const params = { + index, + filterPath: [ + 'hits.hits._source.license.*', + 'hits.hits._source.cluster_settings.cluster.metadata.display_name', + 'hits.hits._source.cluster_uuid', + 'hits.hits._source.cluster_name', + ], + body: { + size: 1, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map(cluster => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get<any>(response, 'hits.hits', []).map((hit: any) => { + const clusterName: string = + get(hit, '_source.cluster_settings.cluster.metadata.display_name') || + get(hit, '_source.cluster_name') || + get(hit, '_source.cluster_uuid'); + const rawLicense: any = get(hit, '_source.license', {}); + const license: AlertLicense = { + status: rawLicense.status, + type: rawLicense.type, + expiryDateMS: rawLicense.expiry_date_in_millis, + clusterUuid: get(hit, '_source.cluster_uuid'), + clusterName, + }; + return license; + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts new file mode 100644 index 00000000000000..9f7c1d5a994d27 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment'; +import { get } from 'lodash'; +import { AlertClusterState } from '../../alerts/types'; +import { ALERT_TYPES, LOGGING_TAG } from '../../../common/constants'; + +export async function fetchStatus( + callCluster: any, + start: number, + end: number, + clusterUuid: string, + server: any +): Promise<any[]> { + // TODO: this shouldn't query task manager directly but rather + // use an api exposed by the alerting/actions plugin + // See https://github.com/elastic/kibana/issues/48442 + const statuses = await Promise.all( + ALERT_TYPES.map( + type => + new Promise(async (resolve, reject) => { + try { + const params = { + index: '.kibana_task_manager', + filterPath: ['hits.hits._source.task.state'], + body: { + size: 1, + sort: [{ updated_at: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + term: { + 'task.taskType': `alerting:${type}`, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + const state = get(response, 'hits.hits[0]._source.task.state', '{}'); + const clusterState: AlertClusterState = get<AlertClusterState>( + JSON.parse(state), + `alertTypeState.${clusterUuid}`, + { + expiredCheckDateMS: 0, + ui: { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + expirationTime: 0, + }, + } + ); + const isInBetween = moment(clusterState.ui.resolvedMS).isBetween(start, end); + if (clusterState.ui.isFiring || isInBetween) { + return resolve({ + type, + ...clusterState.ui, + }); + } + return resolve(false); + } catch (err) { + const reason = get(err, 'body.error.type'); + if (reason === 'index_not_found_exception') { + server.log( + ['error', LOGGING_TAG], + `Unable to fetch alerts. Alerts depends on task manager, which has not been started yet.` + ); + } else { + server.log(['error', LOGGING_TAG], err.message); + } + return resolve(false); + } + }) + ) + ); + + return statuses.filter(Boolean); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts new file mode 100644 index 00000000000000..a5eb1049861612 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.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 { getCcsIndexPattern } from './get_ccs_index_pattern'; + +describe('getCcsIndexPattern', () => { + it('should return an index pattern including remotes', () => { + const remotes = ['Remote1', 'Remote2']; + const index = '.monitoring-es-*'; + const result = getCcsIndexPattern(index, remotes); + expect(result).toBe('.monitoring-es-*,Remote1:.monitoring-es-*,Remote2:.monitoring-es-*'); + }); + + it('should return an index pattern from multiple index patterns including remotes', () => { + const remotes = ['Remote1', 'Remote2']; + const index = '.monitoring-es-*,.monitoring-kibana-*'; + const result = getCcsIndexPattern(index, remotes); + expect(result).toBe( + '.monitoring-es-*,.monitoring-kibana-*,Remote1:.monitoring-es-*,Remote2:.monitoring-es-*,Remote1:.monitoring-kibana-*,Remote2:.monitoring-kibana-*' + ); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts new file mode 100644 index 00000000000000..b562fde2a0810f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export function getCcsIndexPattern(indexPattern: string, remotes: string[]): string { + return `${indexPattern},${indexPattern + .split(',') + .map(pattern => { + return remotes.map(remoteName => `${remoteName}:${pattern}`).join(','); + }) + .join(',')}`; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts new file mode 100644 index 00000000000000..1a2eb1e44be843 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment-timezone'; +import { executeActions, getUiMessage } from './license_expiration.lib'; + +describe('licenseExpiration lib', () => { + describe('executeActions', () => { + const clusterName = 'clusterA'; + const instance: any = { scheduleActions: jest.fn() }; + const license: any = { clusterName }; + const $expiry = moment('2020-01-20'); + const dateFormat = 'dddd, MMMM Do YYYY, h:mm:ss a'; + const emailAddress = 'test@test.com'; + + beforeEach(() => { + instance.scheduleActions.mockClear(); + }); + + it('should schedule actions when firing', () => { + executeActions(instance, license, $expiry, dateFormat, emailAddress, false); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'NEW X-Pack Monitoring: License Expiration', + message: `Cluster '${clusterName}' license is going to expire on Monday, January 20th 2020, 12:00:00 am. Please update your license.`, + to: emailAddress, + }); + }); + + it('should schedule actions when resolved', () => { + executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'RESOLVED X-Pack Monitoring: License Expiration', + message: `This cluster alert has been resolved: Cluster '${clusterName}' license was going to expire on Monday, January 20th 2020, 12:00:00 am.`, + to: emailAddress, + }); + }); + }); + + describe('getUiMessage', () => { + const timezone = 'Europe/London'; + const license: any = { expiryDateMS: moment.tz('2020-01-20 08:00:00', timezone).utc() }; + + it('should return a message when firing', () => { + const message = getUiMessage(license, timezone, false); + expect(message).toBe(`This cluster's license is going to expire in #relative at #absolute.`); + }); + + it('should return a message when resolved', () => { + const message = getUiMessage(license, timezone, true); + expect(message).toBe(`This cluster's license is active.`); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts new file mode 100644 index 00000000000000..8a75fc1fbbd82f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Moment } from 'moment-timezone'; +import { i18n } from '@kbn/i18n'; +import { AlertInstance } from '../../../../alerting/server/alert_instance'; +import { AlertLicense } from '../../alerts/types'; + +const RESOLVED_SUBJECT = i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.resolvedSubject', + { + defaultMessage: 'RESOLVED X-Pack Monitoring: License Expiration', + } +); + +const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.licenseExpiration.newSubject', { + defaultMessage: 'NEW X-Pack Monitoring: License Expiration', +}); + +export function executeActions( + instance: AlertInstance, + license: AlertLicense, + $expiry: Moment, + dateFormat: string, + emailAddress: string, + resolved: boolean = false +) { + if (resolved) { + instance.scheduleActions('default', { + subject: RESOLVED_SUBJECT, + message: `This cluster alert has been resolved: Cluster '${ + license.clusterName + }' license was going to expire on ${$expiry.format(dateFormat)}.`, + to: emailAddress, + }); + } else { + instance.scheduleActions('default', { + subject: NEW_SUBJECT, + message: `Cluster '${license.clusterName}' license is going to expire on ${$expiry.format( + dateFormat + )}. Please update your license.`, + to: emailAddress, + }); + } +} + +export function getUiMessage(license: AlertLicense, timezone: string, resolved: boolean = false) { + if (resolved) { + return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { + defaultMessage: `This cluster's license is active.`, + }); + } + return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { + defaultMessage: `This cluster's license is going to expire in #relative at #absolute.`, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 2b080a5c333fcf..a5426dc04545eb 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -16,6 +16,7 @@ import { getBeatsForClusters } from '../beats'; import { alertsClustersAggregation } from '../../cluster_alerts/alerts_clusters_aggregation'; import { alertsClusterSearch } from '../../cluster_alerts/alerts_cluster_search'; import { checkLicense as checkLicenseForAlerts } from '../../cluster_alerts/check_license'; +import { fetchStatus } from '../alerts/fetch_status'; import { getClustersSummary } from './get_clusters_summary'; import { CLUSTER_ALERTS_SEARCH_SIZE, @@ -27,6 +28,7 @@ import { CODE_PATH_LOGSTASH, CODE_PATH_BEATS, CODE_PATH_APM, + KIBANA_ALERTING_ENABLED, } from '../../../common/constants'; import { getApmsForClusters } from '../apm/get_apms_for_clusters'; import { i18n } from '@kbn/i18n'; @@ -99,15 +101,31 @@ export async function getClustersFromRequest( if (mlJobs !== null) { cluster.ml = { jobs: mlJobs }; } - const alerts = isInCodePath(codePaths, [CODE_PATH_ALERTS]) - ? await alertsClusterSearch(req, alertsIndex, cluster, checkLicenseForAlerts, { + + if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { + if (KIBANA_ALERTING_ENABLED) { + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const callCluster = (...args) => callWithRequest(req, ...args); + cluster.alerts = await fetchStatus( + callCluster, start, end, - size: CLUSTER_ALERTS_SEARCH_SIZE, - }) - : null; - if (alerts) { - cluster.alerts = alerts; + cluster.cluster_uuid, + req.server + ); + } else { + cluster.alerts = await alertsClusterSearch( + req, + alertsIndex, + cluster, + checkLicenseForAlerts, + { + start, + end, + size: CLUSTER_ALERTS_SEARCH_SIZE, + } + ); + } } cluster.logs = isInCodePath(codePaths, [CODE_PATH_LOGS]) diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/index.ts b/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js similarity index 51% rename from x-pack/legacy/plugins/kuery_autocomplete/public/index.ts rename to x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js index c2241f424511ff..89cbf20d9b56f7 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/index.ts +++ b/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/public'; -import { KueryAutocompletePlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); +export async function getDateFormat(req) { + return await req.getUiSettingsService().get('dateFormat'); } diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 5f52e0c6a983b7..a12b48510a6ff7 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -348,7 +348,6 @@ export const getCollectionStatus = async ( }, }; } - const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index ef346e95ad0757..50e5319a0f5263 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -5,12 +5,17 @@ */ import { i18n } from '@kbn/i18n'; -import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../common/constants'; +import { + LOGGING_TAG, + KIBANA_MONITORING_LOGGING_TAG, + KIBANA_ALERTING_ENABLED, +} from '../common/constants'; import { requireUIRoutes } from './routes'; import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; +import { getLicenseExpiration } from './alerts/license_expiration'; import { parseElasticsearchConfig } from './es_client/parse_elasticsearch_config'; export class Plugin { @@ -133,5 +138,37 @@ export class Plugin { showCgroupMetricsLogstash: config.get('monitoring.ui.container.logstash.enabled'), // Note, not currently used, but see https://github.com/elastic/x-pack-kibana/issues/1559 part 2 }; }); + + if (KIBANA_ALERTING_ENABLED && plugins.alerting) { + // this is not ready right away but we need to register alerts right away + async function getMonitoringCluster() { + const configs = config.get('xpack.monitoring.elasticsearch'); + if (configs.hosts) { + const monitoringCluster = plugins.elasticsearch.getCluster('monitoring'); + const { username, password } = configs; + const fakeRequest = { + headers: { + authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, + }, + }; + return { + callCluster: (...args) => monitoringCluster.callWithRequest(fakeRequest, ...args), + }; + } + return null; + } + + function getLogger(contexts) { + return core.logger.get('plugins', LOGGING_TAG, ...contexts); + } + plugins.alerting.setup.registerType( + getLicenseExpiration( + core._hapi, + getMonitoringCluster, + getLogger, + config.get('xpack.monitoring.ccs.enabled') + ) + ); + } } } diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js new file mode 100644 index 00000000000000..f87683effe437c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js @@ -0,0 +1,89 @@ +/* + * 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 Joi from 'joi'; +import { isFunction } from 'lodash'; +import { + ALERT_TYPE_LICENSE_EXPIRATION, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, +} from '../../../../../common/constants'; + +async function createAlerts(req, alertsClient, { selectedEmailActionId }) { + const createdAlerts = []; + + // Create alerts + const ALERT_TYPES = { + [ALERT_TYPE_LICENSE_EXPIRATION]: { + schedule: { interval: '10s' }, + actions: [ + { + group: 'default', + id: selectedEmailActionId, + params: { + subject: '{{context.subject}}', + message: `{{context.message}}`, + to: ['{{context.to}}'], + }, + }, + ], + }, + }; + + for (const alertTypeId of Object.keys(ALERT_TYPES)) { + const existingAlert = await alertsClient.find({ + options: { + search: alertTypeId, + }, + }); + if (existingAlert.total === 1) { + await alertsClient.delete({ id: existingAlert.data[0].id }); + } + + const result = await alertsClient.create({ + data: { + enabled: true, + alertTypeId, + ...ALERT_TYPES[alertTypeId], + }, + }); + createdAlerts.push(result); + } + + return createdAlerts; +} + +async function saveEmailAddress(emailAddress, uiSettingsService) { + await uiSettingsService.set(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, emailAddress); +} + +export function createKibanaAlertsRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/alerts', + config: { + validate: { + payload: Joi.object({ + selectedEmailActionId: Joi.string().required(), + emailAddress: Joi.string().required(), + }), + }, + }, + async handler(req, headers) { + const { emailAddress, selectedEmailActionId } = req.payload; + const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; + if (!alertsClient) { + return headers.response().code(404); + } + + const [alerts, emailResponse] = await Promise.all([ + createAlerts(req, alertsClient, { ...req.params, selectedEmailActionId }), + saveEmailAddress(emailAddress, req.getUiSettingsService()), + ]); + + return { alerts, emailResponse }; + }, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js index cdcd776b349fc6..246cdfde97cff8 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js @@ -4,54 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; -import { checkLicense } from '../../../../cluster_alerts/check_license'; -import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; - -/* - * Cluster Alerts route. - */ -export function clusterAlertsRoute(server) { - server.route({ - method: 'POST', - path: '/api/monitoring/v1/clusters/{clusterUuid}/alerts', - config: { - validate: { - params: Joi.object({ - clusterUuid: Joi.string().required(), - }), - payload: Joi.object({ - ccs: Joi.string().optional(), - timeRange: Joi.object({ - min: Joi.date().required(), - max: Joi.date().required(), - }).required(), - }), - }, - }, - handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; - const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); - const options = { - start: req.payload.timeRange.min, - end: req.payload.timeRange.max, - }; - - return getClusterLicense(req, esIndexPattern, clusterUuid).then(license => - alertsClusterSearch( - req, - alertsIndex, - { cluster_uuid: clusterUuid, license }, - checkLicense, - options - ) - ); - }, - }); -} +export * from './legacy_alerts'; +export * from './alerts'; diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js new file mode 100644 index 00000000000000..a3049f0f3e2d2e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js @@ -0,0 +1,57 @@ +/* + * 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 Joi from 'joi'; +import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; +import { checkLicense } from '../../../../cluster_alerts/check_license'; +import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; +import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; + +/* + * Cluster Alerts route. + */ +export function legacyClusterAlertsRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/clusters/{clusterUuid}/legacy_alerts', + config: { + validate: { + params: Joi.object({ + clusterUuid: Joi.string().required(), + }), + payload: Joi.object({ + ccs: Joi.string().optional(), + timeRange: Joi.object({ + min: Joi.date().required(), + max: Joi.date().required(), + }).required(), + }), + }, + }, + handler(req) { + const config = server.config(); + const ccs = req.payload.ccs; + const clusterUuid = req.params.clusterUuid; + const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); + const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); + const options = { + start: req.payload.timeRange.min, + end: req.payload.timeRange.max, + }; + + return getClusterLicense(req, esIndexPattern, clusterUuid).then(license => + alertsClusterSearch( + req, + alertsIndex, + { cluster_uuid: clusterUuid, license }, + checkLicense, + options + ) + ); + }, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js index baffbfd5f3f6f2..de0213ec84689d 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js @@ -6,7 +6,7 @@ // all routes for the app export { checkAccessRoute } from './check_access'; -export { clusterAlertsRoute } from './alerts/'; +export * from './alerts/'; export { beatsDetailRoute, beatsListingRoute, beatsOverviewRoute } from './beats'; export { clusterRoute, clustersRoute } from './cluster'; export { diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js index 9251deb673bd11..49f167b0f1b103 100644 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ b/x-pack/legacy/plugins/monitoring/ui_exports.js @@ -6,6 +6,10 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; +import { + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from './common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; /** @@ -14,28 +18,48 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; * app (injectDefaultVars and hacks) * @return {Object} data per Kibana plugin uiExport schema */ -export const getUiExports = () => ({ - app: { - title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { - defaultMessage: 'Stack Monitoring', - }), - order: 9002, - description: i18n.translate('xpack.monitoring.uiExportsDescription', { - defaultMessage: 'Monitoring for Elastic Stack', - }), - icon: 'plugins/monitoring/icons/monitoring.svg', - euiIconType: 'monitoringApp', - linkToLastSubUrl: false, - main: 'plugins/monitoring/monitoring', - category: DEFAULT_APP_CATEGORIES.management, - }, - injectDefaultVars(server) { - const config = server.config(); - return { - monitoringUiEnabled: config.get('monitoring.ui.enabled'), +export const getUiExports = () => { + const uiSettingDefaults = {}; + if (KIBANA_ALERTING_ENABLED) { + uiSettingDefaults[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS] = { + name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { + defaultMessage: 'Alerting email address', + }), + value: '', + description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { + defaultMessage: `The default email address to receive alerts from Stack Monitoring`, + }), + category: ['monitoring'], }; - }, - hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], - home: ['plugins/monitoring/register_feature'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), -}); + } + + return { + app: { + title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { + defaultMessage: 'Stack Monitoring', + }), + order: 9002, + description: i18n.translate('xpack.monitoring.uiExportsDescription', { + defaultMessage: 'Monitoring for Elastic Stack', + }), + icon: 'plugins/monitoring/icons/monitoring.svg', + euiIconType: 'monitoringApp', + linkToLastSubUrl: false, + main: 'plugins/monitoring/monitoring', + category: DEFAULT_APP_CATEGORIES.management, + }, + injectDefaultVars(server) { + const config = server.config(); + return { + monitoringUiEnabled: config.get('monitoring.ui.enabled'), + monitoringLegacyEmailAddress: config.get( + 'monitoring.cluster_alerts.email_notifications.email_address' + ), + }; + }, + uiSettingDefaults, + hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], + home: ['plugins/monitoring/register_feature'], + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + }; +}; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js index 06ba36f600b9ba..b21d6283320274 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.js @@ -11,6 +11,7 @@ import { CancellationToken } from '../../../common/cancellation_token'; import { fieldFormats } from '../../../../../../../src/plugins/data/server'; import { LevelLogger } from '../../../server/lib/level_logger'; import { executeJobFactory } from './execute_job'; +import { setFieldFormats } from '../../../server/services'; const delay = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); @@ -73,7 +74,7 @@ describe('CSV Execute Job', function() { uiSettingsGetStub.withArgs('csv:separator').returns(','); uiSettingsGetStub.withArgs('csv:quoteValues').returns(true); - mockServer = { + setFieldFormats({ fieldFormatServiceFactory: function() { const uiConfigMock = {}; uiConfigMock['format:defaultTypeMap'] = { @@ -86,6 +87,17 @@ describe('CSV Execute Job', function() { return fieldFormatsRegistry; }, + }); + + mockServer = { + expose: function() {}, + plugins: { + elasticsearch: { + getCluster: function() { + return clusterStub; + }, + }, + }, config: function() { return { get: configGetStub, diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 280bbf13fa9928..9f94a755cf6555 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -13,6 +13,7 @@ import { ESQueueWorkerExecuteFn, ExecuteJobFactory, Logger, ServerFacade } from import { JobDocPayloadDiscoverCsv } from '../types'; import { fieldFormatMapFactory } from './lib/field_format_map'; import { createGenerateCsv } from './lib/generate_csv'; +import { getFieldFormats } from '../../../server/services'; export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn< JobDocPayloadDiscoverCsv @@ -94,7 +95,7 @@ export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn< const [formatsMap, uiSettings] = await Promise.all([ (async () => { - const fieldFormats = await server.fieldFormatServiceFactory(uiConfig); + const fieldFormats = await getFieldFormats().fieldFormatServiceFactory(uiConfig); return fieldFormatMapFactory(indexPatternSavedObject, fieldFormats); })(), (async () => { diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index d2a68e309a4b37..cbafc4b1ecc4b3 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -6,23 +6,16 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; -import { IUiSettingsClient } from 'kibana/server'; import { resolve } from 'path'; -import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../plugins/security/server'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; -import { LegacySetup, ReportingPlugin, reportingPluginFactory } from './server/plugin'; +import { legacyInit } from './server/legacy'; import { ReportingConfigOptions, ReportingPluginSpecOptions } from './types.d'; const kbToBase64Length = (kb: number) => { return Math.floor((kb * 1024 * 8) / 6); }; -interface ReportingDeps { - data: DataPluginStart; -} - export const reporting = (kibana: any) => { return new kibana.Plugin({ id: PLUGIN_ID, @@ -68,35 +61,7 @@ export const reporting = (kibana: any) => { }, async init(server: Legacy.Server) { - const coreSetup = server.newPlatform.setup.core; - - const fieldFormatServiceFactory = async (uiSettings: IUiSettingsClient) => { - const [, plugins] = await coreSetup.getStartServices(); - const { fieldFormats } = (plugins as ReportingDeps).data; - - return fieldFormats.fieldFormatServiceFactory(uiSettings); - }; - - const __LEGACY: LegacySetup = { - config: server.config, - info: server.info, - route: server.route.bind(server), - plugins: { xpack_main: server.plugins.xpack_main }, - savedObjects: server.savedObjects, - fieldFormatServiceFactory, - uiSettingsServiceFactory: server.uiSettingsServiceFactory, - }; - - const plugin: ReportingPlugin = reportingPluginFactory( - server.newPlatform.coreContext, - __LEGACY, - this - ); - await plugin.setup(coreSetup, { - elasticsearch: coreSetup.elasticsearch, - security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, - usageCollection: server.newPlatform.setup.plugins.usageCollection, - }); + return legacyInit(server, this); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/server/index.ts b/x-pack/legacy/plugins/reporting/server/index.ts new file mode 100644 index 00000000000000..438a3fd595a107 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/index.ts @@ -0,0 +1,12 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { ReportingPlugin as Plugin } from './plugin'; + +export const plugin = (context: PluginInitializerContext) => { + return new Plugin(context); +}; diff --git a/x-pack/legacy/plugins/reporting/server/legacy.ts b/x-pack/legacy/plugins/reporting/server/legacy.ts new file mode 100644 index 00000000000000..c80aef06cf270b --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/legacy.ts @@ -0,0 +1,49 @@ +/* + * 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 { Legacy } from 'kibana'; +import { PluginInitializerContext } from 'src/core/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; +import { ReportingPluginSpecOptions } from '../types'; +import { plugin } from './index'; +import { LegacySetup, ReportingStartDeps } from './plugin'; + +const buildLegacyDependencies = ( + server: Legacy.Server, + reportingPlugin: ReportingPluginSpecOptions +): LegacySetup => ({ + config: server.config, + info: server.info, + route: server.route.bind(server), + plugins: { + elasticsearch: server.plugins.elasticsearch, + xpack_main: server.plugins.xpack_main, + reporting: reportingPlugin, + }, + savedObjects: server.savedObjects, + uiSettingsServiceFactory: server.uiSettingsServiceFactory, +}); + +export const legacyInit = async ( + server: Legacy.Server, + reportingPlugin: ReportingPluginSpecOptions +) => { + const coreSetup = server.newPlatform.setup.core; + const pluginInstance = plugin(server.newPlatform.coreContext as PluginInitializerContext); + + await pluginInstance.setup(coreSetup, { + elasticsearch: coreSetup.elasticsearch, + security: server.newPlatform.setup.plugins.security as SecurityPluginSetup, + usageCollection: server.newPlatform.setup.plugins.usageCollection, + __LEGACY: buildLegacyDependencies(server, reportingPlugin), + }); + + // Schedule to call the "start" hook only after start dependencies are ready + coreSetup.getStartServices().then(([core, plugins]) => + pluginInstance.start(core, { + data: (plugins as ReportingStartDeps).data, + }) + ); +}; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index a2938d442f7df0..ef7b01f8e9c15b 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -9,12 +9,12 @@ import { CoreSetup, CoreStart, ElasticsearchServiceSetup, - LoggerFactory, Plugin, + PluginInitializerContext, } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; // @ts-ignore import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; @@ -24,95 +24,77 @@ import { ReportingPluginSpecOptions } from '../types.d'; import { createBrowserDriverFactory } from './browsers'; import { checkLicenseFactory, getExportTypesRegistry, LevelLogger, runValidations } from './lib'; import { registerRoutes } from './routes'; +import { setFieldFormats } from './services'; import { registerReportingUsageCollector } from './usage'; -export interface ReportingInitializerContext { - logger: LoggerFactory; -} - -// For now there is no exposed functionality to other plugins -export type ReportingSetup = object; -export type ReportingStart = object; - export interface ReportingSetupDeps { elasticsearch: ElasticsearchServiceSetup; usageCollection: UsageCollectionSetup; security: SecurityPluginSetup; + __LEGACY: LegacySetup; +} + +export interface ReportingStartDeps { + data: DataPluginStart; } -export type ReportingStartDeps = object; export interface LegacySetup { config: Legacy.Server['config']; info: Legacy.Server['info']; - plugins: { xpack_main: XPackMainPlugin & { status?: any } }; + plugins: { + elasticsearch: Legacy.Server['plugins']['elasticsearch']; + xpack_main: XPackMainPlugin & { + status?: any; + }; + reporting: ReportingPluginSpecOptions; + }; route: Legacy.Server['route']; savedObjects: Legacy.Server['savedObjects']; uiSettingsServiceFactory: Legacy.Server['uiSettingsServiceFactory']; - fieldFormatServiceFactory: DataPluginStart['fieldFormats']['fieldFormatServiceFactory']; } -export type ReportingPlugin = Plugin< - ReportingSetup, - ReportingStart, - ReportingSetupDeps, - ReportingStartDeps ->; - -/* We need a factory that returns an instance of the class because the class - * implementation itself restricts against having Legacy dependencies passed - * into `setup`. The factory parameters take the legacy dependencies, and the - * `setup` method gets it from enclosure */ -export function reportingPluginFactory( - initializerContext: ReportingInitializerContext, - __LEGACY: LegacySetup, - legacyPlugin: ReportingPluginSpecOptions -) { - return new (class ReportingPlugin implements ReportingPlugin { - private initializerContext: ReportingInitializerContext; - - constructor(context: ReportingInitializerContext) { - this.initializerContext = context; - } - - public async setup(core: CoreSetup, plugins: ReportingSetupDeps): Promise<ReportingSetup> { - const exportTypesRegistry = getExportTypesRegistry(); - const { usageCollection, elasticsearch } = plugins; - - let isCollectorReady = false; - // Register a function with server to manage the collection of usage stats - registerReportingUsageCollector( - usageCollection, - __LEGACY, - () => isCollectorReady, - exportTypesRegistry - ); - - const logger = new LevelLogger(this.initializerContext.logger.get('reporting')); - const browserDriverFactory = await createBrowserDriverFactory(__LEGACY, logger); - - logConfiguration(__LEGACY, logger); - runValidations(__LEGACY, elasticsearch, logger, browserDriverFactory); - - const { xpack_main: xpackMainPlugin } = __LEGACY.plugins; - mirrorPluginStatus(xpackMainPlugin, legacyPlugin); - const checkLicense = checkLicenseFactory(exportTypesRegistry); - (xpackMainPlugin as any).status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); - }); - - // Post initialization of the above code, the collector is now ready to fetch its data - isCollectorReady = true; - - // Reporting routes - registerRoutes(__LEGACY, plugins, exportTypesRegistry, browserDriverFactory, logger); - - return {}; - } - - public start(core: CoreStart, plugins: ReportingStartDeps): ReportingStart { - return {}; - } - })(initializerContext); +export class ReportingPlugin implements Plugin<void, void, ReportingSetupDeps, ReportingStartDeps> { + constructor(private context: PluginInitializerContext) {} + + public async setup(core: CoreSetup, plugins: ReportingSetupDeps) { + const { elasticsearch, usageCollection, __LEGACY } = plugins; + const exportTypesRegistry = getExportTypesRegistry(); + + let isCollectorReady = false; + + // Register a function with server to manage the collection of usage stats + registerReportingUsageCollector( + usageCollection, + __LEGACY, + () => isCollectorReady, + exportTypesRegistry + ); + + const logger = new LevelLogger(this.context.logger.get('reporting')); + const browserDriverFactory = await createBrowserDriverFactory(__LEGACY, logger); + + logConfiguration(__LEGACY, logger); + runValidations(__LEGACY, elasticsearch, logger, browserDriverFactory); + + const { xpack_main: xpackMainPlugin, reporting } = __LEGACY.plugins; + mirrorPluginStatus(xpackMainPlugin, reporting); + + const checkLicense = checkLicenseFactory(exportTypesRegistry); + + (xpackMainPlugin as any).status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); + }); + + // Post initialization of the above code, the collector is now ready to fetch its data + isCollectorReady = true; + + // Reporting routes + registerRoutes(__LEGACY, plugins, exportTypesRegistry, browserDriverFactory, logger); + } + + public start(core: CoreStart, plugins: ReportingStartDeps) { + setFieldFormats(plugins.data.fieldFormats); + } } diff --git a/x-pack/legacy/plugins/reporting/server/services.ts b/x-pack/legacy/plugins/reporting/server/services.ts new file mode 100644 index 00000000000000..7d15d2e1af1ae9 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/services.ts @@ -0,0 +1,12 @@ +/* + * 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 { createGetterSetter } from '../../../../../src/plugins/kibana_utils/server'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; + +export const [getFieldFormats, setFieldFormats] = createGetterSetter< + DataPluginStart['fieldFormats'] +>('FieldFormats'); 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 7834bb4511dc64..19ad0d452feb17 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 @@ -5,7 +5,7 @@ */ import { IndexPatternMapping } from '../types'; -import { IndexPatternSavedObject } from '../../ml_popover/types'; +import { IndexPatternSavedObject } from '../../../hooks/types'; export const mockIndexPatternIds: IndexPatternMapping[] = [ { title: 'filebeat-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, @@ -425,8 +425,7 @@ export const mockLayerListMixed = [ export const mockAPMIndexPattern: IndexPatternSavedObject = { id: 'apm-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-*', }, @@ -435,8 +434,7 @@ export const mockAPMIndexPattern: IndexPatternSavedObject = { export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { id: 'apm-7.*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-7.*', }, @@ -445,8 +443,7 @@ export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { export const mockFilebeatIndexPattern: IndexPatternSavedObject = { id: 'filebeat-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'filebeat-*', }, @@ -455,8 +452,7 @@ export const mockFilebeatIndexPattern: IndexPatternSavedObject = { export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { id: 'auditbeat-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'auditbeat-*', }, @@ -465,8 +461,7 @@ export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { id: 'apm-*-transaction*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-*-transaction*', }, @@ -475,8 +470,7 @@ export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { export const mockGlobIndexPattern: IndexPatternSavedObject = { id: '*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: '*', }, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 2d4714401f3b31..e370cbbf64a4a1 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -21,7 +21,7 @@ import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; import * as i18n from './translations'; import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; -import { IndexPatternSavedObject } from '../ml_popover/types'; +import { IndexPatternSavedObject } from '../../hooks/types'; /** * Creates MapEmbeddable with provided initial configuration diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap index ee76657c8d27a4..4dd77842894c65 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap @@ -3,25 +3,17 @@ exports[`Select Flow Direction rendering it renders the basic group button for uni-direction and bi-direction 1`] = ` <EuiFilterGroup> <EuiFilterButton - color="text" data-test-subj="uniDirectional" - grow={true} hasActiveFilters={true} - iconSide="right" onClick={[Function]} - type="button" withNext={true} > Unidirectional </EuiFilterButton> <EuiFilterButton - color="text" data-test-subj="biDirectional" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" > Bidirectional </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts index 10b2538d1e7852..cb84d9182d2e01 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - +import { npStart } from 'ui/new_platform'; import { Anomalies, InfluencerInput, CriteriaFields } from '../types'; import { throwIfNotOk } from '../../../hooks/api/api'; + export interface Body { jobIds: string[]; criteriaFields: CriteriaFields[]; @@ -22,17 +22,17 @@ export interface Body { } export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise<Anomalies> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/results/anomalies_table_data`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify(body), - headers: { - 'content-Type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - }); - await throwIfNotOk(response); - return response.json(); + const response = await npStart.core.http.fetch<Anomalies>( + '/api/ml/results/anomalies_table_data', + { + method: 'POST', + body: JSON.stringify(body), + asResponse: true, + asSystemRequest: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts index 13339510284945..dcfd7365f8e0d5 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { InfluencerInput, MlCapabilities } from '../types'; import { throwIfNotOk } from '../../../hooks/api/api'; @@ -23,16 +23,13 @@ export interface Body { } export const getMlCapabilities = async (signal: AbortSignal): Promise<MlCapabilities> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/ml_capabilities`, { + const response = await npStart.core.http.fetch<MlCapabilities>('/api/ml/ml_capabilities', { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-Type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, }); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx index a04b8f4b996536..cf939d8e09b7ec 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; + import { CheckRecognizerProps, CloseJobsResponse, @@ -31,21 +32,18 @@ export const checkRecognizer = async ({ indexPatternName, signal, }: CheckRecognizerProps): Promise<RecognizerModule[]> => { - const response = await fetch( - `${chrome.getBasePath()}/api/ml/modules/recognize/${indexPatternName}`, + const response = await npStart.core.http.fetch<RecognizerModule[]>( + `/api/ml/modules/recognize/${indexPatternName}`, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, } ); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -55,18 +53,18 @@ export const checkRecognizer = async ({ * @param signal to cancel request */ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Promise<Module[]> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/get_module/${moduleId}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - }); - await throwIfNotOk(response); - return response.json(); + const response = await npStart.core.http.fetch<Module[]>( + `/api/ml/modules/get_module/${moduleId}`, + { + method: 'GET', + asResponse: true, + asSystemRequest: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -77,7 +75,6 @@ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Pr * @param jobIdErrorFilter - if provided, filters all errors except for given jobIds * @param groups - list of groups to add to jobs being installed * @param prefix - prefix to be added to job name - * @param headers optional headers to add */ export const setupMlJob = async ({ configTemplate, @@ -86,25 +83,26 @@ export const setupMlJob = async ({ groups = ['siem'], prefix = '', }: MlSetupArgs): Promise<SetupMlResponse> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/setup/${configTemplate}`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - prefix, - groups, - indexPatternName, - startDatafeed: false, - useDedicatedIndex: true, - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); - await throwIfNotOk(response); - const json = await response.json(); + const response = await npStart.core.http.fetch<SetupMlResponse>( + `/api/ml/modules/setup/${configTemplate}`, + { + method: 'POST', + body: JSON.stringify({ + prefix, + groups, + indexPatternName, + startDatafeed: false, + useDedicatedIndex: true, + }), + asResponse: true, + asSystemRequest: true, + } + ); + + await throwIfNotOk(response.response); + const json = response.body!; throwIfErrorAttachedToSetup(json, jobIdErrorFilter); + return json; }; @@ -121,22 +119,23 @@ export const startDatafeeds = async ({ datafeedIds: string[]; start: number; }): Promise<StartDatafeedResponse> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/force_start_datafeeds`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - datafeedIds, - ...(start !== 0 && { start }), - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); - await throwIfNotOk(response); - const json = await response.json(); + const response = await npStart.core.http.fetch<StartDatafeedResponse>( + '/api/ml/jobs/force_start_datafeeds', + { + method: 'POST', + body: JSON.stringify({ + datafeedIds, + ...(start !== 0 && { start }), + }), + asResponse: true, + asSystemRequest: true, + } + ); + + await throwIfNotOk(response.response); + const json = response.body!; throwIfErrorAttached(json, datafeedIds); + return json; }; @@ -144,49 +143,46 @@ export const startDatafeeds = async ({ * Stops the given dataFeedIds and sets the corresponding Job's jobState to closed * * @param datafeedIds - * @param headers optional headers to add */ export const stopDatafeeds = async ({ datafeedIds, }: { datafeedIds: string[]; }): Promise<[StopDatafeedResponse | ErrorResponse, CloseJobsResponse]> => { - const stopDatafeedsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/stop_datafeeds`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - datafeedIds, - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); + const stopDatafeedsResponse = await npStart.core.http.fetch<StopDatafeedResponse>( + '/api/ml/jobs/stop_datafeeds', + { + method: 'POST', + body: JSON.stringify({ + datafeedIds, + }), + asResponse: true, + asSystemRequest: true, + } + ); - await throwIfNotOk(stopDatafeedsResponse); - const stopDatafeedsResponseJson = await stopDatafeedsResponse.json(); + await throwIfNotOk(stopDatafeedsResponse.response); + const stopDatafeedsResponseJson = stopDatafeedsResponse.body!; const datafeedPrefix = 'datafeed-'; - const closeJobsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/close_jobs`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - jobIds: datafeedIds.map(dataFeedId => - dataFeedId.startsWith(datafeedPrefix) - ? dataFeedId.substring(datafeedPrefix.length) - : dataFeedId - ), - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); + const closeJobsResponse = await npStart.core.http.fetch<CloseJobsResponse>( + '/api/ml/jobs/close_jobs', + { + method: 'POST', + body: JSON.stringify({ + jobIds: datafeedIds.map(dataFeedId => + dataFeedId.startsWith(datafeedPrefix) + ? dataFeedId.substring(datafeedPrefix.length) + : dataFeedId + ), + }), + asResponse: true, + asSystemRequest: true, + } + ); - await throwIfNotOk(closeJobsResponse); - return [stopDatafeedsResponseJson, await closeJobsResponse.json()]; + await throwIfNotOk(closeJobsResponse.response); + return [stopDatafeedsResponseJson, closeJobsResponse.body!]; }; /** @@ -198,17 +194,14 @@ export const stopDatafeeds = async ({ * @param signal to cancel request */ export const getJobsSummary = async (signal: AbortSignal): Promise<JobSummary[]> => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/jobs_summary`, { + const response = await npStart.core.http.fetch<JobSummary[]>('/api/ml/jobs/jobs_summary', { method: 'POST', - credentials: 'same-origin', body: JSON.stringify({}), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, }); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap index 4c9a27b76060cb..8f40d0203afd43 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap @@ -5,16 +5,12 @@ exports[`GroupsFilterPopover renders correctly against snapshot 1`] = ` anchorPosition="downCenter" button={ <EuiFilterButton - color="text" data-test-subj="groups-filter-popover-button" - grow={true} hasActiveFilters={false} - iconSide="right" iconType="arrowDown" isSelected={false} numActiveFilters={0} onClick={[Function]} - type="button" > Groups </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap index fac91f75978f09..747ac63551b55b 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap @@ -110,25 +110,17 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" data-test-subj="show-elastic-jobs-filter-button" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" withNext={true} > Elastic jobs </EuiFilterButton> <EuiFilterButton - color="text" data-test-subj="show-custom-jobs-filter-button" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" > Custom jobs </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts index 964ae8c8242d4b..f3bf78fdbb94c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts @@ -193,16 +193,6 @@ export interface CloseJobsResponse { }; } -export interface IndexPatternSavedObject { - attributes: { - title: string; - }; - id: string; - type: string; - updated_at: string; - version: string; -} - export interface JobsFilters { filterQuery: string; showCustomJobs: boolean; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx index 006587d8fc294b..e71be5a51e5053 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx @@ -6,27 +6,37 @@ import { mount } from 'enzyme'; import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { TestProviders } from '../../../../mock'; -import { FlowTargetSelectConnected } from './index'; +import { FlowTargetSelectConnectedComponent } from './index'; import { FlowTarget } from '../../../../graphql/types'; -describe.skip('Flow Target Select Connected', () => { +describe('Flow Target Select Connected', () => { test('renders correctly against snapshot flowTarget source', () => { const wrapper = mount( <TestProviders> - <FlowTargetSelectConnected flowTarget={FlowTarget.source} /> + <MemoryRouter> + <FlowTargetSelectConnectedComponent flowTarget={FlowTarget.source} /> + </MemoryRouter> </TestProviders> ); - expect(wrapper.find('FlowTargetSelectConnected')).toMatchSnapshot(); + expect(wrapper.find('Memo(FlowTargetSelectComponent)').prop('selectedTarget')).toEqual( + FlowTarget.source + ); }); test('renders correctly against snapshot flowTarget destination', () => { const wrapper = mount( <TestProviders> - <FlowTargetSelectConnected flowTarget={FlowTarget.destination} /> + <MemoryRouter> + <FlowTargetSelectConnectedComponent flowTarget={FlowTarget.destination} /> + </MemoryRouter> </TestProviders> ); - expect(wrapper.find('FlowTargetSelectConnected')).toMatchSnapshot(); + + expect(wrapper.find('Memo(FlowTargetSelectComponent)').prop('selectedTarget')).toEqual( + FlowTarget.destination + ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx index 1b87c36902159b..2651c31e0a2c9e 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx @@ -36,7 +36,7 @@ const getUpdatedFlowTargetPath = ( return `${newPathame}${location.search}`; }; -const FlowTargetSelectConnectedComponent: React.FC<Props> = ({ flowTarget }) => { +export const FlowTargetSelectConnectedComponent: React.FC<Props> = ({ flowTarget }) => { const history = useHistory(); const location = useLocation(); diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap index 7e3e099bf0276e..28481e9970a5eb 100644 --- a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -3,7 +3,6 @@ exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = ` <EuiOverlayMask> <EuiModal - maxWidth={true} onClose={[Function]} > <EuiModalHeader> diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 8f4abeb31c2269..4f50a9bd147881 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; + import { AddRulesProps, DeleteRulesProps, @@ -19,8 +20,9 @@ import { ImportRulesProps, ExportRulesProps, RuleError, - RuleStatus, + RuleStatusResponse, ImportRulesResponse, + PrePackagedRulesStatusResponse, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; import { @@ -39,19 +41,15 @@ import * as i18n from '../../../pages/detection_engine/rules/translations'; * @param signal to cancel request */ export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { + const response = await npStart.core.http.fetch<NewRule>(DETECTION_ENGINE_RULES_URL, { method: rule.id != null ? 'PUT' : 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify(rule), + asResponse: true, signal, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -79,40 +77,36 @@ export const fetchRules = async ({ signal, }: FetchRulesProps): Promise<FetchRulesResponse> => { const filters = [ - ...(filterOptions.filter.length !== 0 - ? [`alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`] - : []), + ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), ...(filterOptions.showCustomRules - ? ['alert.attributes.tags:%20%22__internal_immutable:false%22'] + ? [`alert.attributes.tags: "__internal_immutable:false"`] : []), ...(filterOptions.showElasticRules - ? ['alert.attributes.tags:%20%22__internal_immutable:true%22'] + ? [`alert.attributes.tags: "__internal_immutable:true"`] : []), - ...(filterOptions.tags?.map(t => `alert.attributes.tags:${encodeURIComponent(t)}`) ?? []), + ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), ]; - const queryParams = [ - `page=${pagination.page}`, - `per_page=${pagination.perPage}`, - `sort_field=${filterOptions.sortField}`, - `sort_order=${filterOptions.sortOrder}`, - ...(filters.length > 0 ? [`filter=${filters.join('%20AND%20')}`] : []), - ]; + const query = { + page: pagination.page, + per_page: pagination.perPage, + sort_field: filterOptions.sortField, + sort_order: filterOptions.sortOrder, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`, + const response = await npStart.core.http.fetch<FetchRulesResponse>( + `${DETECTION_ENGINE_RULES_URL}/_find`, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -123,18 +117,15 @@ export const fetchRules = async ({ * */ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rule> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { + const response = await npStart.core.http.fetch<Rule>(DETECTION_ENGINE_RULES_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query: { id }, + asResponse: true, signal, }); - await throwIfNotOk(response); - const rule: Rule = await response.json(); - return rule; + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -146,21 +137,17 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rul * @throws An error if response is not OK */ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<Rule[]> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + const response = await npStart.core.http.fetch<Rule[]>( + `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { method: 'PUT', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify(ids.map(id => ({ id, enabled }))), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -171,21 +158,17 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<R * @throws An error if response is not OK */ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<Array<Rule | RuleError>> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + const response = await npStart.core.http.fetch<Rule[]>( + `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { - method: 'DELETE', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + method: 'PUT', body: JSON.stringify(ids.map(id => ({ id }))), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -194,15 +177,10 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<Array<Rule * @param rules to duplicate */ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Rule[]> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + const response = await npStart.core.http.fetch<Rule[]>( + `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify( rules.map(rule => ({ ...rule, @@ -223,11 +201,12 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Ru status_date: undefined, })) ), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -236,16 +215,13 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<Ru * @param signal AbortSignal for cancelling request */ export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise<boolean> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_URL}`, { + const response = await npStart.core.http.fetch<unknown>(DETECTION_ENGINE_PREPACKAGED_URL, { method: 'PUT', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); + + await throwIfNotOk(response.response); return true; }; @@ -266,21 +242,19 @@ export const importRules = async ({ const formData = new FormData(); formData.append('file', fileToImport); - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_import?overwrite=${overwrite}`, + const response = await npStart.core.http.fetch<ImportRulesResponse>( + `${DETECTION_ENGINE_RULES_URL}/_import`, { method: 'POST', - credentials: 'same-origin', - headers: { - 'kbn-xsrf': 'true', - }, + query: { overwrite }, body: formData, + asResponse: true, signal, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -304,24 +278,19 @@ export const exportRules = async ({ ? JSON.stringify({ objects: ruleIds.map(rule => ({ rule_id: rule })) }) : undefined; - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_export?exclude_export_details=${excludeExportDetails}&file_name=${encodeURIComponent( - filename - )}`, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - body, - signal, - } - ); + const response = await npStart.core.http.fetch<Blob>(`${DETECTION_ENGINE_RULES_URL}/_export`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + asResponse: true, + }); - await throwIfNotOk(response); - return response.blob(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -338,24 +307,19 @@ export const getRuleStatusById = async ({ }: { id: string; signal: AbortSignal; -}): Promise<Record<string, RuleStatus>> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS_URL}?ids=${encodeURIComponent( - JSON.stringify([id]) - )}`, +}): Promise<RuleStatusResponse> => { + const response = await npStart.core.http.fetch<RuleStatusResponse>( + DETECTION_ENGINE_RULES_STATUS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query: { ids: JSON.stringify([id]) }, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -365,18 +329,14 @@ export const getRuleStatusById = async ({ * */ export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_TAGS_URL}`, { + const response = await npStart.core.http.fetch<string[]>(DETECTION_ENGINE_TAGS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -390,25 +350,16 @@ export const getPrePackagedRulesStatus = async ({ signal, }: { signal: AbortSignal; -}): Promise<{ - rules_custom_installed: number; - rules_installed: number; - rules_not_installed: number; - rules_not_updated: number; -}> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL}`, +}): Promise<PrePackagedRulesStatusResponse> => { + const response = await npStart.core.http.fetch<PrePackagedRulesStatusResponse>( + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index b30c3b211b1b8d..0aaffb7b86b284 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -197,3 +197,12 @@ export interface RuleInfoStatus { last_failure_message: string | null; last_success_message: string | null; } + +export type RuleStatusResponse = Record<string, RuleStatus>; + +export interface PrePackagedRulesStatusResponse { + rules_custom_installed: number; + rules_installed: number; + rules_not_installed: number; + rules_not_updated: number; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index 8754d73637e7c7..d0da70e6461248 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { throwIfNotOk } from '../../../hooks/api/api'; import { @@ -14,40 +14,37 @@ import { DETECTION_ENGINE_PRIVILEGES_URL, } from '../../../../common/constants'; import { + BasicSignals, + PostSignalError, + Privilege, QuerySignals, + SignalIndexError, SignalSearchResponse, - UpdateSignalStatusProps, SignalsIndex, - SignalIndexError, - Privilege, - PostSignalError, - BasicSignals, + UpdateSignalStatusProps, } from './types'; -import { parseJsonFromBody } from '../../../utils/api'; /** * Fetch Signals by providing a query * * @param query String to match a dsl - * @param signal AbortSignal for cancelling request */ export const fetchQuerySignals = async <Hit, Aggregations>({ query, signal, }: QuerySignals): Promise<SignalSearchResponse<Hit, Aggregations>> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_QUERY_SIGNALS_URL}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - body: JSON.stringify(query), - signal, - }); - await throwIfNotOk(response); - const signals = await response.json(); - return signals; + const response = await npStart.core.http.fetch<SignalSearchResponse<Hit, Aggregations>>( + DETECTION_ENGINE_QUERY_SIGNALS_URL, + { + method: 'POST', + body: JSON.stringify(query), + asResponse: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -62,19 +59,15 @@ export const updateSignalStatus = async ({ status, signal, }: UpdateSignalStatusProps): Promise<unknown> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_SIGNALS_STATUS_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify({ status, ...query }), + asResponse: true, signal, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -82,25 +75,18 @@ export const updateSignalStatus = async ({ * * @param signal AbortSignal for cancelling request */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex | null> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - signal, - }); - if (response.ok) { - const signalIndex = await response.json(); - return signalIndex; +export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => { + try { + return await npStart.core.http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { + method: 'GET', + signal, + }); + } catch (e) { + if (e.body) { + throw new SignalIndexError(e.body); + } + throw e; } - const error = await parseJsonFromBody(response); - if (error != null) { - throw new SignalIndexError(error); - } - return null; }; /** @@ -108,19 +94,15 @@ export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsI * * @param signal AbortSignal for cancelling request */ -export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privilege | null> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PRIVILEGES_URL}`, { +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privilege> => { + const response = await npStart.core.http.fetch<Privilege>(DETECTION_ENGINE_PRIVILEGES_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -128,23 +110,16 @@ export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privil * * @param signal AbortSignal for cancelling request */ -export const createSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex | null> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - signal, - }); - if (response.ok) { - const signalIndex = await response.json(); - return signalIndex; - } - const error = await parseJsonFromBody(response); - if (error != null) { - throw new PostSignalError(error); +export const createSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => { + try { + return await npStart.core.http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { + method: 'POST', + signal, + }); + } catch (e) { + if (e.body) { + throw new PostSignalError(e.body); + } + throw e; } - return null; }; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx index 13f53cd34feb62..b12b04e8f760b5 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx @@ -13,8 +13,7 @@ export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [ attributes: { title: 'filebeat-*', }, - updated_at: '2019-08-26T04:30:09.111Z', - version: 'WzE4LLwxXQ==', + _version: 'WzE4LLwxXQ==', }, { type: 'index-pattern', @@ -22,7 +21,6 @@ export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [ attributes: { title: 'auditbeat-*', }, - updated_at: '2019-08-26T04:31:12.934Z', - version: 'WzELLywxXQ==', + _version: 'WzELLywxXQ==', }, ]; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts index 95825b7d4abda5..208a3b14ca283c 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts @@ -13,6 +13,10 @@ describe('api', () => { }); describe('#throwIfNotOk', () => { + test('throws a network error if there is no response', async () => { + await expect(throwIfNotOk()).rejects.toThrow('Network Error'); + }); + test('does a throw if it is given response that is not ok and the body is not parsable', async () => { fetchMock.mock('http://example.com', 500); const response = await fetch('http://example.com'); diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx index 8d319ffe239027..f5f32da7d8c0b2 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx @@ -4,46 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - +import { npStart } from 'ui/new_platform'; import * as i18n from '../translations'; import { parseJsonFromBody, ToasterErrors } from '../../components/ml/api/throw_if_not_ok'; -import { IndexPatternResponse, IndexPatternSavedObject } from '../types'; - -const emptyIndexPattern: IndexPatternSavedObject[] = []; +import { IndexPatternSavedObject, IndexPatternSavedObjectAttributes } from '../types'; /** * Fetches Configured Index Patterns from the Kibana saved objects API * * TODO: Refactor to context provider: https://github.com/elastic/siem-team/issues/448 - * - * @param signal */ -export const getIndexPatterns = async (signal: AbortSignal): Promise<IndexPatternSavedObject[]> => { - const response = await fetch( - `${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`, - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - } - ); - await throwIfNotOk(response); - const results: IndexPatternResponse = await response.json(); +export const getIndexPatterns = async (): Promise<IndexPatternSavedObject[]> => { + const response = await npStart.core.savedObjects.client.find<IndexPatternSavedObjectAttributes>({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); - if (results.saved_objects && Array.isArray(results.saved_objects)) { - return results.saved_objects; - } else { - return emptyIndexPattern; - } + return response.savedObjects; }; -export const throwIfNotOk = async (response: Response): Promise<void> => { +export const throwIfNotOk = async (response?: Response): Promise<void> => { + if (!response) { + throw new ToasterErrors([i18n.NETWORK_ERROR]); + } + if (!response.ok) { const body = await parseJsonFromBody(response); if (body != null && body.message) { diff --git a/x-pack/legacy/plugins/siem/public/hooks/types.ts b/x-pack/legacy/plugins/siem/public/hooks/types.ts index 4d66d8e1912352..301b8bd6553337 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/types.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/types.ts @@ -4,19 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IndexPatternSavedObject { - attributes: { - title: string; - }; - id: string; - type: string; - updated_at: string; - version: string; -} +import { SimpleSavedObject } from '../../../../../../src/core/public'; -export interface IndexPatternResponse { - page: number; - per_page: number; - saved_objects: IndexPatternSavedObject[]; - total: number; -} +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IndexPatternSavedObjectAttributes = { title: string }; + +export type IndexPatternSavedObject = Pick< + SimpleSavedObject<IndexPatternSavedObjectAttributes>, + 'type' | 'id' | 'attributes' | '_version' +>; diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx index 7abe88402096c8..35bed69e8617eb 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx @@ -8,10 +8,10 @@ import { useEffect, useState } from 'react'; import { useStateToaster } from '../components/toasters'; import { errorToToaster } from '../components/ml/api/error_to_toaster'; -import { IndexPatternSavedObject } from '../components/ml_popover/types'; -import { getIndexPatterns } from './api/api'; import * as i18n from './translations'; +import { IndexPatternSavedObject } from './types'; +import { getIndexPatterns } from './api/api'; type Return = [boolean, IndexPatternSavedObject[]]; @@ -22,12 +22,11 @@ export const useIndexPatterns = (refreshToggle = false): Return => { useEffect(() => { let isSubscribed = true; - const abortCtrl = new AbortController(); setIsLoading(true); async function fetchIndexPatterns() { try { - const data = await getIndexPatterns(abortCtrl.signal); + const data = await getIndexPatterns(); if (isSubscribed) { setIndexPatterns(data); @@ -44,7 +43,6 @@ export const useIndexPatterns = (refreshToggle = false): Return => { fetchIndexPatterns(); return () => { isSubscribed = false; - abortCtrl.abort(); }; }, [refreshToggle]); diff --git a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts index 3e203383756163..072aee50d5136b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts @@ -12,7 +12,7 @@ export const EMPTY_TITLE = i18n.translate('xpack.siem.pages.common.emptyTitle', export const EMPTY_MESSAGE = i18n.translate('xpack.siem.pages.common.emptyMessage', { defaultMessage: - 'To begin using security information and event management, you’ll need to begin adding SIEM-related data to Kibana by installing and configuring our data shippers, called Beats. Let’s do that now!', + 'To begin using security information and event management (SIEM), you’ll need to add SIEM-related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', }); export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.pages.common.emptyActionPrimary', { diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index 7911b5eb9833b4..74fc913d2b573a 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -41,6 +41,7 @@ export type Start = ReturnType<Plugin['start']>; export class Plugin implements IPlugin<Setup, Start> { public id = 'siem'; public name = 'SIEM'; + constructor( // @ts-ignore this is added to satisfy the New Platform typing constraint, // but we're not leveraging any of its functionality yet. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index dc308263baab61..8d00ddb18be6b9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -35,32 +35,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([getResult()]); }); - test('should return 2 items over two pages, one per page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.params.immutable = true; - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.params.immutable = true; - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) - ); - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2]); - }); - - test('should return 3 items with over 3 pages one per page', async () => { + test('should return 3 items over 1 page with all on one page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -75,40 +50,17 @@ describe('get_existing_prepackaged_rules', () => { result3.params.immutable = true; result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 1, + page: 1, + total: 3, + }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.params.immutable = true; - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.params.immutable = true; - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.params.immutable = true; - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], @@ -137,7 +89,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([getResult()]); }); - test('should return 2 items over two pages, one per page', async () => { + test('should return 2 items over 1 page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -146,11 +98,19 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 1, + page: 1, + total: 2, + }) ); + + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) + getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; @@ -160,7 +120,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([result1, result2]); }); - test('should return 3 items with over 3 pages one per page', async () => { + test('should return 3 items over 1 page with all on one page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -172,37 +132,17 @@ describe('get_existing_prepackaged_rules', () => { const result3 = getResult(); result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 3, + page: 1, + total: 3, + }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], @@ -241,80 +181,27 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) - ); - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); - expect(rules).toEqual([result1, result2]); - }); - - test('should return 3 items with over 3 pages one per page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ - data: [result1, result2, result3], - perPage: 3, + data: [result1], + perPage: 1, page: 1, - total: 3, + total: 2, }) ); + // second mock which will return all the data on a single page + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) + ); + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rules = await getRules({ alertsClient: unsafeCast, filter: '', }); - expect(rules).toEqual([result1, result2, result3]); + expect(rules).toEqual([result1, result2]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index b7ab6a97634a80..a48957da7aa942 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -9,7 +9,6 @@ import { AlertsClient } from '../../../../../alerting'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; -export const DEFAULT_PER_PAGE = 100; export const FILTER_NON_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`; export const FILTER_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`; @@ -33,84 +32,56 @@ export const getRulesCount = async ({ filter, perPage: 1, page: 1, + sortField: 'createdAt', + sortOrder: 'desc', }); return firstRule.total; }; export const getRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, filter, }: { alertsClient: AlertsClient; - perPage?: number; filter: string; }): Promise<RuleAlertType[]> => { - const firstPrepackedRules = await findRules({ + const count = await getRulesCount({ alertsClient, filter }); + const rules = await findRules({ alertsClient, filter, - perPage, + perPage: count, page: 1, + sortField: 'createdAt', + sortOrder: 'desc', }); - const totalPages = Math.ceil(firstPrepackedRules.total / firstPrepackedRules.perPage); - if (totalPages <= 1) { - if (isAlertTypes(firstPrepackedRules.data)) { - return firstPrepackedRules.data; - } else { - // If this was ever true, you have a really messed up system. - // This is keep typescript happy since we have an unknown with data - return []; - } - } else { - const returnPrepackagedRules = await Array(totalPages - 1) - .fill({}) - .map((_, page) => { - // page index starts at 2 as we already got the first page and we have more pages to go - return findRules({ - alertsClient, - filter, - perPage, - page: page + 2, - }); - }) - .reduce<Promise<object[]>>(async (accum, nextPage) => { - return [...(await accum), ...(await nextPage).data]; - }, Promise.resolve(firstPrepackedRules.data)); - if (isAlertTypes(returnPrepackagedRules)) { - return returnPrepackagedRules; - } else { - // If this was ever true, you have a really messed up system. - // This is keep typescript happy since we have an unknown with data - return []; - } + if (isAlertTypes(rules.data)) { + return rules.data; + } else { + // If this was ever true, you have a really messed up system. + // This is keep typescript happy since we have an unknown with data + return []; } }; export const getNonPackagedRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<RuleAlertType[]> => { return getRules({ alertsClient, - perPage, filter: FILTER_NON_PREPACKED_RULES, }); }; export const getExistingPrepackagedRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<RuleAlertType[]> => { return getRules({ alertsClient, - perPage, filter: FILTER_PREPACKED_RULES, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts index 0f973d816917fc..02456732df3b4e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -9,8 +9,6 @@ import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { AlertsClient } from '../../../../../alerting'; import { findRules } from '../rules/find_rules'; -const DEFAULT_PER_PAGE: number = 1000; - export interface TagType { id: string; tags: string[]; @@ -42,39 +40,37 @@ export const convertTagsToSet = (tagObjects: object[]): Set<string> => { // Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html export const readTags = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<string[]> => { - const tags = await readRawTags({ alertsClient, perPage }); + const tags = await readRawTags({ alertsClient }); return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); }; export const readRawTags = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; perPage?: number; }): Promise<string[]> => { - const firstTags = await findRules({ alertsClient, fields: ['tags'], perPage, page: 1 }); - const firstSet = convertTagsToSet(firstTags.data); - const totalPages = Math.ceil(firstTags.total / firstTags.perPage); - if (totalPages <= 1) { - return Array.from(firstSet); - } else { - const returnTags = await Array(totalPages - 1) - .fill({}) - .map((_, page) => { - // page index starts at 2 as we already got the first page and we have more pages to go - return findRules({ alertsClient, fields: ['tags'], perPage, page: page + 2 }); - }) - .reduce<Promise<Set<string>>>(async (accum, nextTagPage) => { - const tagArray = convertToTags((await nextTagPage).data); - return new Set([...(await accum), ...tagArray]); - }, Promise.resolve(firstSet)); - - return Array.from(returnTags); - } + // Get just one record so we can get the total count + const firstTags = await findRules({ + alertsClient, + fields: ['tags'], + perPage: 1, + page: 1, + sortField: 'createdAt', + sortOrder: 'desc', + }); + // Get all the rules to aggregate over all the tags of the rules + const rules = await findRules({ + alertsClient, + fields: ['tags'], + perPage: firstTags.total, + sortField: 'createdAt', + sortOrder: 'desc', + page: 1, + }); + const tagSet = convertTagsToSet(rules.data); + return Array.from(tagSet); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts index 17d203918d8254..bb4693a7749e7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/source_status/elasticsearch_adapter.ts @@ -6,26 +6,54 @@ import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { SourceStatusAdapter } from './index'; +import { buildQuery } from './query.dsl'; +import { ApmServiceNameAgg } from './types'; + +const APM_INDEX_NAME = 'apm-*-transaction*'; export class ElasticsearchSourceStatusAdapter implements SourceStatusAdapter { constructor(private readonly framework: FrameworkAdapter) {} - public async hasIndices(request: FrameworkRequest, indexNames: string | string[]) { - return this.framework - .callWithRequest(request, 'search', { - index: indexNames, - size: 0, - terminate_after: 1, - allow_no_indices: true, - }) - .then( - response => response._shards.total > 0, - err => { - if (err.status === 404) { - return false; - } - throw err; - } - ); + public async hasIndices(request: FrameworkRequest, indexNames: string[]) { + // Intended flow to determine app-empty state is to first check siem indices (as this is a quick shard count), and + // if no shards exist only then perform the heavier APM query. This optimizes for normal use when siem data exists + try { + // Remove APM index if exists, and only query if length > 0 in case it's the only index provided + const nonApmIndexNames = indexNames.filter(name => name !== APM_INDEX_NAME); + const indexCheckResponse = await (nonApmIndexNames.length > 0 + ? this.framework.callWithRequest(request, 'search', { + index: nonApmIndexNames, + size: 0, + terminate_after: 1, + allow_no_indices: true, + }) + : Promise.resolve(undefined)); + + if ((indexCheckResponse?._shards.total ?? -1) > 0) { + return true; + } + + // Note: Additional check necessary for APM-specific index. For details see: https://github.com/elastic/kibana/issues/56363 + // Only verify if APM data exists if indexNames includes `apm-*-transaction*` (default included apm index) + const includesApmIndex = indexNames.includes(APM_INDEX_NAME); + const hasApmDataResponse = await (includesApmIndex + ? this.framework.callWithRequest<{}, ApmServiceNameAgg>( + request, + 'search', + buildQuery({ defaultIndex: [APM_INDEX_NAME] }) + ) + : Promise.resolve(undefined)); + + if ((hasApmDataResponse?.aggregations?.total_service_names?.value ?? -1) > 0) { + return true; + } + } catch (e) { + if (e.status === 404) { + return false; + } + throw e; + } + + return false; } } diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/query.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/source_status/query.dsl.ts new file mode 100644 index 00000000000000..ead08fff474c73 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/source_status/query.dsl.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +const SERVICE_NAME = 'service.name'; + +export const buildQuery = ({ defaultIndex }: { defaultIndex: string[] }) => { + return { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + terminate_after: 1, + body: { + size: 0, + aggs: { + total_service_names: { + cardinality: { + field: SERVICE_NAME, + }, + }, + }, + }, + track_total_hits: false, + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/source_status/types.ts b/x-pack/legacy/plugins/siem/server/lib/source_status/types.ts new file mode 100644 index 00000000000000..247495ffe4b235 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/source_status/types.ts @@ -0,0 +1,11 @@ +/* + * 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 interface ApmServiceNameAgg { + total_service_names: { + value: number; + }; +} diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 96eef2f44e5a06..94314367be59cf 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -17,7 +17,7 @@ import { ruleStatusSavedObjectType, } from './saved_objects'; -export type SiemPluginSecurity = Pick<SecurityPlugin, 'authc'>; +export type SiemPluginSecurity = Pick<SecurityPluginSetup, 'authc'>; export interface PluginsSetup { features: FeaturesSetupContract; diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap index 8930dedfa0035d..6e422bc13f06be 100644 --- a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap @@ -5,7 +5,6 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` <EuiModal className="spcConfirmDeleteModal" initialFocus="input[name=\\"confirmDeleteSpaceInput\\"]" - maxWidth={true} onClose={[MockFunction]} > <EuiModalHeader> diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index 6eed58a7842124..3a4861f4fbc9ec 100644 --- a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -9,8 +9,6 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - // @ts-ignore - EuiConfirmModal, EuiFieldText, EuiFormRow, EuiModal, @@ -89,7 +87,7 @@ class ConfirmDeleteModalUI extends Component<Props, State> { // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability // to disable the buttons since this could be a long-running operation - const modalProps: EuiModalProps & CommonProps = { + const modalProps: Omit<EuiModalProps, 'children'> & CommonProps = { onClose: onCancel, className: 'spcConfirmDeleteModal', initialFocus: 'input[name="confirmDeleteSpaceInput"]', diff --git a/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap index d7702e2f18d44b..750afcfc44e7e3 100644 --- a/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap @@ -3,7 +3,6 @@ exports[`ConfirmAlterActiveSpaceModal renders as expected 1`] = ` <EuiOverlayMask> <EuiConfirmModal - buttonColor="primary" cancelButtonText="Cancel" confirmButtonText="Update space" defaultFocusedButton="confirm" diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx index 57e6fc4a9e18b1..7d9a963c9c6b32 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/app.tsx @@ -13,12 +13,13 @@ import { IUiSettingsClient, ApplicationStart, } from 'kibana/public'; -import { BASE_PATH, Section } from './constants'; +import { BASE_PATH, Section, routeToAlertDetails } from './constants'; import { TriggersActionsUIHome } from './home'; import { AppContextProvider, useAppDependencies } from './app_context'; import { hasShowAlertsCapability } from './lib/capabilities'; import { LegacyDependencies, ActionTypeModel, AlertTypeModel } from '../types'; import { TypeRegistry } from './type_registry'; +import { AlertDetailsRouteWithApi as AlertDetailsRoute } from './sections/alert_details/components/alert_details_route'; export interface AppDeps { chrome: ChromeStart; @@ -53,11 +54,8 @@ export const AppWithoutRouter = ({ sectionsRegex }: any) => { const DEFAULT_SECTION: Section = canShowAlerts ? 'alerts' : 'connectors'; return ( <Switch> - <Route - exact - path={`${BASE_PATH}/:section(${sectionsRegex})`} - component={TriggersActionsUIHome} - /> + <Route path={`${BASE_PATH}/:section(${sectionsRegex})`} component={TriggersActionsUIHome} /> + {canShowAlerts && <Route path={routeToAlertDetails} component={AlertDetailsRoute} />} <Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}/${DEFAULT_SECTION}`} /> </Switch> ); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts index a8364ffe21019d..11b094dea0e624 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/constants/index.ts @@ -13,6 +13,7 @@ export type Section = 'connectors' | 'alerts'; export const routeToHome = `${BASE_PATH}`; export const routeToConnectors = `${BASE_PATH}/connectors`; export const routeToAlerts = `${BASE_PATH}/alerts`; +export const routeToAlertDetails = `${BASE_PATH}/alert/:alertId`; export { TIME_UNITS } from './time_units'; export enum SORT_ORDERS { diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts index bc2949917edea7..00a55bb2588bb3 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/action_connector_api.test.ts @@ -24,6 +24,7 @@ describe('loadActionTypes', () => { { id: 'test', name: 'Test', + enabled: true, }, ]; http.get.mockResolvedValueOnce(resolvedValue); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts index 0106970cf9c38c..35d1a095188dee 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.test.ts @@ -8,15 +8,22 @@ import { Alert, AlertType } from '../../types'; import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; import { createAlert, + deleteAlert, deleteAlerts, disableAlerts, enableAlerts, + disableAlert, + enableAlert, + loadAlert, loadAlerts, loadAlertTypes, muteAlerts, unmuteAlerts, + muteAlert, + unmuteAlert, updateAlert, } from './alert_api'; +import uuid from 'uuid'; const http = httpServiceMock.createStartContract(); @@ -42,6 +49,31 @@ describe('loadAlertTypes', () => { }); }); +describe('loadAlert', () => { + test('should call get API with base parameters', async () => { + const alertId = uuid.v4(); + const resolvedValue = { + id: alertId, + name: 'name', + tags: [], + enabled: true, + alertTypeId: '.noop', + schedule: { interval: '1s' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await loadAlert({ http, alertId })).toEqual(resolvedValue); + expect(http.get).toHaveBeenCalledWith(`/api/alert/${alertId}`); + }); +}); + describe('loadAlerts', () => { test('should call find API with base parameters', async () => { const resolvedValue = { @@ -230,6 +262,19 @@ describe('loadAlerts', () => { }); }); +describe('deleteAlert', () => { + test('should call delete API for alert', async () => { + const id = '1'; + const result = await deleteAlert({ http, id }); + expect(result).toEqual(undefined); + expect(http.delete.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/alert/1", + ] + `); + }); +}); + describe('deleteAlerts', () => { test('should call delete API for each alert', async () => { const ids = ['1', '2', '3']; @@ -335,6 +380,62 @@ describe('updateAlert', () => { }); }); +describe('enableAlert', () => { + test('should call enable alert API', async () => { + const result = await enableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_enable", + ], + ] + `); + }); +}); + +describe('disableAlert', () => { + test('should call disable alert API', async () => { + const result = await disableAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_disable", + ], + ] + `); + }); +}); + +describe('muteAlert', () => { + test('should call mute alert API', async () => { + const result = await muteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_mute_all", + ], + ] + `); + }); +}); + +describe('unmuteAlert', () => { + test('should call unmute alert API', async () => { + const result = await unmuteAlert({ http, id: '1' }); + expect(result).toEqual(undefined); + expect(http.post.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "/api/alert/1/_unmute_all", + ], + ] + `); + }); +}); + describe('enableAlerts', () => { test('should call enable alert API per alert', async () => { const ids = ['1', '2', '3']; diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts index 0b4f5731c13153..acc318bd5fbea5 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/alert_api.ts @@ -12,6 +12,16 @@ export async function loadAlertTypes({ http }: { http: HttpSetup }): Promise<Ale return await http.get(`${BASE_ALERT_API_PATH}/types`); } +export async function loadAlert({ + http, + alertId, +}: { + http: HttpSetup; + alertId: string; +}): Promise<Alert> { + return await http.get(`${BASE_ALERT_API_PATH}/${alertId}`); +} + export async function loadAlerts({ http, page, @@ -55,6 +65,10 @@ export async function loadAlerts({ }); } +export async function deleteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { + await http.delete(`${BASE_ALERT_API_PATH}/${id}`); +} + export async function deleteAlerts({ ids, http, @@ -62,7 +76,7 @@ export async function deleteAlerts({ ids: string[]; http: HttpSetup; }): Promise<void> { - await Promise.all(ids.map(id => http.delete(`${BASE_ALERT_API_PATH}/${id}`))); + await Promise.all(ids.map(id => deleteAlert({ http, id }))); } export async function createAlert({ @@ -91,6 +105,10 @@ export async function updateAlert({ }); } +export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`); +} + export async function enableAlerts({ ids, http, @@ -98,7 +116,11 @@ export async function enableAlerts({ ids: string[]; http: HttpSetup; }): Promise<void> { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_enable`))); + await Promise.all(ids.map(id => enableAlert({ id, http }))); +} + +export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`); } export async function disableAlerts({ @@ -108,11 +130,19 @@ export async function disableAlerts({ ids: string[]; http: HttpSetup; }): Promise<void> { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_disable`))); + await Promise.all(ids.map(id => disableAlert({ id, http }))); +} + +export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`); } export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise<void> { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_mute_all`))); + await Promise.all(ids.map(id => muteAlert({ http, id }))); +} + +export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { + await http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`); } export async function unmuteAlerts({ @@ -122,5 +152,5 @@ export async function unmuteAlerts({ ids: string[]; http: HttpSetup; }): Promise<void> { - await Promise.all(ids.map(id => http.post(`${BASE_ALERT_API_PATH}/${id}/_unmute_all`))); + await Promise.all(ids.map(id => unmuteAlert({ id, http }))); } diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts new file mode 100644 index 00000000000000..90f575d9391b32 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.test.ts @@ -0,0 +1,77 @@ +/* + * 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 { throwIfAbsent, throwIfIsntContained } from './value_validators'; +import uuid from 'uuid'; + +describe('throwIfAbsent', () => { + test('throws if value is absent', () => { + [undefined, null].forEach(val => { + expect(() => { + throwIfAbsent('OMG no value')(val); + }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`); + }); + }); + + test('doesnt throws if value is present but falsey', () => { + [false, ''].forEach(val => { + expect(throwIfAbsent('OMG no value')(val)).toEqual(val); + }); + }); + + test('doesnt throw if value is present', () => { + expect(throwIfAbsent('OMG no value')({})).toEqual({}); + }); +}); + +describe('throwIfIsntContained', () => { + test('throws if value is absent', () => { + expect(() => { + throwIfIsntContained<string>(new Set([uuid.v4()]), 'OMG no value', val => val)([uuid.v4()]); + }).toThrowErrorMatchingInlineSnapshot(`"OMG no value"`); + }); + + test('throws if value is absent using custom message', () => { + const id = uuid.v4(); + expect(() => { + throwIfIsntContained<string>( + new Set([id]), + (value: string) => `OMG no ${value}`, + val => val + )([uuid.v4()]); + }).toThrow(`OMG no ${id}`); + }); + + test('returns values if value is present', () => { + const id = uuid.v4(); + const values = [uuid.v4(), uuid.v4(), id, uuid.v4()]; + expect(throwIfIsntContained<string>(new Set([id]), 'OMG no value', val => val)(values)).toEqual( + values + ); + }); + + test('returns values if multiple values is present', () => { + const [firstId, secondId] = [uuid.v4(), uuid.v4()]; + const values = [uuid.v4(), uuid.v4(), secondId, uuid.v4(), firstId]; + expect( + throwIfIsntContained<string>(new Set([firstId, secondId]), 'OMG no value', val => val)(values) + ).toEqual(values); + }); + + test('allows a custom value extractor', () => { + const [firstId, secondId] = [uuid.v4(), uuid.v4()]; + const values = [ + { id: firstId, some: 'prop' }, + { id: secondId, someOther: 'prop' }, + ]; + expect( + throwIfIsntContained<{ id: string }>( + new Set([firstId, secondId]), + 'OMG no value', + (val: { id: string }) => val.id + )(values) + ).toEqual(values); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts new file mode 100644 index 00000000000000..7ee73590864065 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/lib/value_validators.ts @@ -0,0 +1,33 @@ +/* + * 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 { constant } from 'lodash'; + +export function throwIfAbsent<T>(message: string) { + return (value: T | undefined): T => { + if (value === undefined || value === null) { + throw new Error(message); + } + return value; + }; +} + +export function throwIfIsntContained<T>( + requiredValues: Set<string>, + message: string | ((requiredValue: string) => string), + valueExtractor: (value: T) => string +) { + const toError = typeof message === 'function' ? message : constant(message); + return (values: T[]) => { + const availableValues = new Set(values.map(valueExtractor)); + for (const value of requiredValues.values()) { + if (!availableValues.has(value)) { + throw new Error(toError(value)); + } + } + return values; + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx index 6896ac954bb068..f27f7d8c3054df 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -82,7 +82,11 @@ describe('action_connector_form', () => { editFlyoutVisible: false, setEditFlyoutVisibility: () => {}, actionTypesIndex: { - 'my-action-type': { id: 'my-action-type', name: 'my-action-type-name' }, + 'my-action-type': { + id: 'my-action-type', + name: 'my-action-type-name', + enabled: true, + }, }, reloadConnectors: () => { return new Promise<void>(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx index 6ef2f62315d9a4..6d98a5e3d120f4 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -75,8 +75,8 @@ describe('connector_add_flyout', () => { editFlyoutVisible: false, setEditFlyoutVisibility: state => {}, actionTypesIndex: { - 'first-action-type': { id: 'first-action-type', name: 'first' }, - 'second-action-type': { id: 'second-action-type', name: 'second' }, + 'first-action-type': { id: 'first-action-type', name: 'first', enabled: true }, + 'second-action-type': { id: 'second-action-type', name: 'second', enabled: true }, }, reloadConnectors: () => { return new Promise<void>(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx index 71ba52f047d617..a03296c7c36792 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_add_flyout.test.tsx @@ -58,7 +58,9 @@ describe('connector_add_flyout', () => { setAddFlyoutVisibility: state => {}, editFlyoutVisible: false, setEditFlyoutVisibility: state => {}, - actionTypesIndex: { 'my-action-type': { id: 'my-action-type', name: 'test' } }, + actionTypesIndex: { + 'my-action-type': { id: 'my-action-type', name: 'test', enabled: true }, + }, reloadConnectors: () => { return new Promise<void>(() => {}); }, diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index 57e950a98eb2ae..0dc38523bfab83 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -84,7 +84,7 @@ describe('connector_edit_flyout', () => { editFlyoutVisible: true, setEditFlyoutVisibility: state => {}, actionTypesIndex: { - 'test-action-type-id': { id: 'test-action-type-id', name: 'test' }, + 'test-action-type-id': { id: 'test-action-type-id', name: 'test', enabled: true }, }, reloadConnectors: () => { return new Promise<void>(() => {}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx new file mode 100644 index 00000000000000..228bceb87cad7d --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.test.tsx @@ -0,0 +1,519 @@ +/* + * 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 React from 'react'; +import uuid from 'uuid'; +import { shallow } from 'enzyme'; +import { AlertDetails } from './alert_details'; +import { Alert, ActionType } from '../../../../types'; +import { EuiTitle, EuiBadge, EuiFlexItem, EuiButtonEmpty, EuiSwitch } from '@elastic/eui'; +import { times, random } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; + +jest.mock('../../../app_context', () => ({ + useAppDependencies: jest.fn(() => ({ + http: jest.fn(), + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), +})); + +jest.mock('../../../lib/capabilities', () => ({ + hasSaveAlertsCapability: jest.fn(() => true), +})); + +const mockAlertApis = { + muteAlert: jest.fn(), + unmuteAlert: jest.fn(), + enableAlert: jest.fn(), + disableAlert: jest.fn(), +}; + +// const AlertDetails = withBulkAlertOperations(RawAlertDetails); +describe('alert_details', () => { + // mock Api handlers + + it('renders the alert name as a title', () => { + const alert = mockAlert(); + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ).containsMatchingElement( + <EuiTitle size="m"> + <h1>{alert.name}</h1> + </EuiTitle> + ) + ).toBeTruthy(); + }); + + it('renders the alert type badge', () => { + const alert = mockAlert(); + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ).containsMatchingElement(<EuiBadge>{alertType.name}</EuiBadge>) + ).toBeTruthy(); + }); + + describe('actions', () => { + it('renders an alert action', () => { + const alert = mockAlert({ + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ], + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + }, + ]; + + expect( + shallow( + <AlertDetails + alert={alert} + alertType={alertType} + actionTypes={actionTypes} + {...mockAlertApis} + /> + ).containsMatchingElement( + <EuiFlexItem grow={false}> + <EuiBadge color="hollow">{actionTypes[0].name}</EuiBadge> + </EuiFlexItem> + ) + ).toBeTruthy(); + }); + + it('renders a counter for multiple alert action', () => { + const actionCount = random(1, 10); + const alert = mockAlert({ + actions: [ + { + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.server-log', + }, + ...times(actionCount, () => ({ + group: 'default', + id: uuid.v4(), + params: {}, + actionTypeId: '.email', + })), + ], + }); + const alertType = { + id: '.noop', + name: 'No Op', + }; + const actionTypes: ActionType[] = [ + { + id: '.server-log', + name: 'Server log', + enabled: true, + }, + { + id: '.email', + name: 'Send email', + enabled: true, + }, + ]; + + const details = shallow( + <AlertDetails + alert={alert} + alertType={alertType} + actionTypes={actionTypes} + {...mockAlertApis} + /> + ); + + expect( + details.containsMatchingElement( + <EuiFlexItem grow={false}> + <EuiBadge color="hollow">{actionTypes[0].name}</EuiBadge> + </EuiFlexItem> + ) + ).toBeTruthy(); + + expect( + details.containsMatchingElement( + <EuiFlexItem grow={false}> + <EuiBadge color="hollow">{`+${actionCount}`}</EuiBadge> + </EuiFlexItem> + ) + ).toBeTruthy(); + }); + }); + + describe('links', () => { + it('links to the Edit flyout', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ).containsMatchingElement( + <EuiButtonEmpty disabled={true} iconType="pencil"> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel" + defaultMessage="Edit" + /> + </EuiButtonEmpty> + ) + ).toBeTruthy(); + }); + + it('links to the app that created the alert', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ).containsMatchingElement( + <EuiButtonEmpty disabled={true} iconType="popout"> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel" + defaultMessage="View in app" + /> + </EuiButtonEmpty> + ) + ).toBeTruthy(); + }); + + it('links to the activity log', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + expect( + shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ).containsMatchingElement( + <EuiButtonEmpty disabled={true} iconType="menuLeft"> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertDetails.activityLogButtonLabel" + defaultMessage="Activity Log" + /> + </EuiButtonEmpty> + ) + ).toBeTruthy(); + }); + }); +}); + +describe('enable button', () => { + it('should render an enable button when alert is enabled', () => { + const alert = mockAlert({ + enabled: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: true, + disabled: false, + }); + }); + + it('should render an enable button when alert is disabled', () => { + const alert = mockAlert({ + enabled: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: false, + }); + }); + + it('should enable the alert when alert is disabled and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const disableAlert = jest.fn(); + const enableButton = shallow( + <AlertDetails + alert={alert} + alertType={alertType} + actionTypes={[]} + {...mockAlertApis} + disableAlert={disableAlert} + /> + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(disableAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(disableAlert).toHaveBeenCalledTimes(1); + }); + + it('should disable the alert when alert is enabled and button is clicked', () => { + const alert = mockAlert({ + enabled: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableAlert = jest.fn(); + const enableButton = shallow( + <AlertDetails + alert={alert} + alertType={alertType} + actionTypes={[]} + {...mockAlertApis} + enableAlert={enableAlert} + /> + ) + .find(EuiSwitch) + .find('[name="enable"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(enableAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(enableAlert).toHaveBeenCalledTimes(1); + }); +}); + +describe('mute button', () => { + it('should render an mute button when alert is enabled', () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: false, + }); + }); + + it('should render an muted button when alert is muted', () => { + const alert = mockAlert({ + enabled: true, + muteAll: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: true, + disabled: false, + }); + }); + + it('should mute the alert when alert is unmuted and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const muteAlert = jest.fn(); + const enableButton = shallow( + <AlertDetails + alert={alert} + alertType={alertType} + actionTypes={[]} + {...mockAlertApis} + muteAlert={muteAlert} + /> + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(muteAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(muteAlert).toHaveBeenCalledTimes(1); + }); + + it('should unmute the alert when alert is muted and button is clicked', () => { + const alert = mockAlert({ + enabled: true, + muteAll: true, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const unmuteAlert = jest.fn(); + const enableButton = shallow( + <AlertDetails + alert={alert} + alertType={alertType} + actionTypes={[]} + {...mockAlertApis} + unmuteAlert={unmuteAlert} + /> + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + enableButton.simulate('click'); + const handler = enableButton.prop('onChange'); + expect(typeof handler).toEqual('function'); + expect(unmuteAlert).toHaveBeenCalledTimes(0); + handler!({} as React.FormEvent); + expect(unmuteAlert).toHaveBeenCalledTimes(1); + }); + + it('should disabled mute button when alert is disabled', () => { + const alert = mockAlert({ + enabled: false, + muteAll: false, + }); + + const alertType = { + id: '.noop', + name: 'No Op', + }; + + const enableButton = shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ) + .find(EuiSwitch) + .find('[name="mute"]') + .first(); + + expect(enableButton.props()).toMatchObject({ + checked: false, + disabled: true, + }); + }); +}); + +function mockAlert(overloads: Partial<Alert> = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx new file mode 100644 index 00000000000000..ffdf846efd49d5 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details.tsx @@ -0,0 +1,176 @@ +/* + * 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 { indexBy } from 'lodash'; +import { + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiPage, + EuiPageContentBody, + EuiButtonEmpty, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useAppDependencies } from '../../../app_context'; +import { hasSaveAlertsCapability } from '../../../lib/capabilities'; +import { Alert, AlertType, ActionType } from '../../../../types'; +import { + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; + +type AlertDetailsProps = { + alert: Alert; + alertType: AlertType; + actionTypes: ActionType[]; +} & Pick<BulkOperationsComponentOpts, 'disableAlert' | 'enableAlert' | 'unmuteAlert' | 'muteAlert'>; + +export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ + alert, + alertType, + actionTypes, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, +}) => { + const { capabilities } = useAppDependencies(); + + const canSave = hasSaveAlertsCapability(capabilities); + + const actionTypesByTypeId = indexBy(actionTypes, 'id'); + const [firstAction, ...otherActions] = alert.actions; + + const [isEnabled, setIsEnabled] = useState<boolean>(alert.enabled); + const [isMuted, setIsMuted] = useState<boolean>(alert.muteAll); + + return ( + <EuiPage> + <EuiPageBody> + <EuiPageContent> + <EuiPageContentHeader> + <EuiPageContentHeaderSection> + <EuiTitle size="m"> + <h1 data-test-subj="alertDetailsTitle">{alert.name}</h1> + </EuiTitle> + </EuiPageContentHeaderSection> + <EuiPageContentHeaderSection> + <EuiFlexGroup responsive={false} gutterSize="xs"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty disabled={true} iconType="pencil"> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel" + defaultMessage="Edit" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty disabled={true} iconType="popout"> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertDetails.viewAlertInAppButtonLabel" + defaultMessage="View in app" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty disabled={true} iconType="menuLeft"> + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertDetails.activityLogButtonLabel" + defaultMessage="Activity Log" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPageContentHeaderSection> + </EuiPageContentHeader> + <EuiPageContentBody> + <EuiFlexGroup wrap responsive={false} gutterSize="m"> + <EuiFlexItem grow={false}> + <EuiFlexGroup wrap responsive={false} gutterSize="xs"> + <EuiFlexItem grow={false}> + <EuiBadge data-test-subj="alertTypeLabel">{alertType.name}</EuiBadge> + </EuiFlexItem> + {firstAction && ( + <EuiFlexItem grow={false}> + <EuiBadge color="hollow" data-test-subj="actionTypeLabel"> + {actionTypesByTypeId[firstAction.actionTypeId].name ?? + firstAction.actionTypeId} + </EuiBadge> + </EuiFlexItem> + )} + {otherActions.length ? ( + <EuiFlexItem grow={false} data-test-subj="actionCountLabel"> + <EuiBadge color="hollow">+{otherActions.length}</EuiBadge> + </EuiFlexItem> + ) : null} + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <EuiFlexGroup wrap responsive={false} gutterSize="m"> + <EuiFlexItem grow={false}> + <EuiSwitch + name="enable" + disabled={!canSave} + checked={isEnabled} + data-test-subj="enableSwitch" + onChange={async () => { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(alert); + } else { + setIsEnabled(true); + await enableAlert(alert); + } + }} + label={ + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.enableTitle" + defaultMessage="Enable" + /> + } + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiSwitch + name="mute" + checked={isMuted} + disabled={!canSave || !isEnabled} + data-test-subj="muteSwitch" + onChange={async () => { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(alert); + } else { + setIsMuted(true); + await muteAlert(alert); + } + }} + label={ + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertsList.collapsedItemActons.muteTitle" + defaultMessage="Mute" + /> + } + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPageContentBody> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); +}; + +export const AlertDetailsWithApi = withBulkAlertOperations(AlertDetails); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx new file mode 100644 index 00000000000000..7a40104e97d9fd --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.test.tsx @@ -0,0 +1,409 @@ +/* + * 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 React from 'react'; +import uuid from 'uuid'; +import { shallow } from 'enzyme'; +import { createMemoryHistory, createLocation } from 'history'; +import { ToastsApi } from 'kibana/public'; +import { AlertDetailsRoute, getAlertData } from './alert_details_route'; +import { Alert } from '../../../../types'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +jest.mock('../../../app_context', () => { + const toastNotifications = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ toastNotifications })), + }; +}); +describe('alert_details_route', () => { + it('render a loader while fetching data', () => { + const alert = mockAlert(); + + expect( + shallow( + <AlertDetailsRoute {...mockRouterProps(alert)} {...mockApis()} /> + ).containsMatchingElement(<EuiLoadingSpinner size="l" />) + ).toBeTruthy(); + }); +}); + +describe('getAlertData useEffect handler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('fetches alert', async () => { + const alert = mockAlert(); + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementationOnce(async () => alert); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + + expect(loadAlert).toHaveBeenCalledWith(alert.id); + expect(setAlert).toHaveBeenCalledWith(alert); + }); + + it('fetches alert and action types', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + const alertType = { + id: alert.alertTypeId, + name: 'type name', + }; + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + + expect(loadAlertTypes).toHaveBeenCalledTimes(1); + expect(loadActionTypes).toHaveBeenCalledTimes(1); + + expect(setAlertType).toHaveBeenCalledWith(alertType); + expect(setActionTypes).toHaveBeenCalledWith([actionType]); + }); + + it('displays an error if the alert isnt found', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => { + throw new Error('OMG'); + }); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG', + }); + }); + + it('displays an error if the alert type isnt loaded', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + + loadAlertTypes.mockImplementation(async () => { + throw new Error('OMG no alert type'); + }); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG no alert type', + }); + }); + + it('displays an error if the action type isnt loaded', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + const alertType = { + id: alert.alertTypeId, + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => { + throw new Error('OMG no action type'); + }); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: 'Unable to load alert: OMG no action type', + }); + }); + + it('displays an error if the alert type isnt found', async () => { + const actionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: actionType.id, + params: {}, + }, + ], + }); + + const alertType = { + id: uuid.v4(), + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [actionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: `Unable to load alert: Invalid Alert Type: ${alert.alertTypeId}`, + }); + }); + + it('displays an error if an action type isnt found', async () => { + const availableActionType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const missingActionType = { + id: '.noop', + name: 'No Op', + enabled: true, + }; + const alert = mockAlert({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: availableActionType.id, + params: {}, + }, + { + group: '', + id: uuid.v4(), + actionTypeId: missingActionType.id, + params: {}, + }, + ], + }); + + const alertType = { + id: uuid.v4(), + name: 'type name', + }; + + const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementation(async () => alert); + loadAlertTypes.mockImplementation(async () => [alertType]); + loadActionTypes.mockImplementation(async () => [availableActionType]); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getAlertData( + alert.id, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); + expect(toastNotifications.addDanger).toHaveBeenCalledWith({ + title: `Unable to load alert: Invalid Action Type: ${missingActionType.id}`, + }); + }); +}); + +function mockApis() { + return { + loadAlert: jest.fn(), + loadAlertTypes: jest.fn(), + loadActionTypes: jest.fn(), + }; +} + +function mockStateSetter() { + return { + setAlert: jest.fn(), + setAlertType: jest.fn(), + setActionTypes: jest.fn(), + }; +} + +function mockRouterProps(alert: Alert) { + return { + match: { + isExact: false, + path: `/alert/${alert.id}`, + url: '', + params: { alertId: alert.id }, + }, + history: createMemoryHistory(), + location: createLocation(`/alert/${alert.id}`), + }; +} +function mockAlert(overloads: Partial<Alert> = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx new file mode 100644 index 00000000000000..4e00ea304d987d --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alert_details/components/alert_details_route.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { ToastsApi } from 'kibana/public'; +import { Alert, AlertType, ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { AlertDetailsWithApi as AlertDetails } from './alert_details'; +import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators'; +import { + ComponentOpts as AlertApis, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; +import { + ComponentOpts as ActionApis, + withActionOperations, +} from '../../common/components/with_actions_api_operations'; + +type AlertDetailsRouteProps = RouteComponentProps<{ + alertId: string; +}> & + Pick<ActionApis, 'loadActionTypes'> & + Pick<AlertApis, 'loadAlert' | 'loadAlertTypes'>; + +export const AlertDetailsRoute: React.FunctionComponent<AlertDetailsRouteProps> = ({ + match: { + params: { alertId }, + }, + loadAlert, + loadAlertTypes, + loadActionTypes, +}) => { + const { http, toastNotifications } = useAppDependencies(); + + const [alert, setAlert] = useState<Alert | null>(null); + const [alertType, setAlertType] = useState<AlertType | null>(null); + const [actionTypes, setActionTypes] = useState<ActionType[] | null>(null); + + useEffect(() => { + getAlertData( + alertId, + loadAlert, + loadAlertTypes, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); + }, [alertId, http, loadActionTypes, loadAlert, loadAlertTypes, toastNotifications]); + + return alert && alertType && actionTypes ? ( + <AlertDetails alert={alert} alertType={alertType} actionTypes={actionTypes} /> + ) : ( + <div + style={{ + textAlign: 'center', + margin: '4em 0em', + }} + > + <EuiLoadingSpinner size="l" /> + </div> + ); +}; + +export async function getAlertData( + alertId: string, + loadAlert: AlertApis['loadAlert'], + loadAlertTypes: AlertApis['loadAlertTypes'], + loadActionTypes: ActionApis['loadActionTypes'], + setAlert: React.Dispatch<React.SetStateAction<Alert | null>>, + setAlertType: React.Dispatch<React.SetStateAction<AlertType | null>>, + setActionTypes: React.Dispatch<React.SetStateAction<ActionType[] | null>>, + toastNotifications: Pick<ToastsApi, 'addDanger'> +) { + try { + const loadedAlert = await loadAlert(alertId); + setAlert(loadedAlert); + + const [loadedAlertType, loadedActionTypes] = await Promise.all<AlertType, ActionType[]>([ + loadAlertTypes() + .then(types => types.find(type => type.id === loadedAlert.alertTypeId)) + .then(throwIfAbsent(`Invalid Alert Type: ${loadedAlert.alertTypeId}`)), + loadActionTypes().then( + throwIfIsntContained( + new Set(loadedAlert.actions.map(action => action.actionTypeId)), + (requiredActionType: string) => `Invalid Action Type: ${requiredActionType}`, + (action: ActionType) => action.id + ) + ), + ]); + + setAlertType(loadedAlertType); + setActionTypes(loadedActionTypes); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.unableToLoadAlertMessage', + { + defaultMessage: 'Unable to load alert: {message}', + values: { + message: e.message, + }, + } + ), + }); + } +} + +export const AlertDetailsRouteWithApi = withActionOperations( + withBulkAlertOperations(AlertDetailsRoute) +); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx index ff1510ea873d3b..f410fff44172fa 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -21,7 +21,14 @@ jest.mock('../../../lib/alert_api', () => ({ loadAlerts: jest.fn(), loadAlertTypes: jest.fn(), })); - +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), + useLocation: () => ({ + pathname: '/triggersActions/alerts/', + }), +})); const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx index 12122983161bd8..32de924f63e807 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/alerts_list.tsx @@ -15,19 +15,23 @@ import { EuiFlexItem, EuiIcon, EuiSpacer, + EuiLink, } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; import { AlertsContextProvider } from '../../../context/alerts_context'; import { useAppDependencies } from '../../../app_context'; import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; import { AlertAdd } from '../../alert_add'; -import { BulkActionPopover } from './bulk_action_popover'; -import { CollapsedItemActions } from './collapsed_item_actions'; +import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; +import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; +import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; import { loadAlerts, loadAlertTypes } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; +import { routeToAlertDetails } from '../../../constants'; const ENTER_KEY = 13; @@ -43,6 +47,7 @@ interface AlertState { } export const AlertsList: React.FunctionComponent = () => { + const history = useHistory(); const { http, injectedMetadata, toastNotifications, capabilities } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); @@ -151,6 +156,18 @@ export const AlertsList: React.FunctionComponent = () => { sortable: false, truncateText: true, 'data-test-subj': 'alertsTableCell-name', + render: (name: string, alert: AlertTableItem) => { + return ( + <EuiLink + title={name} + onClick={() => { + history.push(routeToAlertDetails.replace(`:alertId`, alert.id)); + }} + > + {name} + </EuiLink> + ); + }, }, { field: 'tagsText', @@ -236,17 +253,19 @@ export const AlertsList: React.FunctionComponent = () => { <EuiFlexGroup> {selectedIds.length > 0 && canDelete && ( <EuiFlexItem grow={false}> - <BulkActionPopover - selectedItems={convertAlertsToTableItems( - filterAlertsById(alertsState.data, selectedIds), - alertTypesState.data - )} - onPerformingAction={() => setIsPerformingAction(true)} - onActionPerformed={() => { - loadAlertsData(); - setIsPerformingAction(false); - }} - /> + <BulkOperationPopover> + <AlertQuickEditButtons + selectedItems={convertAlertsToTableItems( + filterAlertsById(alertsState.data, selectedIds), + alertTypesState.data + )} + onPerformingAction={() => setIsPerformingAction(true)} + onActionPerformed={() => { + loadAlertsData(); + setIsPerformingAction(false); + }} + /> + </BulkOperationPopover> </EuiFlexItem> )} <EuiFlexItem> diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index aa1c6dd7c5b9ac..2bac159ed79ede 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -20,23 +20,25 @@ import { AlertTableItem } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { hasDeleteAlertsCapability, hasSaveAlertsCapability } from '../../../lib/capabilities'; import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, -} from '../../../lib/alert_api'; + ComponentOpts as BulkOperationsComponentOpts, + withBulkAlertOperations, +} from '../../common/components/with_bulk_alert_api_operations'; -export interface ComponentOpts { +export type ComponentOpts = { item: AlertTableItem; onAlertChanged: () => void; -} +} & BulkOperationsComponentOpts; export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({ item, onAlertChanged, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, + deleteAlert, }: ComponentOpts) => { - const { http, capabilities } = useAppDependencies(); + const { capabilities } = useAppDependencies(); const canDelete = hasDeleteAlertsCapability(capabilities); const canSave = hasSaveAlertsCapability(capabilities); @@ -71,9 +73,9 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({ data-test-subj="enableSwitch" onChange={async () => { if (item.enabled) { - await disableAlerts({ http, ids: [item.id] }); + await disableAlert(item); } else { - await enableAlerts({ http, ids: [item.id] }); + await enableAlert(item); } onAlertChanged(); }} @@ -93,9 +95,9 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({ data-test-subj="muteSwitch" onChange={async () => { if (item.muteAll) { - await unmuteAlerts({ http, ids: [item.id] }); + await unmuteAlert(item); } else { - await muteAlerts({ http, ids: [item.id] }); + await muteAlert(item); } onAlertChanged(); }} @@ -115,7 +117,7 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({ color="text" data-test-subj="deleteAlert" onClick={async () => { - await deleteAlerts({ http, ids: [item.id] }); + await deleteAlert(item); onAlertChanged(); }} > @@ -129,3 +131,5 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({ </EuiPopover> ); }; + +export const CollapsedItemActionsWithApi = withBulkAlertOperations(CollapsedItemActions); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx similarity index 52% rename from x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx rename to x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx index 59ec52ac83a6c5..9635e6cd119836 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/alerts_list/components/bulk_action_popover.tsx +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/alert_quick_edit_buttons.tsx @@ -5,34 +5,35 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiButtonEmpty, EuiFormRow, EuiPopover } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; -import { AlertTableItem } from '../../../../types'; +import { Alert } from '../../../../types'; import { useAppDependencies } from '../../../app_context'; import { - deleteAlerts, - disableAlerts, - enableAlerts, - muteAlerts, - unmuteAlerts, -} from '../../../lib/alert_api'; + withBulkAlertOperations, + ComponentOpts as BulkOperationsComponentOpts, +} from './with_bulk_alert_api_operations'; -export interface ComponentOpts { - selectedItems: AlertTableItem[]; - onPerformingAction: () => void; - onActionPerformed: () => void; -} +export type ComponentOpts = { + selectedItems: Alert[]; + onPerformingAction?: () => void; + onActionPerformed?: () => void; +} & BulkOperationsComponentOpts; -export const BulkActionPopover: React.FunctionComponent<ComponentOpts> = ({ +export const AlertQuickEditButtons: React.FunctionComponent<ComponentOpts> = ({ selectedItems, - onPerformingAction, - onActionPerformed, + onPerformingAction = noop, + onActionPerformed = noop, + muteAlerts, + unmuteAlerts, + enableAlerts, + disableAlerts, + deleteAlerts, }: ComponentOpts) => { - const { http, toastNotifications } = useAppDependencies(); + const { toastNotifications } = useAppDependencies(); - const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false); const [isMutingAlerts, setIsMutingAlerts] = useState<boolean>(false); const [isUnmutingAlerts, setIsUnmutingAlerts] = useState<boolean>(false); const [isEnablingAlerts, setIsEnablingAlerts] = useState<boolean>(false); @@ -47,9 +48,8 @@ export const BulkActionPopover: React.FunctionComponent<ComponentOpts> = ({ async function onmMuteAllClick() { onPerformingAction(); setIsMutingAlerts(true); - const ids = selectedItems.filter(item => !isAlertMuted(item)).map(item => item.id); try { - await muteAlerts({ http, ids }); + await muteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -68,9 +68,8 @@ export const BulkActionPopover: React.FunctionComponent<ComponentOpts> = ({ async function onUnmuteAllClick() { onPerformingAction(); setIsUnmutingAlerts(true); - const ids = selectedItems.filter(isAlertMuted).map(item => item.id); try { - await unmuteAlerts({ http, ids }); + await unmuteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -89,9 +88,8 @@ export const BulkActionPopover: React.FunctionComponent<ComponentOpts> = ({ async function onEnableAllClick() { onPerformingAction(); setIsEnablingAlerts(true); - const ids = selectedItems.filter(isAlertDisabled).map(item => item.id); try { - await enableAlerts({ http, ids }); + await enableAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -110,9 +108,8 @@ export const BulkActionPopover: React.FunctionComponent<ComponentOpts> = ({ async function onDisableAllClick() { onPerformingAction(); setIsDisablingAlerts(true); - const ids = selectedItems.filter(item => !isAlertDisabled(item)).map(item => item.id); try { - await disableAlerts({ http, ids }); + await disableAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -131,9 +128,8 @@ export const BulkActionPopover: React.FunctionComponent<ComponentOpts> = ({ async function deleteSelectedItems() { onPerformingAction(); setIsDeletingAlerts(true); - const ids = selectedItems.map(item => item.id); try { - await deleteAlerts({ http, ids }); + await deleteAlerts(selectedItems); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( @@ -150,104 +146,83 @@ export const BulkActionPopover: React.FunctionComponent<ComponentOpts> = ({ } return ( - <EuiPopover - isOpen={isPopoverOpen} - closePopover={() => setIsPopoverOpen(false)} - data-test-subj="bulkAction" - button={ - <EuiButton - iconType="arrowDown" - iconSide="right" - onClick={() => setIsPopoverOpen(!isPopoverOpen)} + <Fragment> + {!allAlertsMuted && ( + <EuiButtonEmpty + onClick={onmMuteAllClick} + isLoading={isMutingAlerts} + isDisabled={isPerformingAction} + data-test-subj="muteAll" > <FormattedMessage - id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle" - defaultMessage="Manage alerts" + id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.muteAllTitle" + defaultMessage="Mute" /> - </EuiButton> - } - > - {!allAlertsMuted && ( - <EuiFormRow> - <EuiButtonEmpty - onClick={onmMuteAllClick} - isLoading={isMutingAlerts} - isDisabled={isPerformingAction} - data-test-subj="muteAll" - > - <FormattedMessage - id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.muteAllTitle" - defaultMessage="Mute" - /> - </EuiButtonEmpty> - </EuiFormRow> + </EuiButtonEmpty> )} {allAlertsMuted && ( - <EuiFormRow> - <EuiButtonEmpty - onClick={onUnmuteAllClick} - isLoading={isUnmutingAlerts} - isDisabled={isPerformingAction} - data-test-subj="unmuteAll" - > - <FormattedMessage - id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.unmuteAllTitle" - defaultMessage="Unmute" - /> - </EuiButtonEmpty> - </EuiFormRow> + <EuiButtonEmpty + onClick={onUnmuteAllClick} + isLoading={isUnmutingAlerts} + isDisabled={isPerformingAction} + data-test-subj="unmuteAll" + > + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.unmuteAllTitle" + defaultMessage="Unmute" + /> + </EuiButtonEmpty> )} {allAlertsDisabled && ( - <EuiFormRow> - <EuiButtonEmpty - onClick={onEnableAllClick} - isLoading={isEnablingAlerts} - isDisabled={isPerformingAction} - data-test-subj="enableAll" - > - <FormattedMessage - id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.enableAllTitle" - defaultMessage="Enable" - /> - </EuiButtonEmpty> - </EuiFormRow> + <EuiButtonEmpty + onClick={onEnableAllClick} + isLoading={isEnablingAlerts} + isDisabled={isPerformingAction} + data-test-subj="enableAll" + > + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.enableAllTitle" + defaultMessage="Enable" + /> + </EuiButtonEmpty> )} {!allAlertsDisabled && ( - <EuiFormRow> - <EuiButtonEmpty - onClick={onDisableAllClick} - isLoading={isDisablingAlerts} - isDisabled={isPerformingAction} - data-test-subj="disableAll" - > - <FormattedMessage - id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle" - defaultMessage="Disable" - /> - </EuiButtonEmpty> - </EuiFormRow> - )} - <EuiFormRow> <EuiButtonEmpty - onClick={deleteSelectedItems} - isLoading={isDeletingAlerts} + onClick={onDisableAllClick} + isLoading={isDisablingAlerts} isDisabled={isPerformingAction} - data-test-subj="deleteAll" + data-test-subj="disableAll" > <FormattedMessage - id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle" - defaultMessage="Delete" + id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle" + defaultMessage="Disable" /> </EuiButtonEmpty> - </EuiFormRow> - </EuiPopover> + )} + + <EuiButtonEmpty + onClick={deleteSelectedItems} + isLoading={isDeletingAlerts} + isDisabled={isPerformingAction} + data-test-subj="deleteAll" + > + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle" + defaultMessage="Delete" + /> + </EuiButtonEmpty> + </Fragment> ); }; -function isAlertDisabled(alert: AlertTableItem) { +export const AlertQuickEditButtonsWithApi = withBulkAlertOperations(AlertQuickEditButtons); + +function isAlertDisabled(alert: Alert) { return alert.enabled === false; } -function isAlertMuted(alert: AlertTableItem) { +function isAlertMuted(alert: Alert) { return alert.muteAll === true; } + +function noop() {} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx new file mode 100644 index 00000000000000..d0fd0e17928183 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/bulk_operation_popover.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFormRow, EuiPopover } from '@elastic/eui'; + +export const BulkOperationPopover: React.FunctionComponent = ({ children }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false); + + return ( + <EuiPopover + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + data-test-subj="bulkAction" + button={ + <EuiButton + iconType="arrowDown" + iconSide="right" + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + > + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle" + defaultMessage="Manage alerts" + /> + </EuiButton> + } + > + {children && + React.Children.map(children, child => + React.isValidElement(child) ? ( + <EuiFormRow>{React.cloneElement(child, {})}</EuiFormRow> + ) : ( + child + ) + )} + </EuiPopover> + ); +}; diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx new file mode 100644 index 00000000000000..dd6b8775ba3d09 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.test.tsx @@ -0,0 +1,51 @@ +/* + * 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 React from 'react'; +import { shallow, mount } from 'enzyme'; +import { withActionOperations, ComponentOpts } from './with_actions_api_operations'; +import * as actionApis from '../../../lib/action_connector_api'; +import { useAppDependencies } from '../../../app_context'; + +jest.mock('../../../lib/action_connector_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + })), + }; +}); + +describe('with_action_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with Action Api methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.loadActionTypes).toEqual('function'); + return <div />; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + expect(shallow(<ExtendedComponent />).type()).toEqual(ComponentToExtend); + }); + + it('loadActionTypes calls the loadActionTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadActionTypes }: ComponentOpts) => { + return <button onClick={() => loadActionTypes()}>{'call api'}</button>; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + const component = mount(<ExtendedComponent />); + component.find('button').simulate('click'); + + expect(actionApis.loadActionTypes).toHaveBeenCalledTimes(1); + expect(actionApis.loadActionTypes).toHaveBeenCalledWith({ http }); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx new file mode 100644 index 00000000000000..45e6c6b10532ce --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx @@ -0,0 +1,28 @@ +/* + * 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 { ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { loadActionTypes } from '../../../lib/action_connector_api'; + +export interface ComponentOpts { + loadActionTypes: () => Promise<ActionType[]>; +} + +export type PropsWithOptionalApiHandlers<T> = Omit<T, keyof ComponentOpts> & Partial<ComponentOpts>; + +export function withActionOperations<T>( + WrappedComponent: React.ComponentType<T & ComponentOpts> +): React.FunctionComponent<PropsWithOptionalApiHandlers<T>> { + return (props: PropsWithOptionalApiHandlers<T>) => { + const { http } = useAppDependencies(); + return ( + <WrappedComponent {...(props as T)} loadActionTypes={async () => loadActionTypes({ http })} /> + ); + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx new file mode 100644 index 00000000000000..30a065479ce33c --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx @@ -0,0 +1,269 @@ +/* + * 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 React from 'react'; +import { shallow, mount } from 'enzyme'; +import uuid from 'uuid'; +import { withBulkAlertOperations, ComponentOpts } from './with_bulk_alert_api_operations'; +import * as alertApi from '../../../lib/alert_api'; +import { useAppDependencies } from '../../../app_context'; +import { Alert } from '../../../../types'; + +jest.mock('../../../lib/alert_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), + }; +}); + +describe('with_bulk_alert_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with AlertApi methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.muteAlerts).toEqual('function'); + expect(typeof props.unmuteAlerts).toEqual('function'); + expect(typeof props.enableAlerts).toEqual('function'); + expect(typeof props.disableAlerts).toEqual('function'); + expect(typeof props.deleteAlerts).toEqual('function'); + expect(typeof props.muteAlert).toEqual('function'); + expect(typeof props.unmuteAlert).toEqual('function'); + expect(typeof props.enableAlert).toEqual('function'); + expect(typeof props.disableAlert).toEqual('function'); + expect(typeof props.deleteAlert).toEqual('function'); + expect(typeof props.loadAlert).toEqual('function'); + expect(typeof props.loadAlertTypes).toEqual('function'); + return <div />; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + expect(shallow(<ExtendedComponent />).type()).toEqual(ComponentToExtend); + }); + + // single alert + it('muteAlert calls the muteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return <button onClick={() => muteAlert(alert)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(<ExtendedComponent alert={alert} />); + component.find('button').simulate('click'); + + expect(alertApi.muteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('unmuteAlert calls the unmuteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return <button onClick={() => unmuteAlert(alert)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ muteAll: true }); + const component = mount(<ExtendedComponent alert={alert} />); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('enableAlert calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return <button onClick={() => enableAlert(alert)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ enabled: false }); + const component = mount(<ExtendedComponent alert={alert} />); + component.find('button').simulate('click'); + + expect(alertApi.enableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('disableAlert calls the disableAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return <button onClick={() => disableAlert(alert)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(<ExtendedComponent alert={alert} />); + component.find('button').simulate('click'); + + expect(alertApi.disableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('deleteAlert calls the deleteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return <button onClick={() => deleteAlert(alert)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(<ExtendedComponent alert={alert} />); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + // bulk alerts + it('muteAlerts calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return <button onClick={() => muteAlerts(alerts)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(<ExtendedComponent alerts={alerts} />); + component.find('button').simulate('click'); + + expect(alertApi.muteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('unmuteAlerts calls the unmuteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return <button onClick={() => unmuteAlerts(alerts)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert({ muteAll: true }), mockAlert({ muteAll: true })]; + const component = mount(<ExtendedComponent alerts={alerts} />); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('enableAlerts calls the muteAlertss api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return <button onClick={() => enableAlerts(alerts)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [ + mockAlert({ enabled: false }), + mockAlert({ enabled: true }), + mockAlert({ enabled: false }), + ]; + const component = mount(<ExtendedComponent alerts={alerts} />); + component.find('button').simulate('click'); + + expect(alertApi.enableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[2].id], http }); + }); + + it('disableAlerts calls the disableAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return <button onClick={() => disableAlerts(alerts)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(<ExtendedComponent alerts={alerts} />); + component.find('button').simulate('click'); + + expect(alertApi.disableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlerts).toHaveBeenCalledWith({ + ids: [alerts[0].id, alerts[1].id], + http, + }); + }); + + it('deleteAlerts calls the deleteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return <button onClick={() => deleteAlerts(alerts)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(<ExtendedComponent alerts={alerts} />); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('loadAlert calls the loadAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ + loadAlert, + alertId, + }: ComponentOpts & { alertId: Alert['id'] }) => { + return <button onClick={() => loadAlert(alertId)}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alertId = uuid.v4(); + const component = mount(<ExtendedComponent alertId={alertId} />); + component.find('button').simulate('click'); + + expect(alertApi.loadAlert).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http }); + }); + + it('loadAlertTypes calls the loadAlertTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => { + return <button onClick={() => loadAlertTypes()}>{'call api'}</button>; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const component = mount(<ExtendedComponent />); + component.find('button').simulate('click'); + + expect(alertApi.loadAlertTypes).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlertTypes).toHaveBeenCalledWith({ http }); + }); +}); + +function mockAlert(overloads: Partial<Alert> = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx new file mode 100644 index 00000000000000..c61ba631ab8685 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -0,0 +1,103 @@ +/* + * 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 { Alert, AlertType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { + deleteAlerts, + disableAlerts, + enableAlerts, + muteAlerts, + unmuteAlerts, + deleteAlert, + disableAlert, + enableAlert, + muteAlert, + unmuteAlert, + loadAlert, + loadAlertTypes, +} from '../../../lib/alert_api'; + +export interface ComponentOpts { + muteAlerts: (alerts: Alert[]) => Promise<void>; + unmuteAlerts: (alerts: Alert[]) => Promise<void>; + enableAlerts: (alerts: Alert[]) => Promise<void>; + disableAlerts: (alerts: Alert[]) => Promise<void>; + deleteAlerts: (alerts: Alert[]) => Promise<void>; + muteAlert: (alert: Alert) => Promise<void>; + unmuteAlert: (alert: Alert) => Promise<void>; + enableAlert: (alert: Alert) => Promise<void>; + disableAlert: (alert: Alert) => Promise<void>; + deleteAlert: (alert: Alert) => Promise<void>; + loadAlert: (id: Alert['id']) => Promise<Alert>; + loadAlertTypes: () => Promise<AlertType[]>; +} + +export type PropsWithOptionalApiHandlers<T> = Omit<T, keyof ComponentOpts> & Partial<ComponentOpts>; + +export function withBulkAlertOperations<T>( + WrappedComponent: React.ComponentType<T & ComponentOpts> +): React.FunctionComponent<PropsWithOptionalApiHandlers<T>> { + return (props: PropsWithOptionalApiHandlers<T>) => { + const { http } = useAppDependencies(); + return ( + <WrappedComponent + {...(props as T)} + muteAlerts={async (items: Alert[]) => + muteAlerts({ http, ids: items.filter(item => !isAlertMuted(item)).map(item => item.id) }) + } + unmuteAlerts={async (items: Alert[]) => + unmuteAlerts({ http, ids: items.filter(isAlertMuted).map(item => item.id) }) + } + enableAlerts={async (items: Alert[]) => + enableAlerts({ http, ids: items.filter(isAlertDisabled).map(item => item.id) }) + } + disableAlerts={async (items: Alert[]) => + disableAlerts({ + http, + ids: items.filter(item => !isAlertDisabled(item)).map(item => item.id), + }) + } + deleteAlerts={async (items: Alert[]) => + deleteAlerts({ http, ids: items.map(item => item.id) }) + } + muteAlert={async (alert: Alert) => { + if (!isAlertMuted(alert)) { + return muteAlert({ http, id: alert.id }); + } + }} + unmuteAlert={async (alert: Alert) => { + if (isAlertMuted(alert)) { + return unmuteAlert({ http, id: alert.id }); + } + }} + enableAlert={async (alert: Alert) => { + if (isAlertDisabled(alert)) { + return enableAlert({ http, id: alert.id }); + } + }} + disableAlert={async (alert: Alert) => { + if (!isAlertDisabled(alert)) { + return disableAlert({ http, id: alert.id }); + } + }} + deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })} + loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })} + loadAlertTypes={async () => loadAlertTypes({ http })} + /> + ); + }; +} + +function isAlertDisabled(alert: Alert) { + return alert.enabled === false; +} + +function isAlertMuted(alert: Alert) { + return alert.muteAll === true; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts index ed63ade903104c..7fb7d0bf48e4d2 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeRegistry } from './application/type_registry'; -import { SanitizedAlert as Alert } from '../../../alerting/common'; -export { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { ActionType } from '../../../../../plugins/actions/common'; + +export { Alert, AlertAction }; +export { ActionType }; export type ActionTypeIndex = Record<string, ActionType>; export type AlertTypeIndex = Record<string, AlertType>; @@ -47,11 +50,6 @@ export interface ValidationResult { errors: Record<string, any>; } -export interface ActionType { - id: string; - name: string; -} - export interface ActionConnector { secrets: Record<string, any>; id: string; diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap index 2d3351ec1c0d20..da9153f4a6c8d6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap @@ -6,26 +6,18 @@ exports[`FilterBar renders 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={false} - iconSide="right" key="all" numFilters={2} onClick={[Function]} - type="button" > all </EuiFilterButton> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={true} - iconSide="right" key="critical" numFilters={2} onClick={[Function]} - type="button" > critical </EuiFilterButton> diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap index b36e0c1a2bfdb0..dfc69c57cfff6b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap @@ -6,24 +6,16 @@ exports[`GroupByBar renders 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={true} - iconSide="right" key="message" onClick={[Function]} - type="button" > by issue </EuiFilterButton> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={false} - iconSide="right" key="index" onClick={[Function]} - type="button" > by index </EuiFilterButton> diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx index a6607ca81fc183..cbdd921a36e81b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx @@ -15,34 +15,40 @@ import { getPingHistogram } from '../../../state/actions'; import { selectPingHistogram } from '../../../state/selectors'; import { withResponsiveWrapper, ResponsiveWrapperProps } from '../../higher_order'; import { GetPingHistogramParams, HistogramResult } from '../../../../common/types'; +import { useUrlParams } from '../../../hooks'; -type Props = GetPingHistogramParams & - ResponsiveWrapperProps & - PingHistogramComponentProps & - DispatchProps & { lastRefresh: number }; +type Props = ResponsiveWrapperProps & + Pick<PingHistogramComponentProps, 'height' | 'data' | 'loading'> & + DispatchProps & { lastRefresh: number; monitorId?: string }; const PingHistogramContainer: React.FC<Props> = ({ data, loadData, - statusFilter, - filters, - dateStart, - dateEnd, - absoluteStartDate, - absoluteEndDate, monitorId, lastRefresh, - ...props + height, + loading, }) => { + const [getUrlParams] = useUrlParams(); + const { + absoluteDateRangeStart, + absoluteDateRangeEnd, + dateRangeStart: dateStart, + dateRangeEnd: dateEnd, + statusFilter, + filters, + } = getUrlParams(); + useEffect(() => { loadData({ monitorId, dateStart, dateEnd, statusFilter, filters }); }, [loadData, dateStart, dateEnd, monitorId, filters, statusFilter, lastRefresh]); return ( <PingHistogramComponent data={data} - absoluteStartDate={absoluteStartDate} - absoluteEndDate={absoluteEndDate} - {...props} + absoluteStartDate={absoluteDateRangeStart} + absoluteEndDate={absoluteDateRangeEnd} + height={height} + loading={loading} /> ); }; @@ -68,7 +74,7 @@ const mapDispatchToProps = (dispatch: any): DispatchProps => ({ export const PingHistogram = connect< StateProps, DispatchProps, - PingHistogramComponentProps, + Pick<PingHistogramComponentProps, 'height'>, AppState >( mapStateToProps, diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx new file mode 100644 index 00000000000000..6d01ebae1e100f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx @@ -0,0 +1,95 @@ +/* + * 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, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { useUrlParams } from '../../../hooks'; +import { AppState } from '../../../state'; +import { fetchSnapshotCount } from '../../../state/actions'; +import { SnapshotComponent } from '../../functional/snapshot'; +import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; + +/** + * Props expected from parent components. + */ +interface OwnProps { + /** + * Height is needed, since by default charts takes height of 100% + */ + height?: string; +} + +/** + * Props given by the Redux store based on action input. + */ +interface StoreProps { + count: SnapshotType; + lastRefresh: number; + loading: boolean; +} + +/** + * Contains functions that will dispatch actions used + * for this component's life cycle + */ +interface DispatchProps { + loadSnapshotCount: typeof fetchSnapshotCount; +} + +/** + * Props used to render the Snapshot component. + */ +type Props = OwnProps & StoreProps & DispatchProps; + +export const Container: React.FC<Props> = ({ + count, + height, + lastRefresh, + loading, + loadSnapshotCount, +}: Props) => { + const [getUrlParams] = useUrlParams(); + const { dateRangeStart, dateRangeEnd, statusFilter, filters } = getUrlParams(); + + useEffect(() => { + loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); + }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, loadSnapshotCount, statusFilter]); + return <SnapshotComponent count={count} height={height} loading={loading} />; +}; + +/** + * Provides state to connected component. + * @param state the root app state + */ +const mapStateToProps = ({ + snapshot: { count, loading }, + ui: { lastRefresh }, +}: AppState): StoreProps => ({ + count, + lastRefresh, + loading, +}); + +/** + * Used for fetching snapshot counts. + * @param dispatch redux-provided action dispatcher + */ +const mapDispatchToProps = (dispatch: any) => ({ + loadSnapshotCount: ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): DispatchProps => { + return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); + }, +}); + +export const Snapshot = connect<StoreProps, DispatchProps, OwnProps>( + // @ts-ignore connect is expecting null | undefined for some reason + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx index 2d1c21d1c997da..569c6bb883cbdc 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { useUrlParams } from '../../../hooks'; import { parseFiltersMap } from '../../functional/filter_group/parse_filter_map'; @@ -12,6 +12,7 @@ import { AppState } from '../../../state'; import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions'; import { FilterGroupComponent } from '../../functional/filter_group'; import { OverviewFilters } from '../../../../common/runtime_types/overview_filters'; +import { UptimeRefreshContext } from '../../../contexts'; interface OwnProps { esFilters?: string; @@ -37,8 +38,9 @@ export const Container: React.FC<Props> = ({ loadFilterGroup, overviewFilters, }: Props) => { - const [getUrlParams, updateUrl] = useUrlParams(); + const { lastRefresh } = useContext(UptimeRefreshContext); + const [getUrlParams, updateUrl] = useUrlParams(); const { dateRangeStart, dateRangeEnd, statusFilter, filters: urlFilters } = getUrlParams(); useEffect(() => { @@ -53,7 +55,16 @@ export const Container: React.FC<Props> = ({ statusFilter, tags: filterSelections.tags ?? [], }); - }, [dateRangeStart, dateRangeEnd, esKuery, esFilters, statusFilter, urlFilters, loadFilterGroup]); + }, [ + lastRefresh, + dateRangeStart, + dateRangeEnd, + esKuery, + esFilters, + statusFilter, + urlFilters, + loadFilterGroup, + ]); // update filters in the URL from filter group const onFilterUpdate = (filtersKuery: string) => { diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts index 5bb0d1ae8468f9..2fd4c762cf45f5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts @@ -5,6 +5,9 @@ */ export { PingHistogram } from './charts/ping_histogram'; +export { Snapshot } from './charts/snapshot_container'; export { KueryBar } from './kuerybar/kuery_bar_container'; export { OverviewPage } from './pages/overview_container'; export { FilterGroup } from './filter_group/filter_group_container'; +export { MonitorStatusDetails } from './monitor/status_details_container'; +export { MonitorStatusBar } from './monitor/status_bar_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx new file mode 100644 index 00000000000000..db6337732091a9 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -0,0 +1,85 @@ +/* + * 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, { useContext, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AppState } from '../../../state'; +import { selectMonitorLocations, selectMonitorStatus } from '../../../state/selectors'; +import { MonitorStatusBarComponent } from '../../functional/monitor_status_details/monitor_status_bar'; +import { getMonitorStatus, getSelectedMonitor } from '../../../state/actions'; +import { useUrlParams } from '../../../hooks'; +import { Ping } from '../../../../common/graphql/types'; +import { MonitorLocations } from '../../../../common/runtime_types/monitor'; +import { UptimeRefreshContext } from '../../../contexts'; + +interface StateProps { + monitorStatus: Ping; + monitorLocations: MonitorLocations; +} + +interface DispatchProps { + loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => void; +} + +interface OwnProps { + monitorId: string; +} + +type Props = OwnProps & StateProps & DispatchProps; + +export const Container: React.FC<Props> = ({ + loadMonitorStatus, + monitorId, + monitorStatus, + monitorLocations, +}: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorStatus(dateStart, dateEnd, monitorId); + }, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh]); + + return ( + <MonitorStatusBarComponent + monitorId={monitorId} + monitorStatus={monitorStatus} + monitorLocations={monitorLocations} + /> + ); +}; + +const mapStateToProps = (state: AppState, ownProps: OwnProps) => ({ + monitorStatus: selectMonitorStatus(state), + monitorLocations: selectMonitorLocations(state, ownProps.monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch<any>): DispatchProps => ({ + loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => { + dispatch( + getMonitorStatus({ + monitorId, + dateStart, + dateEnd, + }) + ); + dispatch( + getSelectedMonitor({ + monitorId, + }) + ); + }, +}); + +// @ts-ignore TODO: Investigate typescript issues here +export const MonitorStatusBar = connect<StateProps, DispatchProps, MonitorStatusBarProps>( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx new file mode 100644 index 00000000000000..6929e3bd64c4d0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useContext, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { useUrlParams } from '../../../hooks'; +import { AppState } from '../../../state'; +import { selectMonitorLocations } from '../../../state/selectors'; +import { fetchMonitorLocations, MonitorLocationsPayload } from '../../../state/actions/monitor'; +import { MonitorStatusDetailsComponent } from '../../functional/monitor_status_details'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { UptimeRefreshContext } from '../../../contexts'; + +interface OwnProps { + monitorId: string; +} + +interface StoreProps { + monitorLocations: MonitorLocations; +} + +interface DispatchProps { + loadMonitorLocations: typeof fetchMonitorLocations; +} + +type Props = OwnProps & StoreProps & DispatchProps; + +export const Container: React.FC<Props> = ({ + loadMonitorLocations, + monitorLocations, + monitorId, +}: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorLocations({ dateStart, dateEnd, monitorId }); + }, [loadMonitorLocations, monitorId, dateStart, dateEnd, lastRefresh]); + + return ( + <MonitorStatusDetailsComponent monitorId={monitorId} monitorLocations={monitorLocations} /> + ); +}; +const mapStateToProps = (state: AppState, { monitorId }: OwnProps) => ({ + monitorLocations: selectMonitorLocations(state, monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ + loadMonitorLocations: (params: MonitorLocationsPayload) => { + dispatch(fetchMonitorLocations(params)); + }, +}); + +export const MonitorStatusDetails = connect<StoreProps, DispatchProps, OwnProps>( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap index f6846dfb1164da..9853ed5cadfc9b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap @@ -180,8 +180,6 @@ exports[`MonitorCharts component renders the component without errors 1`] = ` }, } } - dateRangeEnd="2011-12-03T10:15:30+01:00" - dateRangeStart="2011-12-03T10:15:30+01:00" loading={false} mean="mean" monitorId="something" diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx index 81c60c8fbeaaa9..331b5c9c0b0965 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx @@ -73,8 +73,6 @@ describe('MonitorCharts component', () => { range="range" success="success" monitorId="something" - dateRangeStart="2011-12-03T10:15:30+01:00" - dateRangeEnd="2011-12-03T10:15:30+01:00" /> ) ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx index d645eb21ac7766..214b0394369f7a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { Snapshot } from '../../../../common/runtime_types'; -import { PresentationalComponent } from '../snapshot'; +import { SnapshotComponent } from '../snapshot'; describe('Snapshot component', () => { const snapshot: Snapshot = { @@ -17,7 +17,7 @@ describe('Snapshot component', () => { }; it('renders without errors', () => { - const wrapper = shallowWithIntl(<PresentationalComponent count={snapshot} loading={false} />); + const wrapper = shallowWithIntl(<SnapshotComponent count={snapshot} loading={false} />); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap index c3b99c9785cbe0..8ca73879cab8c0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap @@ -1,67 +1,107 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MonitorBarSeries component renders a series when there are down items 1`] = ` +exports[`MonitorBarSeries component renders if the data series is present 1`] = ` <div - style={ - Object { - "height": 50, - "marginRight": 15, - "maxWidth": "1200px", - "width": "100%", - } - } + style="height:50px;width:100%;max-width:1200px;margin-right:15px" > - <Chart - renderer="canvas" + <div + class="echChart" > - <Connect(spec) - xDomain={ - Object { - "max": 1548700920000, - "min": 1548697920000, - } - } + <div + class="echChartStatus" + data-ech-render-complete="false" + data-ech-render-count="0" /> - <Connect(spec) - hide={true} - id="bottom" - position="bottom" - tickFormat={[Function]} + <div + class="echChartResizer" /> - <Connect(spec) - customSeriesColors={ - Array [ - "A danger color", - ] - } - data={ - Array [ - Array [ - 124, - 1, - ], - Array [ - 125, - 1, - ], - Array [ - 126, - 1, - ], - ] - } - id="downSeries" - name="Down checks" - timeZone="local" - xAccessor={0} - xScaleType="time" - yAccessors={ - Array [ - 1, - ] - } - yScaleType="linear" - /> - </Chart> + <div + class="echContainer" + > + <div + class="echReactiveChart_unavailable" + > + <p> + No data to display + </p> + </div> + </div> + </div> </div> `; + +exports[`MonitorBarSeries component shallow renders a series when there are down items 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <MonitorBarSeries + dangerColor="A danger color" + histogramSeries={ + Array [ + Object { + "down": 1, + "timestamp": 124, + "up": 0, + }, + Object { + "down": 1, + "timestamp": 125, + "up": 0, + }, + Object { + "down": 1, + "timestamp": 126, + "up": 0, + }, + ] + } + /> +</ContextProvider> +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx index 3cede0be00ef1c..c3e98134e438d8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx @@ -5,15 +5,16 @@ */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series'; +import { renderWithRouter } from '../../../../lib'; +import { SummaryHistogramPoint } from '../../../../../common/graphql/types'; describe('MonitorBarSeries component', () => { let props: MonitorBarSeriesProps; + let histogramSeries: SummaryHistogramPoint[]; beforeEach(() => { props = { - absoluteStartDate: 1548697920000, - absoluteEndDate: 1548700920000, dangerColor: 'A danger color', histogramSeries: [ { @@ -33,20 +34,144 @@ describe('MonitorBarSeries component', () => { }, ], }; + histogramSeries = [ + { timestamp: 1580387868000, up: 0, down: 5 }, + { timestamp: 1580387904000, up: 0, down: 20 }, + { + timestamp: 1580387940000, + up: 0, + down: 19, + }, + { + timestamp: 1580387976000, + up: 0, + down: 16, + }, + { + timestamp: 1580388012000, + up: 0, + down: 20, + }, + { + timestamp: 1580388048000, + up: 0, + down: 15, + }, + { + timestamp: 1580388084000, + up: 0, + down: 20, + }, + { + timestamp: 1580388120000, + up: 0, + down: 19, + }, + { + timestamp: 1580388156000, + up: 0, + down: 16, + }, + { + timestamp: 1580388192000, + up: 0, + down: 20, + }, + { + timestamp: 1580388228000, + up: 0, + down: 15, + }, + { + timestamp: 1580388264000, + up: 0, + down: 20, + }, + { + timestamp: 1580388300000, + up: 0, + down: 19, + }, + { + timestamp: 1580388336000, + up: 0, + down: 16, + }, + { + timestamp: 1580388372000, + up: 0, + down: 20, + }, + { + timestamp: 1580388408000, + up: 0, + down: 15, + }, + { + timestamp: 1580388444000, + up: 0, + down: 20, + }, + { + timestamp: 1580388480000, + up: 0, + down: 19, + }, + { + timestamp: 1580388516000, + up: 0, + down: 16, + }, + { + timestamp: 1580388552000, + up: 0, + down: 20, + }, + { + timestamp: 1580388588000, + up: 0, + down: 15, + }, + { + timestamp: 1580388624000, + up: 0, + down: 20, + }, + { + timestamp: 1580388660000, + up: 0, + down: 19, + }, + { + timestamp: 1580388696000, + up: 0, + down: 16, + }, + { + timestamp: 1580388732000, + up: 0, + down: 20, + }, + { + timestamp: 1580388768000, + up: 0, + down: 10, + }, + ]; }); - it('renders a series when there are down items', () => { - const component = shallowWithIntl(<MonitorBarSeries {...props} />); + it('shallow renders a series when there are down items', () => { + const component = shallowWithIntl(renderWithRouter(<MonitorBarSeries {...props} />)); expect(component).toMatchSnapshot(); }); - it('renders null when there are no down items', () => { + it('shallow renders null when there are no down items', () => { props.histogramSeries = []; - const component = shallowWithIntl(<MonitorBarSeries {...props} />); + const component = shallowWithIntl(renderWithRouter(<MonitorBarSeries {...props} />)); expect(component).toEqual({}); }); - it('renders nothing if the down count has no counts', () => { + it(' shallow renders nothing if the down count has no counts', () => { props.histogramSeries = [ { timestamp: 123, @@ -64,19 +189,21 @@ describe('MonitorBarSeries component', () => { up: 0, }, ]; - const component = shallowWithIntl(<MonitorBarSeries {...props} />); + const component = shallowWithIntl(renderWithRouter(<MonitorBarSeries {...props} />)); expect(component).toEqual({}); }); - it('renders nothing if the data series is null', () => { + it('shallow renders nothing if the data series is null', () => { const component = shallowWithIntl( - <MonitorBarSeries - absoluteStartDate={123} - absoluteEndDate={124} - dangerColor="danger" - histogramSeries={null} - /> + renderWithRouter(<MonitorBarSeries dangerColor="danger" histogramSeries={null} />) ); expect(component).toEqual({}); }); + + it('renders if the data series is present', () => { + const component = renderWithIntl( + renderWithRouter(<MonitorBarSeries dangerColor="danger" histogramSeries={histogramSeries} />) + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx index ce91bf5b1638f4..2338bf02783485 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx @@ -19,16 +19,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiToolTip } from '@elastic/eui'; import { SummaryHistogramPoint } from '../../../../common/graphql/types'; import { getChartDateLabel, seriesHasDownValues } from '../../../lib/helper'; +import { useUrlParams } from '../../../hooks'; export interface MonitorBarSeriesProps { - /** - * The date/time for the start of the timespan. - */ - absoluteStartDate: number; - /** - * The date/time for the end of the timespan. - */ - absoluteEndDate: number; /** * The color to use for the display of down states. */ @@ -44,23 +37,23 @@ export interface MonitorBarSeriesProps { * so we will only render the series component if there are down counts for the selected monitor. * @param props - the values for the monitor this chart visualizes */ -export const MonitorBarSeries = ({ - absoluteStartDate, - absoluteEndDate, - dangerColor, - histogramSeries, -}: MonitorBarSeriesProps) => { +export const MonitorBarSeries = ({ dangerColor, histogramSeries }: MonitorBarSeriesProps) => { + const [getUrlParams] = useUrlParams(); + const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); + const id = 'downSeries'; return seriesHasDownValues(histogramSeries) ? ( <div style={{ height: 50, width: '100%', maxWidth: '1200px', marginRight: 15 }}> <Chart> - <Settings xDomain={{ min: absoluteStartDate, max: absoluteEndDate }} /> + <Settings xDomain={{ min: absoluteDateRangeStart, max: absoluteDateRangeEnd }} /> <Axis hide id="bottom" position={Position.Bottom} - tickFormat={timeFormatter(getChartDateLabel(absoluteStartDate, absoluteEndDate))} + tickFormat={timeFormatter( + getChartDateLabel(absoluteDateRangeStart, absoluteDateRangeEnd) + )} /> <BarSeries id={id} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts index 6af17cfd67c462..4ec4cf4f52607a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/index.ts @@ -6,13 +6,11 @@ export { DonutChart } from './charts/donut_chart'; export { EmptyState } from './empty_state'; -export { MonitorStatusBar } from './monitor_status_details'; export { IntegrationLink } from './integration_link'; export { KueryBarComponent } from './kuery_bar/kuery_bar'; export { MonitorCharts } from './monitor_charts'; export { MonitorList } from './monitor_list'; export { OverviewPageParsingErrorCallout } from './overview_page_parsing_error_callout'; export { PingList } from './ping_list'; -export { Snapshot } from './snapshot'; export { PingHistogramComponent } from './charts'; export { StatusPanel } from './status_panel'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_charts.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_charts.tsx index ae99d08ab634e0..a5fbb78bdf0594 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_charts.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_charts.tsx @@ -11,7 +11,6 @@ import { MonitorChart } from '../../../common/graphql/types'; import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order'; import { monitorChartsQuery } from '../../queries'; import { DurationChart } from './charts'; -import { useUrlParams } from '../../hooks'; import { PingHistogram } from '../connected'; interface MonitorChartsQueryResult { @@ -24,29 +23,16 @@ interface MonitorChartsProps { mean: string; range: string; success: string; - dateRangeStart: string; - dateRangeEnd: string; } type Props = MonitorChartsProps & UptimeGraphQLQueryProps<MonitorChartsQueryResult>; -export const MonitorChartsComponent = ({ - data, - mean, - range, - monitorId, - dateRangeStart, - dateRangeEnd, - loading, -}: Props) => { - const [getUrlParams] = useUrlParams(); +export const MonitorChartsComponent = ({ data, mean, range, monitorId, loading }: Props) => { if (data && data.monitorChartsData) { const { monitorChartsData: { locationDurationLines }, } = data; - const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); - return ( <EuiFlexGroup> <EuiFlexItem> @@ -58,15 +44,7 @@ export const MonitorChartsComponent = ({ /> </EuiFlexItem> <EuiFlexItem> - <PingHistogram - absoluteStartDate={absoluteDateRangeStart} - absoluteEndDate={absoluteDateRangeEnd} - height="400px" - isResponsive={false} - dateStart={dateRangeStart} - dateEnd={dateRangeEnd} - monitorId={monitorId} - /> + <PingHistogram height="400px" isResponsive={false} monitorId={monitorId} /> </EuiFlexItem> </EuiFlexGroup> ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index f779efca7b18a7..3655db5aaff1ea 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -106,6 +106,560 @@ exports[`MonitorList component renders a no items message when no data is provid `; exports[`MonitorList component renders the monitor list 1`] = ` +.c1 { + padding-left: 17px; +} + +.c2 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width:574px) { + .c0 { + min-width: 230px; + } +} + +<div + class="euiPanel euiPanel--paddingMedium" +> + <h5 + class="euiTitle euiTitle--xsmall" + > + Monitor status + </h5> + <div + class="euiSpacer euiSpacer--s" + /> + <div + aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 2 items." + class="euiBasicTable" + > + <div> + <div + class="euiTableHeaderMobile" + > + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> + <table + class="euiTable euiTable--responsive" + > + <caption + aria-live="polite" + aria-relevant="text" + class="euiScreenReaderOnly euiTableCaption" + role="status" + /> + <thead> + <tr> + <th + class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" + data-test-subj="tableHeaderCell_state.monitor.status_0" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Status + </span> + </div> + </th> + <th + class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" + data-test-subj="tableHeaderCell_state.monitor.name_1" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Name + </span> + </div> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_state.url.full_2" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent" + > + <span + class="euiTableCellContent__text" + > + Url + </span> + </div> + </th> + <th + class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" + data-test-subj="tableHeaderCell_histogram.points_3" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent euiTableCellContent--alignCenter" + > + <span + class="euiTableCellContent__text" + > + Downtime history + </span> + </div> + </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_monitor_id_4" + role="columnheader" + scope="col" + style="width:24px" + > + <div + class="euiTableCellContent euiTableCellContent--alignRight" + > + <span + class="euiTableCellContent__text" + /> + </div> + </th> + </tr> + </thead> + <tbody> + <tr + class="euiTableRow euiTableRow-hasActions euiTableRow-isExpandable" + > + <td + class="euiTableRowCell euiTableRowCell--isMobileFullWidth" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Status + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow c0" + > + <div + class="euiFlexItem euiFlexItem--flexGrow1" + style="flex-basis:40px" + > + <div + class="euiHealth" + style="display:block" + > + <div + class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> + <span + class="c1" + > + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--extraSmall" + > + <div + class="euiTextColor euiTextColor--subdued" + > + 1897 Yr ago + </div> + </div> + </span> + </span> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrow2" + > + <div + class="euiText euiText--small" + > + in 0/1 Location + </div> + </div> + </div> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--isMobileFullWidth" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <button + class="euiLink euiLink--primary" + type="button" + > + <a + data-test-subj="monitor-page-link-foo" + href="/monitor/Zm9v" + > + Unnamed - foo + </a> + </button> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Url + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <button + class="euiLink euiLink--text c2" + type="button" + > + + <svg + aria-hidden="true" + class="euiIcon euiIcon--small euiIcon-isLoading" + focusable="false" + height="16" + role="img" + style="fill:subbdued" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </button> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--hideForMobile" + > + <div + class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent" + > + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + <div + class="euiTextColor euiTextColor--secondary" + > + -- + </div> + </div> + </span> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--isExpander" + style="width:24px" + > + <div + class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent" + > + <button + aria-label="Expand row for monitor with ID foo" + class="euiButtonIcon euiButtonIcon--primary" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </button> + </div> + </td> + </tr> + <tr + class="euiTableRow euiTableRow-hasActions euiTableRow-isExpandable" + > + <td + class="euiTableRowCell euiTableRowCell--isMobileFullWidth" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Status + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <div + class="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow c0" + > + <div + class="euiFlexItem euiFlexItem--flexGrow1" + style="flex-basis:40px" + > + <div + class="euiHealth" + style="display:block" + > + <div + class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + /> + </div> + </div> + <span + class="c1" + > + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--extraSmall" + > + <div + class="euiTextColor euiTextColor--subdued" + > + 1895 Yr ago + </div> + </div> + </span> + </span> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrow2" + > + <div + class="euiText euiText--small" + > + in 1/1 Location + </div> + </div> + </div> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--isMobileFullWidth" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Name + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <button + class="euiLink euiLink--primary" + type="button" + > + <a + data-test-subj="monitor-page-link-bar" + href="/monitor/YmFy" + > + Unnamed - bar + </a> + </button> + </div> + </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + Url + </div> + <div + class="euiTableCellContent euiTableCellContent--overflowingContent" + > + <button + class="euiLink euiLink--text c2" + type="button" + > + + <svg + aria-hidden="true" + class="euiIcon euiIcon--small euiIcon-isLoading" + focusable="false" + height="16" + role="img" + style="fill:subbdued" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </button> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--hideForMobile" + > + <div + class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent" + > + <span + class="euiToolTipAnchor" + > + <div + class="euiText euiText--medium" + > + <div + class="euiTextColor euiTextColor--secondary" + > + -- + </div> + </div> + </span> + </div> + </td> + <td + class="euiTableRowCell euiTableRowCell--isExpander" + style="width:24px" + > + <div + class="euiTableCellContent euiTableCellContent--alignRight euiTableCellContent--overflowingContent" + > + <button + aria-label="Expand row for monitor with ID bar" + class="euiButtonIcon euiButtonIcon--primary" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </button> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> + <div + class="euiSpacer euiSpacer--m" + /> + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="A disabled pagination button indicating that there cannot be any further navigation in the monitors list." + class="euiButtonIcon euiButtonIcon--text" + data-test-subj="xpack.uptime.monitorList.prevButton" + disabled="" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </button> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <button + aria-label="A disabled pagination button indicating that there cannot be any further navigation in the monitors list." + class="euiButtonIcon euiButtonIcon--text" + data-test-subj="xpack.uptime.monitorList.nextButton" + disabled="" + type="button" + > + <svg + aria-hidden="true" + class="euiIcon euiIcon--medium euiIcon-isLoading euiButtonIcon__icon" + focusable="false" + height="16" + role="img" + viewBox="0 0 16 16" + width="16" + xmlns="http://www.w3.org/2000/svg" + /> + </button> + </div> + </div> +</div> +`; + +exports[`MonitorList component shallow renders the monitor list 1`] = ` <Fragment> <EuiPanel> <EuiTitle diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap index 03a5a15eea1a47..29ab7a8455fe67 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list_pagination.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MonitorList component renders a no items message when no data is provided 1`] = ` +exports[`MonitorListPagination component renders a no items message when no data is provided 1`] = ` <Fragment> <EuiPanel> <EuiTitle @@ -105,7 +105,7 @@ exports[`MonitorList component renders a no items message when no data is provid </Fragment> `; -exports[`MonitorList component renders the monitor list 1`] = ` +exports[`MonitorListPagination component renders the monitor list 1`] = ` <Fragment> <EuiPanel> <EuiTitle diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx index fbb477713cb983..bb9ce59ea62b17 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list.test.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { MonitorSummaryResult } from '../../../../../common/graphql/types'; import { MonitorListComponent } from '../monitor_list'; +import { renderWithRouter } from '../../../../lib'; describe('MonitorList component', () => { let result: MonitorSummaryResult; @@ -81,11 +82,9 @@ describe('MonitorList component', () => { }; }); - it('renders the monitor list', () => { + it('shallow renders the monitor list', () => { const component = shallowWithIntl( <MonitorListComponent - absoluteStartDate={123} - absoluteEndDate={125} dangerColor="danger" data={{ monitorStates: result }} hasActiveFilters={false} @@ -100,8 +99,6 @@ describe('MonitorList component', () => { it('renders a no items message when no data is provided', () => { const component = shallowWithIntl( <MonitorListComponent - absoluteStartDate={123} - absoluteEndDate={125} dangerColor="danger" data={{}} hasActiveFilters={false} @@ -111,4 +108,20 @@ describe('MonitorList component', () => { ); expect(component).toMatchSnapshot(); }); + + it('renders the monitor list', () => { + const component = renderWithIntl( + renderWithRouter( + <MonitorListComponent + dangerColor="danger" + data={{ monitorStates: result }} + hasActiveFilters={false} + loading={false} + successColor="primary" + /> + ) + ); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx index a1725134094559..ff54e61006156b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx @@ -13,7 +13,7 @@ import { } from '../../../../../common/graphql/types'; import { MonitorListComponent } from '../monitor_list'; -describe('MonitorList component', () => { +describe('MonitorListPagination component', () => { let result: MonitorSummaryResult; beforeEach(() => { @@ -98,8 +98,6 @@ describe('MonitorList component', () => { it('renders the monitor list', () => { const component = shallowWithIntl( <MonitorListComponent - absoluteStartDate={123} - absoluteEndDate={125} dangerColor="danger" data={{ monitorStates: result }} loading={false} @@ -114,8 +112,6 @@ describe('MonitorList component', () => { it('renders a no items message when no data is provided', () => { const component = shallowWithIntl( <MonitorListComponent - absoluteStartDate={123} - absoluteEndDate={125} dangerColor="danger" data={{}} loading={false} diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx index c8385440a7d49a..de6cc1982f1a85 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/monitor_list.tsx @@ -39,8 +39,6 @@ interface MonitorListQueryResult { } interface MonitorListProps { - absoluteStartDate: number; - absoluteEndDate: number; dangerColor: string; hasActiveFilters: boolean; successColor: string; @@ -56,16 +54,7 @@ const TruncatedEuiLink = styled(EuiLink)` `; export const MonitorListComponent = (props: Props) => { - const { - absoluteStartDate, - absoluteEndDate, - dangerColor, - data, - errors, - hasActiveFilters, - linkParameters, - loading, - } = props; + const { dangerColor, data, errors, hasActiveFilters, linkParameters, loading } = props; const [drawerIds, updateDrawerIds] = useState<string[]>([]); const items = data?.monitorStates?.summaries ?? []; @@ -132,12 +121,7 @@ export const MonitorListComponent = (props: Props) => { show: false, }, render: (histogramSeries: SummaryHistogramPoint[] | null) => ( - <MonitorBarSeries - absoluteStartDate={absoluteStartDate} - absoluteEndDate={absoluteEndDate} - dangerColor={dangerColor} - histogramSeries={histogramSeries} - /> + <MonitorBarSeries dangerColor={dangerColor} histogramSeries={histogramSeries} /> ), }, { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx similarity index 79% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx index 545405f91d5374..0a53eeb89d793f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx @@ -7,14 +7,12 @@ import moment from 'moment'; import React from 'react'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; -import { Ping } from '../../../../common/graphql/types'; -import { MonitorStatusBarComponent } from '../monitor_status_details/monitor_status_bar'; +import { MonitorStatusBarComponent } from '../monitor_status_bar'; +import { Ping } from '../../../../../common/graphql/types'; describe('MonitorStatusBar component', () => { let monitorStatus: Ping; let monitorLocations: any; - let dateStart: string; - let dateEnd: string; beforeEach(() => { monitorStatus = { @@ -46,9 +44,6 @@ describe('MonitorStatusBar component', () => { }, ], }; - - dateStart = moment('01-01-2010').toString(); - dateEnd = moment('10-10-2010').toString(); }); it('renders duration in ms, not us', () => { @@ -56,10 +51,7 @@ describe('MonitorStatusBar component', () => { <MonitorStatusBarComponent monitorStatus={monitorStatus} monitorId="id1" - dateStart={dateStart} - dateEnd={dateEnd} monitorLocations={monitorLocations} - loadMonitorStatus={jest.fn()} /> ); expect(component).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts index 7b4e1ea353c11c..385788cc825a0a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts @@ -3,34 +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 { connect } from 'react-redux'; -import { AppState } from '../../../state'; -import { selectMonitorLocations } from '../../../state/selectors'; -import { fetchMonitorLocations } from '../../../state/actions/monitor'; -import { MonitorStatusDetailsComponent } from './monitor_status_details'; -const mapStateToProps = (state: AppState, { monitorId }: any) => ({ - monitorLocations: selectMonitorLocations(state, monitorId), -}); - -const mapDispatchToProps = (dispatch: any, ownProps: any) => ({ - loadMonitorLocations: () => { - const { dateStart, dateEnd, monitorId } = ownProps; - dispatch( - fetchMonitorLocations({ - monitorId, - dateStart, - dateEnd, - }) - ); - }, -}); - -export const MonitorStatusDetails = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorStatusDetailsComponent); - -export * from './monitor_status_details'; -export { MonitorStatusBar } from './monitor_status_bar'; +export { MonitorStatusBarComponent } from './monitor_status_bar'; +export { MonitorStatusDetailsComponent } from './monitor_status_details'; export { StatusByLocations } from './monitor_status_bar/status_by_location'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts index 94bd7fa7f026bc..0cb11587eee480 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts @@ -4,47 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { - StateProps, - DispatchProps, - MonitorStatusBarComponent, - MonitorStatusBarProps, -} from './monitor_status_bar'; -import { selectMonitorStatus, selectMonitorLocations } from '../../../../state/selectors'; -import { AppState } from '../../../../state'; -import { getMonitorStatus, getSelectedMonitor } from '../../../../state/actions'; - -const mapStateToProps = (state: AppState, ownProps: MonitorStatusBarProps) => ({ - monitorStatus: selectMonitorStatus(state), - monitorLocations: selectMonitorLocations(state, ownProps.monitorId), -}); - -const mapDispatchToProps = (dispatch: Dispatch<any>, ownProps: MonitorStatusBarProps) => ({ - loadMonitorStatus: () => { - const { dateStart, dateEnd, monitorId } = ownProps; - dispatch( - getMonitorStatus({ - monitorId, - dateStart, - dateEnd, - }) - ); - dispatch( - getSelectedMonitor({ - monitorId, - }) - ); - }, -}); - -// @ts-ignore TODO: Investigate typescript issues here -export const MonitorStatusBar = connect<StateProps, DispatchProps, MonitorStatusBarProps>( - // @ts-ignore TODO: Investigate typescript issues here - mapStateToProps, - mapDispatchToProps -)(MonitorStatusBarComponent); - export { MonitorSSLCertificate } from './monitor_ssl_certificate'; -export * from './monitor_status_bar'; +export { MonitorStatusBarComponent } from './monitor_status_bar'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx index 2524039829add1..22e4377944be13 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { EuiLink, EuiTitle, @@ -13,42 +14,23 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import React, { useEffect } from 'react'; import { MonitorSSLCertificate } from './monitor_ssl_certificate'; import * as labels from './translations'; import { StatusByLocations } from './status_by_location'; import { Ping } from '../../../../../common/graphql/types'; import { MonitorLocations } from '../../../../../common/runtime_types'; -export interface StateProps { +interface MonitorStatusBarProps { + monitorId: string; monitorStatus: Ping; monitorLocations: MonitorLocations; } -export interface DispatchProps { - loadMonitorStatus: () => void; -} - -export interface MonitorStatusBarProps { - monitorId: string; - dateStart: string; - dateEnd: string; -} - -type Props = MonitorStatusBarProps & StateProps & DispatchProps; - -export const MonitorStatusBarComponent: React.FC<Props> = ({ - dateStart, - dateEnd, +export const MonitorStatusBarComponent: React.FC<MonitorStatusBarProps> = ({ monitorId, - loadMonitorStatus, monitorStatus, monitorLocations, }) => { - useEffect(() => { - loadMonitorStatus(); - }, [dateStart, dateEnd, loadMonitorStatus]); - const full = monitorStatus?.url?.full ?? ''; return ( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx index 29bd8eb3a71837..7dea73da7bba0e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx @@ -8,16 +8,13 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; import { LocationMap } from '../location_map'; -import { MonitorStatusBar } from './monitor_status_bar'; import { UptimeRefreshContext } from '../../../contexts'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { MonitorStatusBar } from '../../connected'; -interface MonitorStatusBarProps { +interface MonitorStatusDetailsProps { monitorId: string; - variables: any; - loadMonitorLocations: any; - monitorLocations: any; - dateStart: any; - dateEnd: any; + monitorLocations: MonitorLocations; } const WrapFlexItem = styled(EuiFlexItem)` @@ -28,15 +25,8 @@ const WrapFlexItem = styled(EuiFlexItem)` export const MonitorStatusDetailsComponent = ({ monitorId, - variables, - loadMonitorLocations, monitorLocations, - dateStart, - dateEnd, -}: MonitorStatusBarProps) => { - useEffect(() => { - loadMonitorLocations(monitorId); - }, [loadMonitorLocations, monitorId, dateStart, dateEnd]); +}: MonitorStatusDetailsProps) => { const { refreshApp } = useContext(UptimeRefreshContext); const [isTabActive] = useState(document.visibilityState); @@ -63,12 +53,7 @@ export const MonitorStatusDetailsComponent = ({ <EuiPanel> <EuiFlexGroup gutterSize="l" wrap responsive={true}> <EuiFlexItem grow={true}> - <MonitorStatusBar - monitorId={monitorId} - variables={variables} - dateStart={dateStart} - dateEnd={dateEnd} - /> + <MonitorStatusBar monitorId={monitorId} /> </EuiFlexItem> <WrapFlexItem grow={false}> <LocationMap monitorLocations={monitorLocations} /> diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index 90d716001cff96..8531cd1a3cc83d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -5,63 +5,28 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useEffect } from 'react'; +import React from 'react'; import { get } from 'lodash'; -import { connect } from 'react-redux'; -import { Snapshot as SnapshotType } from '../../../common/runtime_types'; import { DonutChart } from './charts'; -import { fetchSnapshotCount } from '../../state/actions'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; -import { AppState } from '../../state'; +import { Snapshot as SnapshotType } from '../../../common/runtime_types'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; -/** - * Props expected from parent components. - */ -interface OwnProps { - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; - /** - * Height is needed, since by default charts takes height of 100% - */ - height?: string; - statusFilter?: string; -} - -/** - * Props given by the Redux store based on action input. - */ -interface StoreProps { +interface SnapshotComponentProps { count: SnapshotType; - lastRefresh: number; loading: boolean; + height?: string; } /** - * Contains functions that will dispatch actions used - * for this component's life cycle - */ -interface DispatchProps { - loadSnapshotCount: typeof fetchSnapshotCount; -} - -/** - * Props used to render the Snapshot component. + * This component visualizes a KPI and histogram chart to help users quickly + * glean the status of their uptime environment. + * @param props the props required by the component */ -type Props = OwnProps & StoreProps & DispatchProps; - -type PresentationalComponentProps = Pick<StoreProps, 'count' | 'loading'> & - Pick<OwnProps, 'height'>; - -export const PresentationalComponent: React.FC<PresentationalComponentProps> = ({ - count, - height, - loading, -}) => ( +export const SnapshotComponent: React.FC<SnapshotComponentProps> = ({ count, height, loading }) => ( <ChartWrapper loading={loading} height={height}> <SnapshotHeading down={get<number>(count, 'down', 0)} total={get<number>(count, 'total', 0)} /> <EuiSpacer size="xs" /> @@ -73,59 +38,3 @@ export const PresentationalComponent: React.FC<PresentationalComponentProps> = ( /> </ChartWrapper> ); - -/** - * This component visualizes a KPI and histogram chart to help users quickly - * glean the status of their uptime environment. - * @param props the props required by the component - */ -export const Container: React.FC<Props> = ({ - count, - dateRangeStart, - dateRangeEnd, - filters, - height, - statusFilter, - lastRefresh, - loading, - loadSnapshotCount, -}: Props) => { - useEffect(() => { - loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); - }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, loadSnapshotCount, statusFilter]); - return <PresentationalComponent count={count} height={height} loading={loading} />; -}; - -/** - * Provides state to connected component. - * @param state the root app state - */ -const mapStateToProps = ({ - snapshot: { count, loading }, - ui: { lastRefresh }, -}: AppState): StoreProps => ({ - count, - lastRefresh, - loading, -}); - -/** - * Used for fetching snapshot counts. - * @param dispatch redux-provided action dispatcher - */ -const mapDispatchToProps = (dispatch: any) => ({ - loadSnapshotCount: ( - dateRangeStart: string, - dateRangeEnd: string, - filters?: string, - statusFilter?: string - ): DispatchProps => { - return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); - }, -}); - -export const Snapshot = connect<StoreProps, DispatchProps, OwnProps>( - // @ts-ignore connect is expecting null | undefined for some reason - mapStateToProps, - mapDispatchToProps -)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx index 03ab9fb5cf194b..2c0be2aa15d6fb 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx @@ -4,52 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import React from 'react'; -import { Snapshot } from './snapshot'; -import { PingHistogram } from '../connected'; - -interface StatusPanelProps { - absoluteDateRangeStart: number; - absoluteDateRangeEnd: number; - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; - statusFilter?: string; -} +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { PingHistogram, Snapshot } from '../connected'; const STATUS_CHART_HEIGHT = '160px'; -export const StatusPanel = ({ - absoluteDateRangeStart, - absoluteDateRangeEnd, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter, -}: StatusPanelProps) => ( +export const StatusPanel = ({}) => ( <EuiPanel> <EuiFlexGroup gutterSize="l"> <EuiFlexItem grow={2}> - <Snapshot - dateRangeStart={dateRangeStart} - dateRangeEnd={dateRangeEnd} - filters={filters} - height={STATUS_CHART_HEIGHT} - statusFilter={statusFilter} - /> + <Snapshot height={STATUS_CHART_HEIGHT} /> </EuiFlexItem> <EuiFlexItem grow={10}> - <PingHistogram - absoluteStartDate={absoluteDateRangeStart} - absoluteEndDate={absoluteDateRangeEnd} - dateStart={dateRangeStart} - dateEnd={dateRangeEnd} - filters={filters} - height={STATUS_CHART_HEIGHT} - statusFilter={statusFilter} - isResponsive={true} - /> + <PingHistogram height={STATUS_CHART_HEIGHT} isResponsive={true} /> </EuiFlexItem> </EuiFlexGroup> </EuiPanel> diff --git a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts index d02a6fc2afb5d2..8c9eec3abe2239 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts @@ -55,9 +55,9 @@ export const useUpdateKueryString = ( const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, indexPattern); esFilters = JSON.stringify(elasticsearchQuery); - - updateEsQueryForFilterGroup(filterQueryString, indexPattern); } + updateEsQueryForFilterGroup(filterQueryString, indexPattern); + return [esFilters]; } catch (err) { return [urlFilters, err]; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 408d2584911e0d..a8501ff14313a8 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -12,8 +12,8 @@ import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeRefreshContext, UptimeThemeContext } from '../contexts'; import { useUptimeTelemetry, useUrlParams, UptimePage } from '../hooks'; import { useTrackPageview } from '../../../infra/public'; -import { MonitorStatusDetails } from '../components/functional/monitor_status_details'; import { PageHeader } from './page_header'; +import { MonitorStatusDetails } from '../components/connected'; interface MonitorPageProps { setBreadcrumbs: UMUpdateBreadcrumbs; @@ -49,20 +49,9 @@ export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => { <Fragment> <PageHeader setBreadcrumbs={setBreadcrumbs} /> <EuiSpacer size="s" /> - <MonitorStatusDetails - monitorId={monitorId} - variables={sharedVariables} - dateStart={absoluteDateRangeStart} - dateEnd={absoluteDateRangeEnd} - /> + <MonitorStatusDetails monitorId={monitorId} /> <EuiSpacer size="s" /> - <MonitorCharts - {...colors} - monitorId={monitorId} - variables={sharedVariables} - dateRangeStart={dateRangeStart} - dateRangeEnd={dateRangeEnd} - /> + <MonitorCharts {...colors} monitorId={monitorId} variables={sharedVariables} /> <EuiSpacer size="s" /> <PingList onPageCountChange={setPingListPageCount} diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index 0f6195c5f4c66e..df6ffba6154e5c 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -83,18 +83,9 @@ export const OverviewPageComponent = ({ autocomplete, setBreadcrumbs, indexPatte {error && <OverviewPageParsingErrorCallout error={error} />} </EuiFlexGroup> <EuiSpacer size="s" /> - <StatusPanel - absoluteDateRangeStart={absoluteDateRangeStart} - absoluteDateRangeEnd={absoluteDateRangeEnd} - dateRangeStart={dateRangeStart} - dateRangeEnd={dateRangeEnd} - filters={esFilters} - statusFilter={statusFilter} - /> + <StatusPanel /> <EuiSpacer size="s" /> <MonitorList - absoluteStartDate={absoluteDateRangeStart} - absoluteEndDate={absoluteDateRangeEnd} dangerColor={colors.danger} hasActiveFilters={!!esFilters} implementsCustomErrorState={true} diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts new file mode 100644 index 00000000000000..8924d07ac0c4dc --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { QueryContext } from '../query_context'; +import { CursorPagination } from '../..'; +import { CursorDirection, SortOrder } from '../../../../../../common/graphql/types'; + +describe(QueryContext, () => { + // 10 minute range + const rangeStart = '2019-02-03T19:06:54.939Z'; + const rangeEnd = '2019-02-03T19:16:54.939Z'; + + const pagination: CursorPagination = { + cursorDirection: CursorDirection.AFTER, + sortOrder: SortOrder.DESC, + }; + + let qc: QueryContext; + beforeEach(() => (qc = new QueryContext({}, rangeStart, rangeEnd, pagination, null, 10))); + + describe('dateRangeFilter()', () => { + const expectedRange = { + range: { + '@timestamp': { + gte: rangeStart, + lte: rangeEnd, + }, + }, + }; + describe('when hasTimespan() is true', () => { + it('should create a date range filter including the timespan', async () => { + const mockHasTimespan = jest.fn(); + mockHasTimespan.mockReturnValue(true); + qc.hasTimespan = mockHasTimespan; + + expect(await qc.dateRangeFilter()).toEqual({ + bool: { + filter: [ + expectedRange, + { + bool: { + should: [ + qc.timespanClause(), + { bool: { must_not: { exists: { field: 'monitor.timespan' } } } }, + ], + }, + }, + ], + }, + }); + }); + }); + + describe('when hasTimespan() is false', () => { + it('should only use the timestamp fields in the returned filter', async () => { + const mockHasTimespan = jest.fn(); + mockHasTimespan.mockReturnValue(false); + qc.hasTimespan = mockHasTimespan; + + expect(await qc.dateRangeFilter()).toEqual(expectedRange); + }); + }); + }); + + describe('timespanClause()', () => { + it('should always cover the last 5m', () => { + // 5m expected range between GTE and LTE in the response + // since timespan is hardcoded to 5m + expect(qc.timespanClause()).toEqual({ + range: { + 'monitor.timespan': { + // end date minus 5m + gte: new Date(Date.parse(rangeEnd) - 5 * 60 * 1000).toISOString(), + lte: rangeEnd, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts index 961cc94dcea19f..a51931ba11630b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { APICaller } from 'kibana/server'; import { CursorPagination } from '../adapter_types'; import { INDEX_NAMES } from '../../../../../common/constants'; @@ -97,7 +98,7 @@ export class QueryContext { // behavior. const tsEnd = parseRelativeDate(this.dateRangeEnd, { roundUp: true })!; - const tsStart = tsEnd.subtract(5, 'minutes'); + const tsStart = moment(tsEnd).subtract(5, 'minutes'); return { range: { diff --git a/x-pack/package.json b/x-pack/package.json index ad0be351483f61..99e2a32bf3372f 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -176,7 +176,7 @@ "@elastic/apm-rum-react": "^0.3.2", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.1.0", "@elastic/node-crypto": "^1.0.0", diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 784125b83859d4..fbd7404a2f15eb 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -9,3 +9,10 @@ export interface ActionType { name: string; enabled: boolean; } + +export interface ActionResult { + id: string; + actionTypeId: string; + name: string; + config: Record<string, any>; +} diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index be6916a74fe88d..03a892a42792e2 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,7 +12,7 @@ import { GetServicesFunction, RawAction, } from '../types'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger } from '../../../event_log/server'; @@ -21,7 +21,7 @@ export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceSetup; getServices: GetServicesFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index c3e89e0c16efcc..c78b43f4ef3ba3 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -8,12 +8,12 @@ import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; import { Logger, CoreStart } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types'; export interface TaskRunnerContext { logger: Logger; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; getScopedSavedObjectsClient: CoreStart['savedObjects']['getScopedClient']; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index cb0e3347541fd7..dab09fc455ecf6 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -20,8 +20,8 @@ import { } from '../../../../src/core/server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -67,13 +67,13 @@ export interface PluginStartContract { export interface ActionsPluginsSetup { taskManager: TaskManagerSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; spaces?: SpacesPluginSetup; event_log: IEventLogService; } export interface ActionsPluginsStart { - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index bb5d553b26b364..5399d139371790 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -84,7 +84,7 @@ It allows you to monitor the performance of thousands of applications in real ti '{config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html', }, }), - euiIconType: 'logoAPM', + euiIconType: 'apmApp', artifacts, onPrem: onPremInstructions(indices), elasticCloud: createElasticCloudInstructions(cloud), diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index c52461cade0586..37d087433a2ed1 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -9,7 +9,7 @@ import { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; import { CaseService } from './services'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; function createConfig$(context: PluginInitializerContext) { return context.config.create<ConfigType>().pipe(map(config => config)); diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 684d905a5c71fe..531d5fa5b87e5b 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -21,10 +21,7 @@ import { UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; -import { - AuthenticatedUser, - PluginSetupContract as SecurityPluginSetup, -} from '../../../security/server'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; interface ClientArgs { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json new file mode 100644 index 00000000000000..4dbfe958eff68f --- /dev/null +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -0,0 +1,13 @@ +{ + "id": "data_enhanced", + "version": "8.0.0", + "kibanaVersion": "kibana", + "configPath": [ + "xpack" + ], + "requiredPlugins": [ + "data" + ], + "server": false, + "ui": true +} diff --git a/x-pack/plugins/data_enhanced/public/autocomplete/index.ts b/x-pack/plugins/data_enhanced/public/autocomplete/index.ts new file mode 100644 index 00000000000000..59670a89d2cafc --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/autocomplete/index.ts @@ -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. + */ + +export { + setupKqlQuerySuggestionProvider, + KUERY_LANGUAGE_NAME, +} from './providers/kql_query_suggestion'; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__fixtures__/index_pattern_response.json b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/__fixtures__/index_pattern_response.json rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/__fixtures__/index_pattern_response.json diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts similarity index 93% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts index e8aec0deec6d75..d993c3d8ad51d2 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.test.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.test.ts @@ -5,8 +5,8 @@ */ import { setupGetConjunctionSuggestions } from './conjunction'; -import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { autocomplete, esKuery } from '../../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; const mockKueryNode = (kueryNode: Partial<esKuery.KueryNode>) => (kueryNode as unknown) as esKuery.KueryNode; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx similarity index 69% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx index f570586274fdd8..fa655562134cca 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/conjunction.tsx +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/conjunction.tsx @@ -7,21 +7,21 @@ import React from 'react'; import { $Keys } from 'utility-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { KqlQuerySuggestionProvider } from './types'; -import { autocomplete } from '../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; const bothArgumentsText = ( <FormattedMessage - id="xpack.kueryAutocomplete.andOperatorDescription.bothArgumentsText" + id="xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText" defaultMessage="both arguments" - description="Part of xpack.kueryAutocomplete.andOperatorDescription. Full text: 'Requires both arguments to be true'" + description="Part of xpack.data.kueryAutocomplete.andOperatorDescription. Full text: 'Requires both arguments to be true'" /> ); const oneOrMoreArgumentsText = ( <FormattedMessage - id="xpack.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText" + id="xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText" defaultMessage="one or more arguments" - description="Part of xpack.kueryAutocomplete.orOperatorDescription. Full text: 'Requires one or more arguments to be true'" + description="Part of xpack.data.kueryAutocomplete.orOperatorDescription. Full text: 'Requires one or more arguments to be true'" /> ); @@ -29,20 +29,20 @@ const conjunctions: Record<string, JSX.Element> = { and: ( <p> <FormattedMessage - id="xpack.kueryAutocomplete.andOperatorDescription" + id="xpack.data.kueryAutocomplete.andOperatorDescription" defaultMessage="Requires {bothArguments} to be true" values={{ bothArguments: <span className="kbnSuggestionItem__callout">{bothArgumentsText}</span>, }} description="Full text: ' Requires both arguments to be true'. See - 'xpack.kueryAutocomplete.andOperatorDescription.bothArgumentsText' for 'both arguments' part." + 'xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText' for 'both arguments' part." /> </p> ), or: ( <p> <FormattedMessage - id="xpack.kueryAutocomplete.orOperatorDescription" + id="xpack.data.kueryAutocomplete.orOperatorDescription" defaultMessage="Requires {oneOrMoreArguments} to be true" values={{ oneOrMoreArguments: ( @@ -50,7 +50,7 @@ const conjunctions: Record<string, JSX.Element> = { ), }} description="Full text: 'Requires one or more arguments to be true'. See - 'xpack.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText' for 'one or more arguments' part." + 'xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText' for 'one or more arguments' part." /> </p> ), diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.test.ts similarity index 98% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.test.ts index 2fd5cfd17eb69f..d05fd49d266f22 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.test.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.test.ts @@ -6,8 +6,8 @@ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; import { setupGetFieldSuggestions } from './field'; -import { isFilterable, autocomplete, esKuery } from '../../../../../../src/plugins/data/public'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { isFilterable, autocomplete, esKuery } from '../../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; const mockKueryNode = (kueryNode: Partial<esKuery.KueryNode>) => (kueryNode as unknown) as esKuery.KueryNode; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.tsx similarity index 94% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.tsx index a8af884c24fc32..f04312b925436b 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/field.tsx +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/field.tsx @@ -8,14 +8,18 @@ import { flatten } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { escapeKuery } from './lib/escape_kuery'; import { sortPrefixFirst } from './sort_prefix_first'; -import { IFieldType, isFilterable, autocomplete } from '../../../../../../src/plugins/data/public'; +import { + IFieldType, + isFilterable, + autocomplete, +} from '../../../../../../../src/plugins/data/public'; import { KqlQuerySuggestionProvider } from './types'; const getDescription = (field: IFieldType) => { return ( <p> <FormattedMessage - id="xpack.kueryAutocomplete.filterResultsDescription" + id="xpack.data.kueryAutocomplete.filterResultsDescription" defaultMessage="Filter results that contain {fieldName}" values={{ fieldName: <span className="kbnSuggestionItem__callout">{field.name}</span> }} /> diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts similarity index 94% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts index 2cc15fe4c92801..6168ab8fc34628 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/index.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/index.ts @@ -11,13 +11,15 @@ import { setupGetFieldSuggestions } from './field'; import { setupGetValueSuggestions } from './value'; import { setupGetOperatorSuggestions } from './operator'; import { setupGetConjunctionSuggestions } from './conjunction'; -import { esKuery, autocomplete } from '../../../../../../src/plugins/data/public'; +import { esKuery, autocomplete } from '../../../../../../../src/plugins/data/public'; const cursorSymbol = '@kuery-cursor@'; const dedup = (suggestions: autocomplete.QuerySuggestion[]): autocomplete.QuerySuggestion[] => uniq(suggestions, ({ type, text, start, end }) => [type, text, start, end].join('|')); +export const KUERY_LANGUAGE_NAME = 'kuery'; + export const setupKqlQuerySuggestionProvider = ( core: CoreSetup ): autocomplete.QuerySuggestionsGetFn => { diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.test.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.test.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.test.ts diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/lib/escape_kuery.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/lib/escape_kuery.ts diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.test.ts similarity index 94% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.test.ts index acafc4e169c8f2..7e564b96064ef5 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.test.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.test.ts @@ -6,8 +6,8 @@ import indexPatternResponse from './__fixtures__/index_pattern_response.json'; import { setupGetOperatorSuggestions } from './operator'; -import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public'; -import { coreMock } from '../../../../../../src/core/public/mocks'; +import { autocomplete, esKuery } from '../../../../../../../src/plugins/data/public'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; const mockKueryNode = (kueryNode: Partial<esKuery.KueryNode>) => (kueryNode as unknown) as esKuery.KueryNode; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.tsx similarity index 64% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.tsx index 6e9010c4310fbe..af90e7bfe1172f 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/operator.tsx +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/operator.tsx @@ -10,48 +10,48 @@ import { $Keys } from 'utility-types'; import { flatten } from 'lodash'; import { KqlQuerySuggestionProvider } from './types'; -import { autocomplete } from '../../../../../../src/plugins/data/public'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; const equalsText = ( <FormattedMessage - id="xpack.kueryAutocomplete.equalOperatorDescription.equalsText" + id="xpack.data.kueryAutocomplete.equalOperatorDescription.equalsText" defaultMessage="equals" - description="Part of xpack.kueryAutocomplete.equalOperatorDescription. Full text: 'equals some value'" + description="Part of xpack.data.kueryAutocomplete.equalOperatorDescription. Full text: 'equals some value'" /> ); const lessThanOrEqualToText = ( <FormattedMessage - id="xpack.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText" + id="xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText" defaultMessage="less than or equal to" - description="Part of xpack.kueryAutocomplete.lessThanOrEqualOperatorDescription. Full text: 'is less than or equal to some value'" + description="Part of xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription. Full text: 'is less than or equal to some value'" /> ); const greaterThanOrEqualToText = ( <FormattedMessage - id="xpack.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText" + id="xpack.data.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText" defaultMessage="greater than or equal to" - description="Part of xpack.kueryAutocomplete.greaterThanOrEqualOperatorDescription. Full text: 'is greater than or equal to some value'" + description="Part of xpack.data.kueryAutocomplete.greaterThanOrEqualOperatorDescription. Full text: 'is greater than or equal to some value'" /> ); const lessThanText = ( <FormattedMessage - id="xpack.kueryAutocomplete.lessThanOperatorDescription.lessThanText" + id="xpack.data.kueryAutocomplete.lessThanOperatorDescription.lessThanText" defaultMessage="less than" - description="Part of xpack.kueryAutocomplete.lessThanOperatorDescription. Full text: 'is less than some value'" + description="Part of xpack.data.kueryAutocomplete.lessThanOperatorDescription. Full text: 'is less than some value'" /> ); const greaterThanText = ( <FormattedMessage - id="xpack.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText" + id="xpack.data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText" defaultMessage="greater than" - description="Part of xpack.kueryAutocomplete.greaterThanOperatorDescription. Full text: 'is greater than some value'" + description="Part of xpack.data.kueryAutocomplete.greaterThanOperatorDescription. Full text: 'is greater than some value'" /> ); const existsText = ( <FormattedMessage - id="xpack.kueryAutocomplete.existOperatorDescription.existsText" + id="xpack.data.kueryAutocomplete.existOperatorDescription.existsText" defaultMessage="exists" - description="Part of xpack.kueryAutocomplete.existOperatorDescription. Full text: 'exists in any form'" + description="Part of xpack.data.kueryAutocomplete.existOperatorDescription. Full text: 'exists in any form'" /> ); @@ -59,11 +59,11 @@ const operators = { ':': { description: ( <FormattedMessage - id="xpack.kueryAutocomplete.equalOperatorDescription" + id="xpack.data.kueryAutocomplete.equalOperatorDescription" defaultMessage="{equals} some value" values={{ equals: <span className="kbnSuggestionItem__callout">{equalsText}</span> }} description="Full text: 'equals some value'. See - 'xpack.kueryAutocomplete.equalOperatorDescription.equalsText' for 'equals' part." + 'xpack.data.kueryAutocomplete.equalOperatorDescription.equalsText' for 'equals' part." /> ), fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape', 'boolean'], @@ -71,7 +71,7 @@ const operators = { '<=': { description: ( <FormattedMessage - id="xpack.kueryAutocomplete.lessThanOrEqualOperatorDescription" + id="xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription" defaultMessage="is {lessThanOrEqualTo} some value" values={{ lessThanOrEqualTo: ( @@ -79,7 +79,7 @@ const operators = { ), }} description="Full text: 'is less than or equal to some value'. See - 'xpack.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText' for 'less than or equal to' part." + 'xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText' for 'less than or equal to' part." /> ), fieldTypes: ['number', 'date', 'ip'], @@ -87,7 +87,7 @@ const operators = { '>=': { description: ( <FormattedMessage - id="xpack.kueryAutocomplete.greaterThanOrEqualOperatorDescription" + id="xpack.data.kueryAutocomplete.greaterThanOrEqualOperatorDescription" defaultMessage="is {greaterThanOrEqualTo} some value" values={{ greaterThanOrEqualTo: ( @@ -95,7 +95,7 @@ const operators = { ), }} description="Full text: 'is greater than or equal to some value'. See - 'xpack.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText' for 'greater than or equal to' part." + 'xpack.data.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText' for 'greater than or equal to' part." /> ), fieldTypes: ['number', 'date', 'ip'], @@ -103,11 +103,11 @@ const operators = { '<': { description: ( <FormattedMessage - id="xpack.kueryAutocomplete.lessThanOperatorDescription" + id="xpack.data.kueryAutocomplete.lessThanOperatorDescription" defaultMessage="is {lessThan} some value" values={{ lessThan: <span className="kbnSuggestionItem__callout">{lessThanText}</span> }} description="Full text: 'is less than some value'. See - 'xpack.kueryAutocomplete.lessThanOperatorDescription.lessThanText' for 'less than' part." + 'xpack.data.kueryAutocomplete.lessThanOperatorDescription.lessThanText' for 'less than' part." /> ), fieldTypes: ['number', 'date', 'ip'], @@ -115,13 +115,13 @@ const operators = { '>': { description: ( <FormattedMessage - id="xpack.kueryAutocomplete.greaterThanOperatorDescription" + id="xpack.data.kueryAutocomplete.greaterThanOperatorDescription" defaultMessage="is {greaterThan} some value" values={{ greaterThan: <span className="kbnSuggestionItem__callout">{greaterThanText}</span>, }} description="Full text: 'is greater than some value'. See - 'xpack.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText' for 'greater than' part." + 'xpack.data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText' for 'greater than' part." /> ), fieldTypes: ['number', 'date', 'ip'], @@ -129,11 +129,11 @@ const operators = { ': *': { description: ( <FormattedMessage - id="xpack.kueryAutocomplete.existOperatorDescription" + id="xpack.data.kueryAutocomplete.existOperatorDescription" defaultMessage="{exists} in any form" values={{ exists: <span className="kbnSuggestionItem__callout">{existsText}</span> }} description="Full text: 'exists in any form'. See - 'xpack.kueryAutocomplete.existOperatorDescription.existsText' for 'exists' part." + 'xpack.data.kueryAutocomplete.existOperatorDescription.existsText' for 'exists' part." /> ), fieldTypes: undefined, diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.test.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.test.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.test.ts diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts similarity index 100% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/sort_prefix_first.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/sort_prefix_first.ts diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/types.ts similarity index 85% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/types.ts index c51b75e001b9f8..8e3146ab098484 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/types.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/types.ts @@ -5,7 +5,7 @@ */ import { CoreSetup } from 'kibana/public'; -import { esKuery, autocomplete } from '../../../../../../src/plugins/data/public'; +import { esKuery, autocomplete } from '../../../../../../../src/plugins/data/public'; export type KqlQuerySuggestionProvider<T = autocomplete.BasicQuerySuggestion> = ( core: CoreSetup diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.test.ts similarity index 96% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.test.ts index 5ffe30c877868f..14eeabda97d1a4 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.test.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.test.ts @@ -5,9 +5,9 @@ */ import { setupGetValueSuggestions } from './value'; import indexPatternResponse from './__fixtures__/index_pattern_response.json'; -import { coreMock } from '../../../../../../src/core/public/mocks'; -import { autocomplete, esKuery } from '../../../../../../src/plugins/data/public'; -import { setAutocompleteService } from '../services'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { autocomplete, esKuery } from '../../../../../../../src/plugins/data/public'; +import { setAutocompleteService } from '../../../services'; const mockKueryNode = (kueryNode: Partial<esKuery.KueryNode>) => (kueryNode as unknown) as esKuery.KueryNode; diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts similarity index 93% rename from x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts rename to x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts index 242b9ccba3508e..83b8024d8314de 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/kql_query_suggestion/value.ts +++ b/x-pack/plugins/data_enhanced/public/autocomplete/providers/kql_query_suggestion/value.ts @@ -7,8 +7,8 @@ import { flatten } from 'lodash'; import { escapeQuotes } from './lib/escape_kuery'; import { KqlQuerySuggestionProvider } from './types'; -import { getAutocompleteService } from '../services'; -import { autocomplete } from '../../../../../../src/plugins/data/public'; +import { getAutocompleteService } from '../../../services'; +import { autocomplete } from '../../../../../../../src/plugins/data/public'; const wrapAsSuggestions = (start: number, end: number, query: string, values: string[]) => values diff --git a/x-pack/plugins/data_enhanced/public/index.ts b/x-pack/plugins/data_enhanced/public/index.ts new file mode 100644 index 00000000000000..93b6b7a9571824 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { DataEnhancedPlugin, DataEnhancedSetup, DataEnhancedStart } from './plugin'; + +export const plugin = () => new DataEnhancedPlugin(); + +export { DataEnhancedSetup, DataEnhancedStart }; diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts new file mode 100644 index 00000000000000..14b5382bc85aaa --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -0,0 +1,33 @@ +/* + * 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 { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { setAutocompleteService } from './services'; +import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete'; + +export interface DataEnhancedSetupDependencies { + data: DataPublicPluginSetup; +} +export interface DataEnhancedStartDependencies { + data: DataPublicPluginStart; +} + +export type DataEnhancedSetup = ReturnType<DataEnhancedPlugin['setup']>; +export type DataEnhancedStart = ReturnType<DataEnhancedPlugin['start']>; + +export class DataEnhancedPlugin implements Plugin { + public setup(core: CoreSetup, plugins: DataEnhancedSetupDependencies) { + plugins.data.autocomplete.addQuerySuggestionProvider( + KUERY_LANGUAGE_NAME, + setupKqlQuerySuggestionProvider(core) + ); + } + + public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { + setAutocompleteService(plugins.data.autocomplete); + } +} diff --git a/x-pack/legacy/plugins/kuery_autocomplete/public/services.ts b/x-pack/plugins/data_enhanced/public/services.ts similarity index 70% rename from x-pack/legacy/plugins/kuery_autocomplete/public/services.ts rename to x-pack/plugins/data_enhanced/public/services.ts index 1ec48e597f6360..847b4c783fd7fd 100644 --- a/x-pack/legacy/plugins/kuery_autocomplete/public/services.ts +++ b/x-pack/plugins/data_enhanced/public/services.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { createGetterSetter } from '../../../../src/plugins/kibana_utils/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; export const [getAutocompleteService, setAutocompleteService] = createGetterSetter< DataPublicPluginStart['autocomplete'] diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 5e6edb95ec37a6..3b4b91de355c74 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -9,7 +9,7 @@ import { ConfigSchema } from './config'; import { Plugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; -export { PluginSetupContract, PluginStartContract } from './plugin'; +export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 7f53f47760f129..13d7127db78353 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract, PluginStartContract } from './plugin'; +import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; function createEncryptedSavedObjectsSetupMock() { return { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, usingEphemeralEncryptionKey: true, - } as jest.Mocked<PluginSetupContract>; + } as jest.Mocked<EncryptedSavedObjectsPluginSetup>; } function createEncryptedSavedObjectsStartMock() { return { isEncryptionError: jest.fn(), getDecryptedAsInternalUser: jest.fn(), - } as jest.Mocked<PluginStartContract>; + } as jest.Mocked<EncryptedSavedObjectsPluginStart>; } export const encryptedSavedObjectsMock = { diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index d9185251ca4664..a0218c51c2723c 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -20,13 +20,13 @@ import { import { EncryptedSavedObjectsAuditLogger } from './audit'; import { SavedObjectsSetup, setupSavedObjects } from './saved_objects'; -export interface PluginSetupContract { +export interface EncryptedSavedObjectsPluginSetup { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void }; usingEphemeralEncryptionKey: boolean; } -export interface PluginStartContract extends SavedObjectsSetup { +export interface EncryptedSavedObjectsPluginStart extends SavedObjectsSetup { isEncryptionError: (error: Error) => boolean; } @@ -59,7 +59,7 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup): Promise<PluginSetupContract> { + public async setup(core: CoreSetup): Promise<EncryptedSavedObjectsPluginSetup> { const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) .pipe(first()) .toPromise(); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index 42fd4417e238b1..f8bbfbc8bb33de 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -49,7 +49,6 @@ exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.59 10.059L7.35 5.18h1.3L8.4 10.06h-.81zm.394 1.901a.61.61 0 01-.448-.186.606.606 0 01-.186-.444c0-.174.062-.323.186-.446a.614.614 0 01.448-.184c.169 0 .315.06.44.182.124.122.186.27.186.448a.6.6 0 01-.189.446.607.607 0 01-.437.184zM2 14a1 1 0 01-.878-1.479l6-11a1 1 0 011.756 0l6 11A1 1 0 0114 14H2zm0-1h12L8 2 2 13z" fillRule="evenodd" @@ -189,7 +188,6 @@ exports[`APIKeysGridPage renders permission denied if user does not have require width={32} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M14 32l-.36-.14A21.07 21.07 0 010 12.07V5.44L14 .06l14 5.38v6.63a21.07 21.07 0 01-13.64 19.78L14 32zM2 6.82v5.25a19.08 19.08 0 0012 17.77 19.08 19.08 0 0012-17.77V6.82L14 2.2 2 6.82z" /> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap index 37db2e118861e8..a2741773f183bf 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap @@ -39,6 +39,7 @@ exports[`<SimplePrivilegeForm> renders without crashing 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index e9f2f946e98859..8d10e27df9694e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -176,6 +176,7 @@ exports[`<PrivilegeSpaceForm> renders without crashing 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap index 970cbfd03954a4..4789314d9f780b 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap @@ -67,7 +67,6 @@ exports[`<RolesGridPage /> renders permission denied if required 1`] = ` width={32} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M14 32l-.36-.14A21.07 21.07 0 010 12.07V5.44L14 .06l14 5.38v6.63a21.07 21.07 0 01-13.64 19.78L14 32zM2 6.82v5.25a19.08 19.08 0 0012 17.77 19.08 19.08 0 0012-17.77V6.82L14 2.2 2 6.82z" /> diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 17e49b8cf40d30..c0e86b289fe545 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -12,7 +12,7 @@ import { RecursiveReadonly, } from '../../../../src/core/server'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupContract, PluginSetupDependencies } from './plugin'; +import { Plugin, SecurityPluginSetup, PluginSetupDependencies } from './plugin'; // These exports are part of public Security plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. @@ -24,7 +24,7 @@ export { InvalidateAPIKeyParams, InvalidateAPIKeyResult, } from './authentication'; -export { PluginSetupContract }; +export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = { @@ -35,7 +35,7 @@ export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = { ], }; export const plugin: PluginInitializer< - RecursiveReadonly<PluginSetupContract>, + RecursiveReadonly<SecurityPluginSetup>, void, PluginSetupDependencies > = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index d5c08d5ab1ab94..ababf12c2be606 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract } from './plugin'; +import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; @@ -19,7 +19,7 @@ function createSetupMock() { mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), - __legacyCompat: {} as PluginSetupContract['__legacyCompat'], + __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index ce682d8b30eb76..57644182347398 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -49,7 +49,7 @@ export interface LegacyAPI { /** * Describes public Security plugin contract returned at the `setup` stage. */ -export interface PluginSetupContract { +export interface SecurityPluginSetup { authc: Authentication; authz: Pick<Authorization, 'actions' | 'checkPrivilegesWithRequest' | 'mode'>; @@ -166,7 +166,7 @@ export class Plugin { csp: core.http.csp, }); - return deepFreeze<PluginSetupContract>({ + return deepFreeze<SecurityPluginSetup>({ authc, authz: { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 24a994e836e87d..74e75fb8f12c79 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract as SecuritySetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { SpacesClient } from './spaces_client'; import { ConfigType, ConfigSchema } from '../../config'; import { GetSpacePurpose } from '../../../common/model/types'; @@ -224,17 +224,17 @@ describe('#getAll', () => { [ { purpose: undefined, - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'any', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'copySavedObjectsIntoSpace', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), }, ].forEach(scenario => { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index f964ae7d7ac32d..22c34c03368e36 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { omit } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { isReservedSpace } from '../../../common/is_reserved_space'; import { Space } from '../../../common/model/space'; import { SpacesAuditLogger } from '../audit_logger'; @@ -17,7 +17,7 @@ const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObject const PURPOSE_PRIVILEGE_MAP: Record< GetSpacePurpose, - (authorization: SecurityPluginSetupContract['authz']) => string + (authorization: SecurityPluginSetup['authz']) => string > = { any: authorization => authorization.actions.login, copySavedObjectsIntoSpace: authorization => @@ -28,7 +28,7 @@ export class SpacesClient { constructor( private readonly auditLogger: SpacesAuditLogger, private readonly debugLogger: (message: string) => void, - private readonly authorization: SecurityPluginSetupContract['authz'] | null, + private readonly authorization: SecurityPluginSetup['authz'] | null, private readonly callWithRequestSavedObjectRepository: any, private readonly config: ConfigType, private readonly internalSavedObjectRepository: any, diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index b8ef81c05f7aa4..52ff7eaee3d68c 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -14,7 +14,7 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; import { createDefaultSpace } from './lib/create_default_space'; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index f8ed58fa575518..95bda96d894615 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -8,7 +8,7 @@ import { map, take } from 'rxjs/operators'; import { Observable, Subscription } from 'rxjs'; import { Legacy } from 'kibana'; import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginSetup } from '../../../security/server'; import { LegacyAPI } from '../plugin'; import { SpacesClient } from '../lib/spaces_client'; import { ConfigType } from '../config'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 47e11817ffa5d4..abf56cd2d05b69 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -183,17 +183,6 @@ "common.ui.chrome.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "common.ui.chrome.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "common.ui.chrome.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", - "common.ui.directives.fieldNameIcons.booleanAriaLabel": "ブールフィールド", - "common.ui.directives.fieldNameIcons.conflictFieldAriaLabel": "矛盾フィールド", - "common.ui.directives.fieldNameIcons.dateFieldAriaLabel": "日付フィールド", - "common.ui.directives.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイント", - "common.ui.directives.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報図形", - "common.ui.directives.fieldNameIcons.ipAddressFieldAriaLabel": "IP アドレスフィールド", - "common.ui.directives.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 フィールド", - "common.ui.directives.fieldNameIcons.numberFieldAriaLabel": "数値フィールド", - "common.ui.directives.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド", - "common.ui.directives.fieldNameIcons.stringFieldAriaLabel": "文字列フィールド", - "common.ui.directives.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド", "common.ui.directives.paginate.size.allDropDownOptionLabel": "すべて", "common.ui.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "値は {min} と {max} の間でなければなりません", @@ -1114,6 +1103,17 @@ "kbn.discover.fieldChooser.searchPlaceHolder": "検索フィールド", "kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "フィールド設定を非表示", "kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "フィールド設定を表示", + "kbn.discover.fieldNameIcons.booleanAriaLabel": "ブールフィールド", + "kbn.discover.fieldNameIcons.conflictFieldAriaLabel": "矛盾フィールド", + "kbn.discover.fieldNameIcons.dateFieldAriaLabel": "日付フィールド", + "kbn.discover.fieldNameIcons.geoPointFieldAriaLabel": "地理ポイント", + "kbn.discover.fieldNameIcons.geoShapeFieldAriaLabel": "地理情報図形", + "kbn.discover.fieldNameIcons.ipAddressFieldAriaLabel": "IP アドレスフィールド", + "kbn.discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 フィールド", + "kbn.discover.fieldNameIcons.numberFieldAriaLabel": "数値フィールド", + "kbn.discover.fieldNameIcons.sourceFieldAriaLabel": "ソースフィールド", + "kbn.discover.fieldNameIcons.stringFieldAriaLabel": "文字列フィールド", + "kbn.discover.fieldNameIcons.unknownFieldAriaLabel": "不明なフィールド", "kbn.discover.histogram.partialData.bucketTooltipText": "選択された時間範囲にはこのバケット全体は含まれていませんが、一部データが含まれている可能性があります。", "kbn.discover.histogramOfFoundDocumentsAriaLabel": "発見されたドキュメントのヒストグラム", "kbn.discover.hitsPluralTitle": "{hits, plural, one {ヒット} other {ヒット}}", @@ -1198,8 +1198,6 @@ "kbn.embeddable.inspectorRequestDataTitle": "データ", "kbn.embeddable.inspectorRequestDescription": "このリクエストは Elasticsearch にクエリをかけ、検索データを取得します。", "kbn.embeddable.search.displayName": "検索", - "kbn.home.addData.addDataToKibanaDescription": "これらのソリューションで、データを作成済みのダッシュボードと監視システムへとすぐに変えることができます。", - "kbn.home.addData.addDataToKibanaTitle": "Kibana にデータを追加", "kbn.home.addData.apm.addApmButtonLabel": "APM を追加", "kbn.home.addData.apm.nameDescription": "APM は、集約内から自動的に詳細なパフォーマンスメトリックやエラーを集めます。", "kbn.home.addData.apm.nameTitle": "APM", @@ -6755,23 +6753,23 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "メトリックのオプションまたは値を選択できません", "xpack.infra.waffleTime.autoRefreshButtonLabel": "自動更新", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "更新中止", - "xpack.kueryAutocomplete.andOperatorDescription": "{bothArguments}がtrueであることを条件とする", - "xpack.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "両方の引数", - "xpack.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", - "xpack.kueryAutocomplete.equalOperatorDescription.equalsText": "一致する", - "xpack.kueryAutocomplete.existOperatorDescription": "いずれかの形式中に{exists}", - "xpack.kueryAutocomplete.existOperatorDescription.existsText": "存在する", - "xpack.kueryAutocomplete.filterResultsDescription": "{fieldName}を含む結果をフィルタリング", - "xpack.kueryAutocomplete.greaterThanOperatorDescription": "が一部の値{greaterThan}", - "xpack.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "より大きい", - "xpack.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "が一部の値{greaterThanOrEqualTo}", - "xpack.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "よりも大きいまたは等しい", - "xpack.kueryAutocomplete.lessThanOperatorDescription": "が一部の値{lessThan}", - "xpack.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "より小さい", - "xpack.kueryAutocomplete.lessThanOrEqualOperatorDescription": "が一部の値{lessThanOrEqualTo}", - "xpack.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "より小さいまたは等しい", - "xpack.kueryAutocomplete.orOperatorDescription": "{oneOrMoreArguments}がtrueであることを条件とする", - "xpack.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "1つ以上の引数", + "xpack.data.kueryAutocomplete.andOperatorDescription": "{bothArguments}がtrueであることを条件とする", + "xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "両方の引数", + "xpack.data.kueryAutocomplete.equalOperatorDescription": "一部の値に{equals}", + "xpack.data.kueryAutocomplete.equalOperatorDescription.equalsText": "一致する", + "xpack.data.kueryAutocomplete.existOperatorDescription": "いずれかの形式中に{exists}", + "xpack.data.kueryAutocomplete.existOperatorDescription.existsText": "存在する", + "xpack.data.kueryAutocomplete.filterResultsDescription": "{fieldName}を含む結果をフィルタリング", + "xpack.data.kueryAutocomplete.greaterThanOperatorDescription": "が一部の値{greaterThan}", + "xpack.data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "より大きい", + "xpack.data.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "が一部の値{greaterThanOrEqualTo}", + "xpack.data.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "よりも大きいまたは等しい", + "xpack.data.kueryAutocomplete.lessThanOperatorDescription": "が一部の値{lessThan}", + "xpack.data.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "より小さい", + "xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription": "が一部の値{lessThanOrEqualTo}", + "xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "より小さいまたは等しい", + "xpack.data.kueryAutocomplete.orOperatorDescription": "{oneOrMoreArguments}がtrueであることを条件とする", + "xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "1つ以上の引数", "xpack.lens.app.docLoadingError": "保存されたドキュメントの保存中にエラーが発生", "xpack.lens.app.docSavingError": "ドキュメントの保存中にエラーが発生", "xpack.lens.app.indexPatternLoadingError": "インデックスパターンの読み込み中にエラーが発生", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 86d9a69dc0900b..b97982b5c99dae 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -183,17 +183,6 @@ "common.ui.chrome.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "common.ui.chrome.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "common.ui.chrome.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", - "common.ui.directives.fieldNameIcons.booleanAriaLabel": "布尔字段", - "common.ui.directives.fieldNameIcons.conflictFieldAriaLabel": "冲突字段", - "common.ui.directives.fieldNameIcons.dateFieldAriaLabel": "日期字段", - "common.ui.directives.fieldNameIcons.geoPointFieldAriaLabel": "地理位置点字段", - "common.ui.directives.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状字段", - "common.ui.directives.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址字段", - "common.ui.directives.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 字段", - "common.ui.directives.fieldNameIcons.numberFieldAriaLabel": "数字字段", - "common.ui.directives.fieldNameIcons.sourceFieldAriaLabel": "源字段", - "common.ui.directives.fieldNameIcons.stringFieldAriaLabel": "字符串字段", - "common.ui.directives.fieldNameIcons.unknownFieldAriaLabel": "未知字段", "common.ui.directives.paginate.size.allDropDownOptionLabel": "全部", "common.ui.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", "common.ui.dualRangeControl.outsideOfRangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", @@ -1114,6 +1103,17 @@ "kbn.discover.fieldChooser.searchPlaceHolder": "搜索字段", "kbn.discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "隐藏字段设置", "kbn.discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "显示字段设置", + "kbn.discover.fieldNameIcons.booleanAriaLabel": "布尔字段", + "kbn.discover.fieldNameIcons.conflictFieldAriaLabel": "冲突字段", + "kbn.discover.fieldNameIcons.dateFieldAriaLabel": "日期字段", + "kbn.discover.fieldNameIcons.geoPointFieldAriaLabel": "地理位置点字段", + "kbn.discover.fieldNameIcons.geoShapeFieldAriaLabel": "几何形状字段", + "kbn.discover.fieldNameIcons.ipAddressFieldAriaLabel": "IP 地址字段", + "kbn.discover.fieldNameIcons.murmur3FieldAriaLabel": "Murmur3 字段", + "kbn.discover.fieldNameIcons.numberFieldAriaLabel": "数字字段", + "kbn.discover.fieldNameIcons.sourceFieldAriaLabel": "源字段", + "kbn.discover.fieldNameIcons.stringFieldAriaLabel": "字符串字段", + "kbn.discover.fieldNameIcons.unknownFieldAriaLabel": "未知字段", "kbn.discover.histogram.partialData.bucketTooltipText": "选定的时间范围不包括此整个存储桶,其可能包含部分数据。", "kbn.discover.histogramOfFoundDocumentsAriaLabel": "已找到文档的直方图", "kbn.discover.hitsPluralTitle": "{hits, plural, one {次命中} other {次命中}}", @@ -1198,8 +1198,6 @@ "kbn.embeddable.inspectorRequestDataTitle": "数据", "kbn.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "kbn.embeddable.search.displayName": "搜索", - "kbn.home.addData.addDataToKibanaDescription": "使用这些解决方案可快速将您的数据转换成预建仪表板和监测系统。", - "kbn.home.addData.addDataToKibanaTitle": "将数据添加到 Kibana", "kbn.home.addData.apm.addApmButtonLabel": "添加 APM", "kbn.home.addData.apm.nameDescription": "APM 自动从您的应用程序内收集深入全面的性能指标和错误。", "kbn.home.addData.apm.nameTitle": "APM", @@ -6754,23 +6752,23 @@ "xpack.infra.waffle.unableToSelectMetricErrorTitle": "无法选择指标选项或指标值。", "xpack.infra.waffleTime.autoRefreshButtonLabel": "自动刷新", "xpack.infra.waffleTime.stopRefreshingButtonLabel": "停止刷新", - "xpack.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为真", - "xpack.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "两个参数都", - "xpack.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", - "xpack.kueryAutocomplete.equalOperatorDescription.equalsText": "等于", - "xpack.kueryAutocomplete.existOperatorDescription": "以任意形式{exists}", - "xpack.kueryAutocomplete.existOperatorDescription.existsText": "存在", - "xpack.kueryAutocomplete.filterResultsDescription": "筛选包含 {fieldName} 的结果", - "xpack.kueryAutocomplete.greaterThanOperatorDescription": "{greaterThan}某一值", - "xpack.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "大于", - "xpack.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "{greaterThanOrEqualTo}某一值", - "xpack.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "大于或等于", - "xpack.kueryAutocomplete.lessThanOperatorDescription": "{lessThan}某一值", - "xpack.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "小于", - "xpack.kueryAutocomplete.lessThanOrEqualOperatorDescription": "{lessThanOrEqualTo}某一值", - "xpack.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "小于或等于", - "xpack.kueryAutocomplete.orOperatorDescription": "需要{oneOrMoreArguments}为真", - "xpack.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "一个或多个参数", + "xpack.data.kueryAutocomplete.andOperatorDescription": "需要{bothArguments}为真", + "xpack.data.kueryAutocomplete.andOperatorDescription.bothArgumentsText": "两个参数都", + "xpack.data.kueryAutocomplete.equalOperatorDescription": "{equals}某一值", + "xpack.data.kueryAutocomplete.equalOperatorDescription.equalsText": "等于", + "xpack.data.kueryAutocomplete.existOperatorDescription": "以任意形式{exists}", + "xpack.data.kueryAutocomplete.existOperatorDescription.existsText": "存在", + "xpack.data.kueryAutocomplete.filterResultsDescription": "筛选包含 {fieldName} 的结果", + "xpack.data.kueryAutocomplete.greaterThanOperatorDescription": "{greaterThan}某一值", + "xpack.data.kueryAutocomplete.greaterThanOperatorDescription.greaterThanText": "大于", + "xpack.data.kueryAutocomplete.greaterThanOrEqualOperatorDescription": "{greaterThanOrEqualTo}某一值", + "xpack.data.kueryAutocomplete.greaterThanOrEqualOperatorDescription.greaterThanOrEqualToText": "大于或等于", + "xpack.data.kueryAutocomplete.lessThanOperatorDescription": "{lessThan}某一值", + "xpack.data.kueryAutocomplete.lessThanOperatorDescription.lessThanText": "小于", + "xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription": "{lessThanOrEqualTo}某一值", + "xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "小于或等于", + "xpack.data.kueryAutocomplete.orOperatorDescription": "需要{oneOrMoreArguments}为真", + "xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "一个或多个参数", "xpack.lens.app.docLoadingError": "加载已保存文档时出错", "xpack.lens.app.docSavingError": "保存文档时出错", "xpack.lens.app.indexPatternLoadingError": "加载索引模式时出错", diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts index d7bee93f5c94b9..7194c642e7015e 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { Legacy } from 'kibana'; import KbnServer from '../../../../../../../src/legacy/server/kbn_server'; -import { PluginStartContract } from '../../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../../plugins/encrypted_saved_objects/server'; interface CheckAADRequest extends Hapi.Request { payload: { @@ -25,7 +25,8 @@ export default function(kibana: any) { name: 'aad-fixtures', init(server: Legacy.Server) { const newPlatform = ((server as unknown) as KbnServer).newPlatform; - const esoPlugin = newPlatform.start.plugins.encryptedSavedObjects as PluginStartContract; + const esoPlugin = newPlatform.start.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; server.route({ method: 'POST', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts index aab683df097404..3da9552df976c9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/delete.ts @@ -108,6 +108,58 @@ export default function createDeleteTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } }); + + it('should still be able to delete alert when AAD is broken', async () => { + const { body: createdAlert } = await supertest + .post(`${getUrlPrefix(space.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send(getTestAlertData()) + .expect(200); + + await supertest + .put(`${getUrlPrefix(space.id)}/api/saved_objects/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + + const response = await supertestWithoutAuth + .delete(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`) + .set('kbn-xsrf', 'foo') + .auth(user.username, user.password); + + switch (scenario.id) { + case 'no_kibana_privileges at space1': + case 'space_1_all at space2': + case 'global_read at space1': + expect(response.statusCode).to.eql(404); + expect(response.body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + objectRemover.add(space.id, createdAlert.id, 'alert'); + // Ensure task still exists + await getScheduledTask(createdAlert.scheduledTaskId); + break; + case 'superuser at space1': + case 'space_1_all at space1': + expect(response.statusCode).to.eql(204); + expect(response.body).to.eql(''); + try { + await getScheduledTask(createdAlert.scheduledTaskId); + throw new Error('Should have removed scheduled task'); + } catch (e) { + expect(e.status).to.eql(404); + } + break; + default: + throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); + } + }); }); } }); diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index b706dc8cce5469..597b522a88c51b 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -12,6 +12,8 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./log_entry_categories_tab')); + loadTestFile(require.resolve('./log_entry_rate_tab')); loadTestFile(require.resolve('./logs_source_configuration')); loadTestFile(require.resolve('./metrics_source_configuration')); loadTestFile(require.resolve('./link_to')); diff --git a/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts new file mode 100644 index 00000000000000..c703738e37228a --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const logsUi = getService('logsUi'); + const retry = getService('retry'); + + describe('Log Entry Categories Tab', function() { + this.tags('smoke'); + + describe('with a trial license', () => { + it('is visible', async () => { + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts new file mode 100644 index 00000000000000..95228a520aaa25 --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const logsUi = getService('logsUi'); + const retry = getService('retry'); + + describe('Log Entry Rate Tab', function() { + this.tags('smoke'); + + describe('with a trial license', () => { + it('is visible', async () => { + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 183acf3a980eeb..ecad5a40ec42e9 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -10,12 +10,14 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); - const infraLogStream = getService('infraLogStream'); + const logsUi = getService('logsUi'); const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); const pageObjects = getPageObjects(['common', 'infraLogs']); + const retry = getService('retry'); describe('Logs Source Configuration', function() { this.tags('smoke'); + before(async () => { await esArchiver.load('empty_kibana'); }); @@ -32,8 +34,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log indices to a pattern that matches nothing', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); const nameInput = await infraSourceConfigurationForm.getNameInput(); await nameInput.clearValueWithKeyboard({ charByChar: true }); @@ -47,13 +52,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the no indices screen when no indices match the pattern', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - await pageObjects.infraLogs.getNoLogsIndicesPrompt(); + await logsUi.logStreamPage.navigateTo(); + + await retry.try(async () => { + await logsUi.logStreamPage.getNoLogsIndicesPrompt(); + }); }); it('can change the log indices back to a pattern that matches something', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); const logIndicesInput = await infraSourceConfigurationForm.getLogIndicesInput(); await logIndicesInput.clearValueWithKeyboard({ charByChar: true }); @@ -63,16 +74,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the default log columns with their headers', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); + await logsUi.logStreamPage.navigateTo(); - expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']); + await retry.try(async () => { + const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - const logStreamEntries = await infraLogStream.getStreamEntries(); + expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']); + }); + + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry( + const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( firstLogStreamEntry ); @@ -80,32 +94,34 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log columns', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); await infraSourceConfigurationForm.removeAllLogColumns(); await infraSourceConfigurationForm.addTimestampLogColumn(); await infraSourceConfigurationForm.addFieldLogColumn('host.name'); - // await infraSourceConfigurationForm.moveLogColumn(0, 1); - await infraSourceConfigurationForm.saveConfiguration(); }); it('renders the changed log columns with their headers', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); + await logsUi.logStreamPage.navigateTo(); + + await retry.try(async () => { + const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - // TODO: make test more robust - // expect(columnHeaderLabels).to.eql(['host.name', 'Timestamp']); - expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']); + expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']); + }); - const logStreamEntries = await infraLogStream.getStreamEntries(); + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry( + const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( firstLogStreamEntry ); diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 73b91a61196bfa..9a879032fadc14 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -53,8 +53,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('pagination is cleared when filter criteria changes', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await pageObjects.uptime.changePage('next'); - // there should now be pagination data in the URL - await pageObjects.uptime.pageUrlContains('pagination'); await pageObjects.uptime.pageHasExpectedIds([ '0010-down', '0011-up', @@ -67,9 +65,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '0018-up', '0019-up', ]); + await retry.tryForTime(12000, async () => { + // there should now be pagination data in the URL + await pageObjects.uptime.pageUrlContains('pagination'); + }); await pageObjects.uptime.setStatusFilter('up'); - // ensure that pagination is removed from the URL - await pageObjects.uptime.pageUrlContains('pagination', false); await pageObjects.uptime.pageHasExpectedIds([ '0000-intermittent', '0001-up', @@ -82,6 +82,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '0008-up', '0009-up', ]); + await retry.tryForTime(12000, async () => { + // ensure that pagination is removed from the URL + await pageObjects.uptime.pageUrlContains('pagination', false); + }); }); describe('snapshot counts', () => { diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts index 6eb1349210bae3..1c58f8dc41eba0 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -18,12 +18,14 @@ export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProvide await pageObjects.common.navigateToApp('infraLogs'); }, - async getLogStream() { - return await testSubjects.find('logStream'); + async navigateToTab(logsUiTab: LogsUiTab) { + await pageObjects.common.navigateToActualUrl('infraLogs', `/logs/${logsUiTab}`); }, - async getNoLogsIndicesPrompt() { - return await testSubjects.find('noLogsIndicesPrompt'); + async getLogStream() { + return await testSubjects.find('logStream'); }, }; } + +type LogsUiTab = 'log-categories' | 'log-rate' | 'settings' | 'stream'; diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 84d5a792ae6cae..aec91ba9e90341 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -46,7 +46,7 @@ import { GrokDebuggerProvider } from './grok_debugger'; import { UserMenuProvider } from './user_menu'; import { UptimeProvider } from './uptime'; import { InfraSourceConfigurationFormProvider } from './infra_source_configuration_form'; -import { InfraLogStreamProvider } from './infra_log_stream'; +import { LogsUiProvider } from './logs_ui'; import { MachineLearningProvider } from './ml'; import { TransformProvider } from './transform'; @@ -88,7 +88,7 @@ export const services = { userMenu: UserMenuProvider, uptime: UptimeProvider, infraSourceConfigurationForm: InfraSourceConfigurationFormProvider, - infraLogStream: InfraLogStreamProvider, + logsUi: LogsUiProvider, ml: MachineLearningProvider, transform: TransformProvider, }; diff --git a/x-pack/test/functional/services/logs_ui/index.ts b/x-pack/test/functional/services/logs_ui/index.ts new file mode 100644 index 00000000000000..c70a8470aafce3 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/index.ts @@ -0,0 +1,18 @@ +/* + * 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'; +import { LogEntryCategoriesPageProvider } from './log_entry_categories'; +import { LogEntryRatePageProvider } from './log_entry_rate'; +import { LogStreamPageProvider } from './log_stream'; + +export function LogsUiProvider(context: FtrProviderContext) { + return { + logEntryCategoriesPage: LogEntryCategoriesPageProvider(context), + logEntryRatePage: LogEntryRatePageProvider(context), + logStreamPage: LogStreamPageProvider(context), + }; +} diff --git a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts new file mode 100644 index 00000000000000..b9a400b1556797 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/log_entry_categories.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 { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogEntryCategoriesPageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); + const testSubjects = getService('testSubjects'); + + return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('log-categories'); + }, + + async getSetupScreen(): Promise<WebElementWrapper> { + return await testSubjects.find('logEntryCategoriesSetupPage'); + }, + }; +} diff --git a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts new file mode 100644 index 00000000000000..96c69e85aa0a48 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/log_entry_rate.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 { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogEntryRatePageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); + const testSubjects = getService('testSubjects'); + + return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('log-rate'); + }, + + async getSetupScreen(): Promise<WebElementWrapper> { + return await testSubjects.find('logEntryRateSetupPage'); + }, + }; +} diff --git a/x-pack/test/functional/services/infra_log_stream.ts b/x-pack/test/functional/services/logs_ui/log_stream.ts similarity index 70% rename from x-pack/test/functional/services/infra_log_stream.ts rename to x-pack/test/functional/services/logs_ui/log_stream.ts index af113d3afffb42..ce37d2d5a60daa 100644 --- a/x-pack/test/functional/services/infra_log_stream.ts +++ b/x-pack/test/functional/services/logs_ui/log_stream.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../ftr_provider_context'; -import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; -export function InfraLogStreamProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); +export function LogStreamPageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); const retry = getService('retry'); + const testSubjects = getService('testSubjects'); return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('stream'); + }, + async getColumnHeaderLabels(): Promise<string[]> { const columnHeaderElements: WebElementWrapper[] = await testSubjects.findAll( '~logColumnHeader' @@ -35,5 +40,9 @@ export function InfraLogStreamProvider({ getService }: FtrProviderContext) { ): Promise<WebElementWrapper[]> { return await testSubjects.findAllDescendant('~logColumn', entryElement); }, + + async getNoLogsIndicesPrompt() { + return await testSubjects.find('noLogsIndicesPrompt'); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_table.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_table.ts index 0324b440548bc4..1d710a1c4cec72 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_table.ts @@ -92,7 +92,7 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F public async openResultsView() { await this.assertJobViewButtonExists(); await testSubjects.click('mlAnalyticsJobViewButton'); - await testSubjects.existOrFail('mlPageDataFrameAnalyticsExploration', { timeout: 5000 }); + await testSubjects.existOrFail('mlPageDataFrameAnalyticsExploration', { timeout: 20 * 1000 }); } public async filterWithSearchString(filter: string) { diff --git a/x-pack/test/functional/services/machine_learning/job_table.ts b/x-pack/test/functional/services/machine_learning/job_table.ts index eed6d180b4dca9..153a0ac477b47b 100644 --- a/x-pack/test/functional/services/machine_learning/job_table.ts +++ b/x-pack/test/functional/services/machine_learning/job_table.ts @@ -176,6 +176,7 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } public async assertJobRowFields(jobId: string, expectedRow: object) { + await this.refreshJobList(); const rows = await this.parseJobTable(); const jobRow = rows.filter(row => row.id === jobId)[0]; expect(jobRow).to.eql(expectedRow); diff --git a/x-pack/test/functional/services/uptime.ts b/x-pack/test/functional/services/uptime.ts index ed39f28aabbfad..1d8e0c97b99c4d 100644 --- a/x-pack/test/functional/services/uptime.ts +++ b/x-pack/test/functional/services/uptime.ts @@ -38,10 +38,10 @@ export function UptimeProvider({ getService }: FtrProviderContext) { await browser.pressKeys(browser.keys.ENTER); }, async goToNextPage() { - await testSubjects.click('xpack.uptime.monitorList.nextButton'); + await testSubjects.click('xpack.uptime.monitorList.nextButton', 5000); }, async goToPreviousPage() { - await testSubjects.click('xpack.uptime.monitorList.prevButton'); + await testSubjects.click('xpack.uptime.monitorList.prevButton', 5000); }, async setStatusFilterUp() { await testSubjects.click('xpack.uptime.filterBar.filterStatusUp'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts new file mode 100644 index 00000000000000..e8ed54571c77cd --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']); + const browser = getService('browser'); + const alerting = getService('alerting'); + + describe('Alert Details', function() { + const testRunUuid = uuid.v4(); + + before(async () => { + await pageObjects.common.navigateToApp('triggersActions'); + + const actions = await Promise.all([ + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${0}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${1}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + ]); + + const alert = await alerting.alerts.createAlwaysFiringWithActions( + `test-alert-${testRunUuid}`, + actions.map(action => ({ + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + })) + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + }); + + it('renders the alert details', async () => { + const headingText = await pageObjects.alertDetailsUI.getHeadingText(); + expect(headingText).to.be(`test-alert-${testRunUuid}`); + + const alertType = await pageObjects.alertDetailsUI.getAlertType(); + expect(alertType).to.be(`Always Firing`); + + const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels(); + expect(actionType).to.be(`Server log`); + expect(actionCount).to.be(`+1`); + }); + + it('should disable the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await enableSwitch.click(); + + const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + + it('shouldnt allow you to mute a disabled alert', async () => { + const disabledEnableSwitch = await testSubjects.find('enableSwitch'); + expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false'); + + const muteSwitch = await testSubjects.find('muteSwitch'); + expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch'); + const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute( + 'aria-checked' + ); + expect(isDisabledMuteAfterDisabling).to.eql('false'); + }); + + it('should reenable a disabled the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await enableSwitch.click(); + + const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should mute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should unmute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await muteSwitch.click(); + + const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 13f50a505b0b6f..307f39382a2363 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -12,6 +12,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const log = getService('log'); const browser = getService('browser'); + const alerting = getService('alerting'); describe('Home page', function() { before(async () => { @@ -55,6 +56,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertsList'); }); + + it('navigates to an alert details page', async () => { + const action = await alerting.actions.createAction({ + name: `server-log-${Date.now()}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }); + + const alert = await alerting.alerts.createAlwaysFiringWithAction( + `test-alert-${Date.now()}`, + { + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + } + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + // Verify url + expect(await browser.getCurrentUrl()).to.contain(`/alert/${alert.id}`); + + await alerting.alerts.deleteAlert(alert.id); + }); }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index c76f477c8cfbef..a771fbf85e0b6e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -12,5 +12,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./details')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts index df651c67c2c281..43162e92563703 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts @@ -12,13 +12,42 @@ export default function(kibana: any) { require: ['alerting'], name: 'alerts', init(server: any) { - const noopAlertType: AlertType = { - id: 'test.noop', - name: 'Test: Noop', - actionGroups: ['default'], - async executor() {}, - }; - server.plugins.alerting.setup.registerType(noopAlertType); + createNoopAlertType(server.plugins.alerting.setup); + createAlwaysFiringAlertType(server.plugins.alerting.setup); }, }); } + +function createNoopAlertType(setupContract: any) { + const noopAlertType: AlertType = { + id: 'test.noop', + name: 'Test: Noop', + actionGroups: ['default'], + async executor() {}, + }; + setupContract.registerType(noopAlertType); +} + +function createAlwaysFiringAlertType(setupContract: any) { + // Alert types + const alwaysFiringAlertType: any = { + id: 'test.always-firing', + name: 'Always Firing', + actionGroups: ['default', 'other'], + async executor(alertExecutorOptions: any) { + const { services, state } = alertExecutorOptions; + + services + .alertInstanceFactory('1') + .replaceState({ instanceStateValue: true }) + .scheduleActions('default', { + instanceContextValue: true, + }); + return { + globalStateValue: true, + groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, + }; + }, + }; + setupContract.registerType(alwaysFiringAlertType); +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts new file mode 100644 index 00000000000000..6d2038a6ba04c3 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts @@ -0,0 +1,26 @@ +/* + * 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 AlertDetailsPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async getHeadingText() { + return await testSubjects.getVisibleText('alertDetailsTitle'); + }, + async getAlertType() { + return await testSubjects.getVisibleText('alertTypeLabel'); + }, + async getActionsLabels() { + return { + actionType: await testSubjects.getVisibleText('actionTypeLabel'), + actionCount: await testSubjects.getVisibleText('actionCountLabel'), + }; + }, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/index.ts b/x-pack/test/functional_with_es_ssl/page_objects/index.ts index a068ba7dfe81d3..cfc44221a9c17f 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/index.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/index.ts @@ -6,8 +6,10 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { TriggersActionsPageProvider } from './triggers_actions_ui_page'; +import { AlertDetailsPageProvider } from './alert_details'; export const pageObjects = { ...xpackFunctionalPageObjects, triggersActionsUI: TriggersActionsPageProvider, + alertDetailsUI: AlertDetailsPageProvider, }; diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index a04ecc969a7e1f..ae66ac0ddddfb7 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -92,6 +92,10 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }; }); }, + async clickOnAlertInAlertsList(name: string) { + await this.searchAlerts(name); + await find.clickDisplayedByCssSelector(`[data-test-subj="alertsList"] [title="${name}"]`); + }, async changeTabs(tab: 'alertsTab' | 'connectorsTab') { return await testSubjects.click(tab); }, diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts new file mode 100644 index 00000000000000..9454a32757068b --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts @@ -0,0 +1,48 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Actions { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/actions' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAction(actionParams: { + name: string; + actionTypeId: string; + config: Record<string, any>; + secrets: Record<string, any>; + }) { + this.log.debug(`creating action ${actionParams.name}`); + + const { data: action, status: actionStatus, actionStatusText } = await this.axios.post( + `/api/action`, + actionParams + ); + if (actionStatus !== 200) { + throw new Error( + `Expected status code of 200, received ${actionStatus} ${actionStatusText}: ${util.inspect( + action + )}` + ); + } + + this.log.debug(`created action ${action.id}`); + return action; + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts new file mode 100644 index 00000000000000..1a31d4796d5bc4 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -0,0 +1,79 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Alerts { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/alerts' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAlwaysFiringWithActions( + name: string, + actions: Array<{ + id: string; + group: string; + params: Record<string, any>; + }> + ) { + this.log.debug(`creating alert ${name}`); + + const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + enabled: true, + name, + tags: ['foo'], + alertTypeId: 'test.always-firing', + consumer: 'bar', + schedule: { interval: '1m' }, + throttle: '1m', + actions, + params: {}, + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + + this.log.debug(`created alert ${alert.id}`); + + return alert; + } + + public async createAlwaysFiringWithAction( + name: string, + action: { + id: string; + group: string; + params: Record<string, any>; + } + ) { + return this.createAlwaysFiringWithActions(name, [action]); + } + + public async deleteAlert(id: string) { + this.log.debug(`deleting alert ${id}`); + + const { data: alert, status, statusText } = await this.axios.delete(`/api/alert/${id}`); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + this.log.debug(`deleted alert ${alert.id}`); + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/index.ts b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts new file mode 100644 index 00000000000000..e0aa827316c01a --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { format as formatUrl } from 'url'; + +import { Alerts } from './alerts'; +import { Actions } from './actions'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function AlertsServiceProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new (class AlertingService { + actions = new Actions(url, log); + alerts = new Alerts(url, log); + })(); +} diff --git a/x-pack/test/functional_with_es_ssl/services/index.ts b/x-pack/test/functional_with_es_ssl/services/index.ts index 6e96921c25a316..f04c2c980055d3 100644 --- a/x-pack/test/functional_with_es_ssl/services/index.ts +++ b/x-pack/test/functional_with_es_ssl/services/index.ts @@ -5,7 +5,9 @@ */ import { services as xpackFunctionalServices } from '../../functional/services'; +import { AlertsServiceProvider } from './alerting'; export const services = { ...xpackFunctionalServices, + alerting: AlertsServiceProvider, }; diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts index a194e477da7559..e61b8f24a1f69b 100644 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts @@ -8,8 +8,8 @@ import { Request } from 'hapi'; import { boomify, badRequest } from 'boom'; import { Legacy } from 'kibana'; import { - PluginSetupContract, - PluginStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; @@ -26,7 +26,7 @@ export default function esoPlugin(kibana: any) { path: '/api/saved_objects/get-decrypted-as-internal-user/{id}', async handler(request: Request) { const encryptedSavedObjectsStart = server.newPlatform.start.plugins - .encryptedSavedObjects as PluginStartContract; + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); try { return await encryptedSavedObjectsStart.getDecryptedAsInternalUser( @@ -44,7 +44,8 @@ export default function esoPlugin(kibana: any) { }, }); - (server.newPlatform.setup.plugins.encryptedSavedObjects as PluginSetupContract).registerType({ + (server.newPlatform.setup.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup).registerType({ type: SAVED_OBJECT_WITH_SECRET_TYPE, attributesToEncrypt: new Set(['privateProperty']), attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']), diff --git a/yarn.lock b/yarn.lock index a3acc2ae216c5a..caa5587a0bbd03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1953,10 +1953,10 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@18.2.1": - version "18.2.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-18.2.1.tgz#6ce6d0bd1d0541052d21f2918305524d71e91678" - integrity sha512-6C5tnWJTlBB++475i0vRoCsnz4JaYznb4zMNFLc+z5GY3vA3/E3AXTjmmBwybEicCCi3h1SnpJxZsgMakiZwRA== +"@elastic/eui@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-18.3.0.tgz#e21c6246624f694e2ae1c7c1f1a11b612faf260a" + integrity sha512-Rkj1rTtDa6iZMUF7pxYRojku1sLXzTU0FK1D9i0XE3H//exy3VyTV6qUlbdkiKXjO7emrgQqfzKDeXT+ZYztgg== dependencies: "@types/chroma-js" "^1.4.3" "@types/lodash" "^4.14.116" @@ -12591,6 +12591,11 @@ eslint-plugin-react-hooks@^2.3.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz#53e073961f1f5ccf8dd19558036c1fac8c29d99a" integrity sha512-gLKCa52G4ee7uXzdLiorca7JIQZPPXRAQDXV83J4bUEeUuc5pIEyZYAZ45Xnxe5IuupxEqHS+hUhSLIimK1EMw== +eslint-plugin-react-perf@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-perf/-/eslint-plugin-react-perf-3.2.3.tgz#e28d42d3a1f7ec3c8976a94735d8e17e7d652a45" + integrity sha512-bMiPt7uywwS1Ly25n752NE3Ei0XBZ3igplTkZ8GPJKyZVVUd3cHgzILGeQW2HIeAkzQ9zwk9HM6EcYDipdFk3Q== + eslint-plugin-react@^7.17.0: version "7.17.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz#a31b3e134b76046abe3cd278e7482bd35a1d12d7"